Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 0 additions & 25 deletions src/common/utils/cursor-pagination.util.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { CursorPagination } from '../interfaces';

const encodeCursor = (id: string) => Buffer.from(id).toString('base64');
export const decodeCursor = (cursor: string | undefined) => {
if (!cursor) return undefined;
return Buffer.from(cursor, 'base64').toString('utf-8');
Expand All @@ -16,30 +15,6 @@ export const decodeCompositeCursor = <T>(cursorString: string): T | undefined =>
return JSON.parse(jsonString) as T;
};

export const paginateSingle = <T>(
items: T[],
limit: number,
prevCursor: string | undefined,
getId: (item: T) => bigint | string,
): CursorPagination => {
const hasNextPage = items.length > limit;
let nextCursor: string | null = null;

if (hasNextPage) {
const nextItem = items.pop();
if (nextItem) {
const id = getId(nextItem);
nextCursor = encodeCursor(id.toString());
}
}

return {
cursor: prevCursor || null,
nextCursor,
hasNextPage,
};
};

export const paginateComposite = <T, C extends Record<string, unknown>>(
items: T[],
limit: number,
Expand Down
7 changes: 4 additions & 3 deletions src/notifications/utils/fcm-notification-body-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,17 +88,18 @@ export function buildFcmNotificationText(opts: {
locale,
} = opts;

const templates = locale ? TEMPLATES[locale] : TEMPLATES[DEFAULT_LOCALE];
const effectiveLocale = locale ?? DEFAULT_LOCALE;
const templates = TEMPLATES[effectiveLocale];

const leadActor = previewActors[0] ?? 'Someone';
const remainingCount = Math.max(0, (totalActorCount ?? 1) - 1);

let key = 'generic';
const params: Record<string, unknown> = {};

if (locale === LanguageCode.AR && isAggregated && remainingCount > 0) {
if (effectiveLocale === LanguageCode.AR && isAggregated && remainingCount > 0) {
params.sar = remainingCount === 1 ? '' : 'ون';
} else if (locale === LanguageCode.EN && isAggregated && remainingCount > 0) {
} else if (effectiveLocale === LanguageCode.EN && isAggregated && remainingCount > 0) {
params.s = remainingCount === 1 ? '' : 's';
}
switch (notificationType) {
Expand Down
198 changes: 196 additions & 2 deletions test/auth/jwt-auth.guard.spec.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,212 @@
/* eslint-disable @typescript-eslint/unbound-method */
import { Test, TestingModule } from '@nestjs/testing';
import { ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { JwtAuthGuard } from 'src/auth/guards';
import { OPTIONAL_AUTH_KEY } from 'src/common/decorators/optional-auth.decorator';
import { RequestUser } from 'src/common/interfaces';

describe('JwtAuthGurad', () => {
describe('JwtAuthGuard', () => {
let guard: JwtAuthGuard;
let reflector: jest.Mocked<Reflector>;

beforeEach(async () => {
const mockReflector = {
getAllAndOverride: jest.fn(),
};

const module: TestingModule = await Test.createTestingModule({
providers: [JwtAuthGuard],
providers: [JwtAuthGuard, { provide: Reflector, useValue: mockReflector }],
}).compile();

guard = module.get<JwtAuthGuard>(JwtAuthGuard);
reflector = module.get(Reflector);
});

it('should be defined', () => {
expect(guard).toBeDefined();
});

describe('canActivate', () => {
it('should call super.canActivate', () => {
const mockContext = {
switchToHttp: jest.fn(),
getHandler: jest.fn(),
getClass: jest.fn(),
} as unknown as ExecutionContext;

const superCanActivateSpy = jest
.spyOn(Object.getPrototypeOf(JwtAuthGuard.prototype), 'canActivate')
.mockReturnValue(true);

const result = guard.canActivate(mockContext);

expect(superCanActivateSpy).toHaveBeenCalledWith(mockContext);
expect(result).toBe(true);

superCanActivateSpy.mockRestore();
});
});

describe('handleRequest', () => {
let mockContext: ExecutionContext;

beforeEach(() => {
mockContext = {
getHandler: jest.fn(),
getClass: jest.fn(),
switchToHttp: jest.fn(),
} as unknown as ExecutionContext;
});

it('should return user when user is authenticated', () => {
const mockUser: RequestUser = {
id: '1',
};

reflector.getAllAndOverride.mockReturnValue(false);

const result = guard.handleRequest(
null,
mockUser,
null,
mockContext,
) as unknown as RequestUser | null;

expect(result).toEqual(mockUser);

expect(reflector.getAllAndOverride).toHaveBeenCalledWith(OPTIONAL_AUTH_KEY, [
mockContext.getHandler(),
mockContext.getClass(),
]);
});

it('should return null when user is not authenticated and auth is optional', () => {
reflector.getAllAndOverride.mockReturnValue(true);

const result = guard.handleRequest(
null,
null,
null,
mockContext,
) as unknown as RequestUser | null;

expect(result).toBeNull();
expect(reflector.getAllAndOverride).toHaveBeenCalledWith(OPTIONAL_AUTH_KEY, [
mockContext.getHandler(),
mockContext.getClass(),
]);
});

it('should throw UnauthorizedException when user is not authenticated and auth is required', () => {
reflector.getAllAndOverride.mockReturnValue(false);

expect(
() => guard.handleRequest(null, null, null, mockContext) as unknown as RequestUser | null,
).toThrow(UnauthorizedException);
});

it('should throw error when error is provided and user is null', () => {
const customError = new Error('Custom auth error');
reflector.getAllAndOverride.mockReturnValue(false);

expect(
() =>
guard.handleRequest(
customError,
null,
null,
mockContext,
) as unknown as RequestUser | null,
).toThrow('Custom auth error');
});

it('should return user even when auth is optional and user exists', () => {
const mockUser: RequestUser = {
id: '2',
};

reflector.getAllAndOverride.mockReturnValue(true);

const result = guard.handleRequest(
null,
mockUser,
null,
mockContext,
) as unknown as RequestUser | null;

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

it('should handle user with all properties', () => {
const mockUser: RequestUser = {
id: '1',
};

reflector.getAllAndOverride.mockReturnValue(false);

const result = guard.handleRequest(
null,
mockUser,
null,
mockContext,
) as unknown as RequestUser | null;

expect(result).toEqual(mockUser);
expect(result!.id).toBe('1');
});

it('should prioritize error over UnauthorizedException', () => {
const customError = new Error('Token expired');
reflector.getAllAndOverride.mockReturnValue(false);

expect(
() =>
guard.handleRequest(
customError,
null,
null,
mockContext,
) as unknown as RequestUser | null,
).toThrow('Token expired');
});

it('should check both handler and class for optional auth decorator', () => {
const mockHandler = jest.fn();
const mockClass = jest.fn();

mockContext = {
getHandler: jest.fn().mockReturnValue(mockHandler),
getClass: jest.fn().mockReturnValue(mockClass),
switchToHttp: jest.fn(),
} as unknown as ExecutionContext;

reflector.getAllAndOverride.mockReturnValue(true);

guard.handleRequest(null, null, null, mockContext);

expect(reflector.getAllAndOverride).toHaveBeenCalledWith(OPTIONAL_AUTH_KEY, [
mockHandler,
mockClass,
]);
});

it('should handle bigint user id correctly', () => {
const mockUser: RequestUser = {
id: '9007199254740991', // Large BigInt
};

reflector.getAllAndOverride.mockReturnValue(false);

const result = guard.handleRequest(
null,
mockUser,
null,
mockContext,
) as unknown as RequestUser | null;

expect(result?.id).toBe('9007199254740991');
expect(typeof result?.id).toBe('string');
});
});
});
Loading
Loading