Skip to content

Commit

Permalink
Merge pull request #49 from jembi/feat/swagger-docs
Browse files Browse the repository at this point in the history
Added swagger docs. Refactored code and tests.
  • Loading branch information
jacob-khoza-symb committed Sep 12, 2024
2 parents 7bb87cb + efe2d9e commit 58ae104
Show file tree
Hide file tree
Showing 22 changed files with 2,095 additions and 89 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@
"axios": "^1.7.5",
"next": "14.2.5",
"next-auth": "^4.24.7",
"next-swagger-doc": "^0.4.0",
"prisma-field-encryption": "^1.5.2",
"react": "^18",
"react-dom": "^18",
"swagger-ui-react": "^5.17.14",
"zod": "^3.23.8"
},
"devDependencies": {
Expand Down
12 changes: 12 additions & 0 deletions src/app/api/docs/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { getApiDocs } from "@/lib/swagger";

import ReactSwagger from "./react-swagger";

export default async function IndexPage() {
const spec = await getApiDocs();
return (
<section className="container">
<ReactSwagger spec={spec} />
</section>
);
}
14 changes: 14 additions & 0 deletions src/app/api/docs/react-swagger.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use client';

import SwaggerUI from 'swagger-ui-react';
import 'swagger-ui-react/swagger-ui.css';

type Props = {
spec: Record<string, any>,
};

function ReactSwagger({ spec }: Props) {
return <SwaggerUI spec={spec} />;
}

export default ReactSwagger;
36 changes: 36 additions & 0 deletions src/app/api/v1/server-configs/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,27 @@ const repo = container.get<IServerConfigRepository>(
ServerConfigRepositoryToken,
);

/**
* @swagger
* /api/v1/server-configs:
* post:
* tags: [Admin]
* description: Create a server config.
* requestBody:
* content:
* application/json:
* schema:
* type: object
* $ref: '#/components/schemas/CreateServerConfig'
* responses:
* 200:
* description: Create Server Config
* content:
* application/json:
* schema:
* type: object
* $ref: '#/components/schemas/ServerConfig'
*/
export async function POST(request: Request) {
let dto: CreateServerConfigDto = await request.json();
try {
Expand All @@ -29,6 +50,21 @@ export async function POST(request: Request) {
}
}

/**
* @swagger
* /api/v1/server-configs:
* get:
* tags: [Admin]
* description: Gets all server configs
* responses:
* 200:
* description: Server Configs
* content:
* application/json:
* schema:
* type: object
* $ref: '#/components/schemas/ServerConfig'
*/
export async function GET(request: Request) {
const serverConfigs = await getServerConfigsUseCase({ repo });
return NextResponse.json(
Expand Down
27 changes: 27 additions & 0 deletions src/app/api/v1/share-links/[id]/accesses/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,33 @@ import { getSingleSHLinkUseCase } from "@/usecases/shlinks/get-single-shlink";
const repo = container.get<ISHLinkAccessRepository>(SHLinkAccessRepositoryToken);
const shlinkRepo = container.get<ISHLinkRepository>(SHLinkRepositoryToken);

/**
* @swagger
* /api/v1/shlinks/{id}/accesses:
* post:
* tags: [Share Link Accesses]
* description: Get Share link Accesses.
* parameters:
* - name: id
* in: path
* description: A string representing the share link's unique identifier.
* required: true
* requestBody:
* content:
* application/json:
* schema:
* type: object
* $ref: '#/components/schemas/SHLinkAccessRequest'
* responses:
* 200:
* description: Share Link Accesses
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: '#/components/schemas/SHLinkAccess'
*/
export async function POST(request: Request, params: {id: string }) {
try{
const dto: SHLinkAccessRequestDto = await request.json();
Expand Down
8 changes: 4 additions & 4 deletions src/app/api/v1/share-links/[id]/deactivate/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { getUserProfile } from '@/app/utils/authentication';
import { mapModelToDto } from '@/mappers/shlink-mapper';
import { deactivateSHLinksUseCase } from '@/usecases/shlinks/deactivate-shlink';

import { GET } from './route';
import { DELETE } from './route';

jest.mock('@/usecases/shlinks/deactivate-shlink', () => ({
deactivateSHLinksUseCase: jest.fn(),
Expand All @@ -26,7 +26,7 @@ jest.mock('@/app/utils/authentication', () => ({
getUserProfile: jest.fn(),
}));

describe('GET /api/v1/share-link/[id]/deactivate', () => {
describe('DELETE /api/v1/share-link/[id]/deactivate', () => {
const mockId: string = '1';

const mockModel = {
Expand Down Expand Up @@ -61,7 +61,7 @@ describe('GET /api/v1/share-link/[id]/deactivate', () => {

const mockRequest = () =>
new NextRequest('http://localhost/api/share-link/1/deactivate', {
method: 'GET',
method: 'DELETE',
});

beforeEach(() => {
Expand All @@ -74,7 +74,7 @@ describe('GET /api/v1/share-link/[id]/deactivate', () => {
(mapModelToDto as jest.Mock).mockReturnValue(mockDto);

const request = mockRequest();
const response = await GET(request, {params: {id: 'user-123456',}});
const response = await DELETE(request, {params: {id: 'user-123456',}});

expect(response).toBeInstanceOf(NextResponse);
expect(response.status).toBe(200);
Expand Down
22 changes: 21 additions & 1 deletion src/app/api/v1/share-links/[id]/deactivate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,27 @@ import { deactivateSHLinksUseCase } from "@/usecases/shlinks/deactivate-shlink";

const repo = container.get<ISHLinkRepository>(SHLinkRepositoryToken);

export async function GET(request: Request, { params }: { params: { id: string } }) {
/**
* @swagger
* /api/v1/shlinks/{id}/deactivate:
* delete:
* tags: [Share Links]
* description: Deactivate a share link.
* parameters:
* - name: id
* in: path
* description: A string representing the share link's unique identifier.
* required: true
* responses:
* 200:
* description: Deactivated Share Link
* content:
* application/json:
* schema:
* type: object
* $ref: '#/components/schemas/SHLinkMini'
*/
export async function DELETE(request: Request, { params }: { params: { id: string } }) {
try{
const user = await getUserProfile(request);
const result = await deactivateSHLinksUseCase({ repo }, { id: params.id, user });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@ jest.mock('@/usecases/patient/get-patient-data');
jest.mock('@/usecases/shlinks/get-single-shlink');
jest.mock('@/usecases/users/get-user');

describe('GET /api/v1/[id]/[endpointId]', () => {
describe('GET /api/v1/share-links/[id]/endpoints/[endpointId]', () => {
const mockRequest = (ticketId: string | null) => {
const url = new URL(
'http://localhost/api/v1/123/endpoint?ticket=' + ticketId,
'http://localhost/api/v1/share-links/12356/endpoints/endpoint?ticket=' +
ticketId,
);
return new NextRequest(url.toString(), { method: 'GET' });
};
Expand Down Expand Up @@ -66,16 +67,18 @@ describe('GET /api/v1/[id]/[endpointId]', () => {
(getAccessTicketUseCase as jest.Mock).mockResolvedValue(mockTicket);
(getSingleSHLinkUseCase as jest.Mock).mockResolvedValue(null);

const request = mockRequest('valid-ticket');
const response = await GET(request, { params: mockParams });
const request = mockRequest('123456789');
const response = await GET(request, {
params: { id: 'abc', endpointId: '' },
});

expect(response).toBeInstanceOf(NextResponse);
expect(response.status).toBe(404);
const responseBody = await response.json();
expect(responseBody).toEqual({ message: NOT_FOUND });
});

it('should return 404 if user is not found', async () => {
it('should return 401 if user is not authorized', async () => {
(getAccessTicketUseCase as jest.Mock).mockResolvedValue(mockTicket);
(getSingleSHLinkUseCase as jest.Mock).mockResolvedValue(mockShlink);
(getUserUseCase as jest.Mock).mockResolvedValue(null);
Expand All @@ -84,9 +87,9 @@ describe('GET /api/v1/[id]/[endpointId]', () => {
const response = await GET(request, { params: mockParams });

expect(response).toBeInstanceOf(NextResponse);
expect(response.status).toBe(404);
expect(response.status).toBe(401);
const responseBody = await response.json();
expect(responseBody).toEqual({ message: NOT_FOUND });
expect(responseBody).toEqual({ message: UNAUTHORIZED_REQUEST });
});

it('should return 200 with patient data if everything is valid', async () => {
Expand All @@ -110,7 +113,7 @@ describe('GET /api/v1/[id]/[endpointId]', () => {
expect(jsonResponse).toEqual(mockPatientData);
});

it('should return 404 if patient data is not found', async () => {
it('should return 401 if patient data is not found', async () => {
(getAccessTicketUseCase as jest.Mock).mockResolvedValue(mockTicket);
(getSingleSHLinkUseCase as jest.Mock).mockResolvedValue(mockShlink);
(getUserUseCase as jest.Mock).mockResolvedValue(mockUser);
Expand All @@ -120,8 +123,8 @@ describe('GET /api/v1/[id]/[endpointId]', () => {
const response = await GET(request, { params: mockParams });

expect(response).toBeInstanceOf(NextResponse);
expect(response.status).toBe(404);
expect(response.status).toBe(401);
const responseBody = await response.json();
expect(responseBody).toEqual({ message: NOT_FOUND });
expect(responseBody).toEqual({ message: UNAUTHORIZED_REQUEST });
});
});
31 changes: 26 additions & 5 deletions src/app/api/v1/share-links/[id]/endpoints/[endpointId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,29 @@ const serverConfigRepo = container.get<IServerConfigRepository>(
ServerConfigRepositoryToken,
);

/**
* @swagger
* /api/v1/shlinks/{id}/endpoints/{endpointId}:
* get:
* tags: [Share Link Endpoints]
* description: Get a share link endpoint.
* parameters:
* - name: id
* in: path
* description: A string representing the share link's unique identifier.
* required: true
* - name: endpointId
* in: path
* description: A string representing the share link endpoint's unique identifier.
* required: true
* responses:
* 200:
* description: Get Share Link
* content:
* application/json:
* schema:
* type: object
*/
export async function GET(
request: Request,
{ params }: { params: { id: string; endpointId: string } },
Expand All @@ -45,22 +68,20 @@ export async function GET(
ticketId,
);

if (!ticket) {
if (!ticket || ticket.getSHLinkId() !== params.id) {
return NextResponse.json(
{ message: UNAUTHORIZED_REQUEST },
{ status: 401 },
);
}

if (ticket.getSHLinkId() !== params.id) {
return NextResponse.json({ message: NOT_FOUND }, { status: 404 });
}

const shlink = await getSingleSHLinkUseCase(
{ repo: shlinkRepo },
{ id: params.id },
);

if(!shlink) return NextResponse.json({ message: NOT_FOUND }, { status: 404 });

const user = await getUserUseCase(
{ repo: userRepo },
{ userId: shlink.getUserId() },
Expand Down
40 changes: 33 additions & 7 deletions src/app/api/v1/share-links/[id]/endpoints/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { NextResponse } from 'next/server';

import { NOT_FOUND } from '@/app/constants/http-constants';
import { handleApiValidationError } from '@/app/utils/error-handler';
import { container, ServerConfigRepositoryToken, SHLinkEndpointRepositoryToken, SHLinkRepositoryToken } from '@/container';
import {
CreateSHLinkEndpointDto,
SHLinkEndpointDto,
} from '@/domain/dtos/shlink-endpoint';
import prisma from '@/infrastructure/clients/prisma';
import { ServerConfigPrismaRepository } from '@/infrastructure/repositories/prisma/server-config-repository';
import { SHLinkEndpointPrismaRepository } from '@/infrastructure/repositories/prisma/shlink-endpoint-repository';
import { SHLinkPrismaRepository } from '@/infrastructure/repositories/prisma/shlink-repository';
import { IServerConfigRepository } from '@/infrastructure/repositories/interfaces/server-config-repository';
import { ISHLinkEndpointRepository } from '@/infrastructure/repositories/interfaces/shlink-endpoint-repository';
import { ISHLinkRepository } from '@/infrastructure/repositories/interfaces/shlink-repository';
import {
mapDtoToModel,
mapModelToDto as mapModelToDtoEndpoint,
Expand All @@ -19,10 +19,36 @@ import { getServerConfigsUseCase } from '@/usecases/server-configs/get-server-co
import { addEndpointUseCase } from '@/usecases/shlink-endpoint/add-endpoint';
import { getSingleSHLinkUseCase } from '@/usecases/shlinks/get-single-shlink';

const shlRepo = new SHLinkPrismaRepository(prisma);
const shlEndpointRepo = new SHLinkEndpointPrismaRepository(prisma);
const serverConfigRepo = new ServerConfigPrismaRepository(prisma);
const shlRepo = container.get<ISHLinkRepository>(SHLinkRepositoryToken);
const shlEndpointRepo = container.get<ISHLinkEndpointRepository>(SHLinkEndpointRepositoryToken);
const serverConfigRepo = container.get<IServerConfigRepository>(ServerConfigRepositoryToken);

/**
* @swagger
* /api/v1/shlinks/{id}/endpoints:
* post:
* tags: [Share Link Endpoints]
* description: Create a share link endpoint.
* parameters:
* - name: id
* in: path
* description: A string representing the share link's unique identifier.
* required: true
* requestBody:
* content:
* application/json:
* schema:
* type: object
* $ref: '#/components/schemas/CreateSHLinkEndpoint'
* responses:
* 200:
* description: Get Share Link
* content:
* application/json:
* schema:
* type: object
* $ref: '#/components/schemas/SHLinkEndpoint'
*/
export async function POST(
request: Request,
{ params }: { params: { id: string } },
Expand Down
Loading

0 comments on commit 58ae104

Please sign in to comment.