Skip to content

Commit

Permalink
Merge pull request #47 from jembi/feat/get-shlink-accesses
Browse files Browse the repository at this point in the history
Feat/get shlink accesses
  • Loading branch information
jacob-khoza-symb authored Sep 11, 2024
2 parents 306f15b + 676f1a9 commit 4571c5d
Show file tree
Hide file tree
Showing 12 changed files with 343 additions and 42 deletions.
107 changes: 107 additions & 0 deletions src/app/api/v1/share-links/[id]/accesses/route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/**
* @jest-environment node
*/

import { NOT_FOUND } from "@/app/constants/http-constants";
import { handleApiValidationError } from "@/app/utils/error-handler";
import { mapModelToDto } from "@/mappers/shlink-access-mapper";
import { getSHLinkAccessesUseCase } from "@/usecases/shlink-access/get-shlink-accesses";
import { getSingleSHLinkUseCase } from "@/usecases/shlinks/get-single-shlink";

import { POST } from "./route";

jest.mock("@/container", () => ({
container: {
get: jest.fn(),
},
SHLinkAccessRepositoryToken: Symbol('SHLinkAccessRepositoryToken'),
SHLinkRepositoryToken: Symbol('SHLinkRepositoryToken'),
}));

jest.mock("@/usecases/shlink-access/get-shlink-accesses", () => ({
getSHLinkAccessesUseCase: jest.fn(),
}));

jest.mock("@/usecases/shlinks/get-single-shlink", () => ({
getSingleSHLinkUseCase: jest.fn(),
}));

jest.mock("@/mappers/shlink-access-mapper", () => ({
mapModelToDto: jest.fn(),
}));

jest.mock("@/app/utils/error-handler", () => ({
handleApiValidationError: jest.fn(),
}));

describe("POST function", () => {
let mockGetSingleSHLinkUseCase: jest.Mock;
let mockGetSHLinkAccessesUseCase: jest.Mock;
let mockMapModelToDto: jest.Mock;
let mockHandleApiValidationError: jest.Mock;

beforeEach(() => {
mockGetSingleSHLinkUseCase = getSingleSHLinkUseCase as jest.Mock;
mockGetSHLinkAccessesUseCase = getSHLinkAccessesUseCase as jest.Mock;
mockMapModelToDto = mapModelToDto as jest.Mock;
mockHandleApiValidationError = handleApiValidationError as jest.Mock;
});

it("should return 404 if shlink is not found", async () => {
// Arrange
const request = new Request("http://localhost", {
method: "POST",
body: JSON.stringify({ managementToken: "token" }),
});
const params = { id: "non-existent-id" };

mockGetSingleSHLinkUseCase.mockResolvedValue(null);

// Act
const response = await POST(request, params);

// Assert
expect(response.status).toBe(404);
expect(await response.json()).toEqual({ message: NOT_FOUND });
});

it("should return 200 with access data if shlink is found", async () => {
// Arrange
const request = new Request("http://localhost", {
method: "POST",
body: JSON.stringify({ managementToken: "token" }),
});
const params = { id: "existent-id" };
const shlink = { getId: jest.fn().mockReturnValue("shlink-id") };
const accesses = [{ id: "access-id-1" }, { id: "access-id-2" }];
const dto = { id: "access-id-1" };

mockGetSingleSHLinkUseCase.mockResolvedValue(shlink);
mockGetSHLinkAccessesUseCase.mockResolvedValue(accesses);
mockMapModelToDto.mockImplementation((model) => ({ id: model.id }));

// Act
const response = await POST(request, params);

// Assert
expect(response.status).toBe(200);
expect(await response.json()).toEqual(accesses.map(x => ({ id: x.id })));
});

it("should handle errors and call handleApiValidationError", async () => {
// Arrange
const request = new Request("http://localhost", {
method: "POST",
body: JSON.stringify({ managementToken: "token" }),
});
const params = { id: "some-id" };

mockGetSingleSHLinkUseCase.mockRejectedValue(new Error("Test error"));

// Act
await POST(request, params);

// Assert
expect(mockHandleApiValidationError).toHaveBeenCalled();
});
});
30 changes: 30 additions & 0 deletions src/app/api/v1/share-links/[id]/accesses/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { NextResponse } from "next/server";

import { NOT_FOUND } from "@/app/constants/http-constants";
import { handleApiValidationError } from "@/app/utils/error-handler";
import { container, SHLinkAccessRepositoryToken, SHLinkRepositoryToken } from "@/container";
import { SHLinkAccessRequestDto } from "@/domain/dtos/shlink-access";
import { ISHLinkAccessRepository } from "@/infrastructure/repositories/interfaces/shlink-access-repository";
import { ISHLinkRepository } from "@/infrastructure/repositories/interfaces/shlink-repository";
import { mapModelToDto } from "@/mappers/shlink-access-mapper";
import { getSHLinkAccessesUseCase } from "@/usecases/shlink-access/get-shlink-accesses";
import { getSingleSHLinkUseCase } from "@/usecases/shlinks/get-single-shlink";

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

export async function POST(request: Request, params: {id: string }) {
try{
const dto: SHLinkAccessRequestDto = await request.json();
const shlink = await getSingleSHLinkUseCase({ repo: shlinkRepo }, { id: params.id, managementToken: dto.managementToken });
if(!shlink){
return NextResponse.json({ message: NOT_FOUND }, { status: 404 });
}

const accesses = await getSHLinkAccessesUseCase({ repo }, { shlinkId: shlink.getId() });
return NextResponse.json(accesses.map(x => mapModelToDto(x)), { status: 200 });
}
catch(error){
return handleApiValidationError(error);
}
}
26 changes: 7 additions & 19 deletions src/app/api/v1/share-links/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/
import { NextRequest, NextResponse } from "next/server";

import { validateUser } from "@/app/utils/authentication";
import { getUserProfile, validateUser } from "@/app/utils/authentication";
import { handleApiValidationError } from "@/app/utils/error-handler";
import { CreateSHLinkDto, SHLinkDto } from "@/domain/dtos/shlink";
import { SHLinkModel } from "@/domain/models/shlink";
Expand Down Expand Up @@ -135,7 +135,7 @@ describe("API Route Handlers", () => {

describe("GET /shlink", () => {
it("should handle successful GET request with valid user_id", async () => {
(validateUser as jest.Mock).mockResolvedValue({id: '1234567890'});
(getUserProfile as jest.Mock).mockResolvedValue({id: '1234567890'});
(getSHLinkUseCase as jest.Mock).mockResolvedValue([mockEntity]);
(mapEntityToModel as jest.Mock).mockReturnValue(mockModel);
(mapModelToMiniDto as jest.Mock).mockReturnValue({
Expand All @@ -144,11 +144,11 @@ describe("API Route Handlers", () => {
// Map other properties as necessary
});

const request = new NextRequest('http://localhost/api/share-link?user_id=1234567890', { method: 'GET' });
const request = new NextRequest('http://localhost/api/share-link', { method: 'GET' });

const response = await GET(request);

expect(validateUser).toHaveBeenCalledWith(request, '1234567890');
expect(getUserProfile).toHaveBeenCalledWith(request);
expect(response.status).toBe(200);
expect(response).toBeInstanceOf(NextResponse);

Expand All @@ -160,29 +160,17 @@ describe("API Route Handlers", () => {
}]);
});

it("should handle missing user_id", async () => {
const request = new NextRequest('http://localhost/api/share-link', { method: 'GET' });

const response = await GET(request);

expect(response.status).toBe(404);
expect(response).toBeInstanceOf(NextResponse);

const json = await response.json();
expect(json).toEqual({ message: 'Not Found' });
});

it("should handle errors during GET request", async () => {
const error = new Error('Database error');
(validateUser as jest.Mock).mockResolvedValue({id: '1234567890'});
(getUserProfile as jest.Mock).mockResolvedValue({id: '1234567890'});
(getSHLinkUseCase as jest.Mock).mockRejectedValue(error);
(handleApiValidationError as jest.Mock).mockReturnValue(NextResponse.json({ message: 'Database error' }, { status: 500 }));

const request = new NextRequest('http://localhost/api/share-link?user_id=1234567890', { method: 'GET' });
const request = new NextRequest('http://localhost/api/share-link', { method: 'GET' });

const response = await GET(request);

expect(validateUser).toHaveBeenCalledWith(request, '1234567890');
expect(getUserProfile).toHaveBeenCalledWith(request);
expect(handleApiValidationError).toHaveBeenCalledWith(error);
expect(response).toBeInstanceOf(NextResponse);
expect(response.status).toBe(500);
Expand Down
17 changes: 5 additions & 12 deletions src/app/api/v1/share-links/route.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import { NextResponse } from "next/server";

import { NOT_FOUND } from "@/app/constants/http-constants";
import { validateUser } from "@/app/utils/authentication";
import { getUserProfile, validateUser } from "@/app/utils/authentication";
import { handleApiValidationError } from "@/app/utils/error-handler";
import { container, SHLinkRepositoryToken } from "@/container";
import { CreateSHLinkDto, SHLinkDto } from "@/domain/dtos/shlink";
import { SHLinkEntity } from "@/entities/shlink";
import { ISHLinkRepository } from "@/infrastructure/repositories/interfaces/shlink-repository";
import {
mapDtoToModel,
mapEntityToModel,
mapModelToDto,
mapModelToMiniDto
} from "@/mappers/shlink-mapper";
Expand All @@ -32,15 +29,11 @@ export async function POST(request: Request) {
}

export async function GET(request: Request) {
const url = new URL(request.url);
const userId = url.searchParams.get('user_id');

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

try{
await validateUser(request, userId);
const newShlink:SHLinkEntity[] = await getSHLinkUseCase({ repo }, { user_id: userId })
return NextResponse.json(newShlink.map(shlink => mapModelToMiniDto(mapEntityToModel(shlink))), { status: 200 });
const user = await getUserProfile(request);

const newShlink = await getSHLinkUseCase({ repo }, { user_id: user.id })
return NextResponse.json(newShlink.map(shlink => mapModelToMiniDto(shlink)), { status: 200 });
}
catch(error){
return handleApiValidationError(error);
Expand Down
2 changes: 1 addition & 1 deletion src/app/utils/authentication.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { NextRequest } from "next/server";
import { getToken } from "next-auth/jwt";

import { UNAUTHORIZED_REQUEST } from "../constants/http-constants";
import { NextRequest } from "next/server";

export interface UserProfile{
name: string;
Expand Down
10 changes: 10 additions & 0 deletions src/domain/dtos/shlink-access.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export class SHLinkAccessRequestDto {
managementToken: string
}

export class SHLinkAccessDto {
shlinkId: string;
accessTime: Date;
recipient: string;
id?: string;
}
74 changes: 73 additions & 1 deletion src/mappers/shlink-access-mapper.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { SHLinkAccessDto } from '@/domain/dtos/shlink-access';
import { SHLinkAccessModel } from '@/domain/models/shlink-access';
import { SHLinkAccessEntity } from '@/entities/shlink-access';

import { mapModelToEntity } from './shlink-access-mapper';
import { mapModelToEntity, mapEntityToModel, mapModelToDto } from './shlink-access-mapper';

// Mock the SHLinkAccessModel class
jest.mock('@/domain/models/shlink-access', () => {
Expand Down Expand Up @@ -54,3 +55,74 @@ describe('mapModelToEntity', () => {
expect(result).toBeUndefined();
});
});

describe('mapEntityToModel', () => {
it('should map a SHLinkAccessEntity to SHLinkAccessModel', () => {
const accessTime = new Date('2024-09-01T12:00:00Z');
const recipient = '[email protected]';
const shlinkId = 'abc123';
const id = 'def456';

const entity: SHLinkAccessEntity = {
access_time: accessTime,
recipient: recipient,
shlink_id: shlinkId,
id: id,
};

const expectedModel = new SHLinkAccessModel(shlinkId, accessTime, recipient, id);

const result = mapEntityToModel(entity);

expect(result.getId()).toEqual(expectedModel.getId());
expect(result.getAccessTime()).toEqual(expectedModel.getAccessTime());
expect(result.getRecipient()).toEqual(expectedModel.getRecipient());
expect(result.getSHLinkId()).toEqual(expectedModel.getSHLinkId());
});

it('should return undefined if SHLinkAccessEntity is undefined', () => {
const result = mapEntityToModel(undefined);

expect(result).toBeUndefined();
});

it('should return undefined if SHLinkAccessEntity is null', () => {
const result = mapEntityToModel(null as any);

expect(result).toBeUndefined();
});
});

describe('mapModelToDto', () => {
it('should map a SHLinkAccessModel to SHLinkAccessDto', () => {
const accessTime = new Date('2024-09-01T12:00:00Z');
const recipient = '[email protected]';
const shlinkId = 'abc123';
const id = 'def456';

const model = new SHLinkAccessModel(shlinkId, accessTime, recipient, id);

const expectedDto: SHLinkAccessDto = {
accessTime: accessTime,
recipient: recipient,
shlinkId: shlinkId,
id: id,
};

const result = mapModelToDto(model);

expect(result).toEqual(expectedDto);
});

it('should return undefined if SHLinkAccessModel is undefined', () => {
const result = mapModelToDto(undefined);

expect(result).toBeUndefined();
});

it('should return undefined if SHLinkAccessModel is null', () => {
const result = mapModelToDto(null as any);

expect(result).toBeUndefined();
});
});
20 changes: 20 additions & 0 deletions src/mappers/shlink-access-mapper.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { SHLinkAccessDto } from '@/domain/dtos/shlink-access';
import { SHLinkAccessModel } from '@/domain/models/shlink-access';
import { SHLinkAccessEntity } from '@/entities/shlink-access';

Expand All @@ -13,3 +14,22 @@ export const mapModelToEntity = (
}
: undefined;
};


export const mapEntityToModel = (shlinkAccessEntity?: SHLinkAccessEntity): SHLinkAccessModel | undefined => {
return shlinkAccessEntity ? new SHLinkAccessModel(
shlinkAccessEntity.shlink_id,
shlinkAccessEntity.access_time,
shlinkAccessEntity.recipient,
shlinkAccessEntity.id
) : undefined;
}

export const mapModelToDto = (shlinkAccessModel?: SHLinkAccessModel): SHLinkAccessDto | undefined => {
return shlinkAccessModel ? {
id: shlinkAccessModel.getId(),
shlinkId: shlinkAccessModel.getSHLinkId(),
accessTime: shlinkAccessModel.getAccessTime(),
recipient: shlinkAccessModel.getRecipient(),
} : undefined;
}
Loading

0 comments on commit 4571c5d

Please sign in to comment.