Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
71 changes: 71 additions & 0 deletions src/analytics/analytics.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { ForbiddenException } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { AnalyticsController } from './analytics.controller';
import { AnalyticsService } from './analytics.service';
import { AuthenticatedUser } from '../common/decorators/current-user.decorator';
import { UserRole } from '../common/enums';

describe('AnalyticsController', () => {
let controller: AnalyticsController;
let analyticsService: {
forContributor: jest.Mock;
platformSummary: jest.Mock;
};

const userAlice: AuthenticatedUser = {
userId: 'user-1',
username: 'alice',
roles: [UserRole.CONTRIBUTOR],
};

const userBob: AuthenticatedUser = {
userId: 'user-2',
username: 'bob',
roles: [UserRole.CONTRIBUTOR],
};

beforeEach(async () => {
analyticsService = {
forContributor: jest.fn().mockResolvedValue({ userId: 'user-1' }),
platformSummary: jest.fn().mockResolvedValue({ totalBounties: 10 }),
};

const module: TestingModule = await Test.createTestingModule({
controllers: [AnalyticsController],
providers: [{ provide: AnalyticsService, useValue: analyticsService }],
}).compile();

controller = module.get(AnalyticsController);
});

describe('forContributor', () => {
it('returns analytics for the user', async () => {
const res = await controller.forContributor(
'user-1',
userAlice,
'2024-01-01',
'2024-06-01',
);
expect(res).toEqual({ userId: 'user-1' });
expect(analyticsService.forContributor).toHaveBeenCalledWith('user-1', {
from: '2024-01-01',
to: '2024-06-01',
});
});

it('throws 403 ForbiddenException when accessing another contributor analytics', async () => {
expect(() =>
controller.forContributor('user-1', userBob, undefined, undefined),
).toThrow(ForbiddenException);
expect(analyticsService.forContributor).not.toHaveBeenCalled();
});
});

describe('platformSummary', () => {
it('returns platform-wide analytics summary', async () => {
const res = await controller.platformSummary();
expect(res).toEqual({ totalBounties: 10 });
expect(analyticsService.platformSummary).toHaveBeenCalled();
});
});
});
79 changes: 79 additions & 0 deletions src/analytics/ttl-cache.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { TtlCache } from './ttl-cache';

describe('TtlCache', () => {
beforeEach(() => {
jest.useFakeTimers();
});

afterEach(() => {
jest.useRealTimers();
});

it('loads and caches data for the duration of the TTL', async () => {
const cache = new TtlCache<string>(5000);
const loader = jest.fn().mockResolvedValue('value-1');

const result1 = await cache.getOrLoad(loader);
expect(result1).toBe('value-1');
expect(loader).toHaveBeenCalledTimes(1);

// Call within TTL should return cached value
jest.advanceTimersByTime(2000);
const result2 = await cache.getOrLoad(loader);
expect(result2).toBe('value-1');
expect(loader).toHaveBeenCalledTimes(1);

// After TTL expires, loader should be called again
jest.advanceTimersByTime(4000);
loader.mockResolvedValueOnce('value-2');
const result3 = await cache.getOrLoad(loader);
expect(result3).toBe('value-2');
expect(loader).toHaveBeenCalledTimes(2);
});

it('coalesces concurrent misses so loader is only called once', async () => {
const cache = new TtlCache<string>(5000);
let resolveLoader!: (val: string) => void;
const loader = jest.fn(
() =>
new Promise<string>((resolve) => {
resolveLoader = resolve;
}),
);

const promise1 = cache.getOrLoad(loader);
const promise2 = cache.getOrLoad(loader);

resolveLoader('concurrent-result');

const [res1, res2] = await Promise.all([promise1, promise2]);
expect(res1).toBe('concurrent-result');
expect(res2).toBe('concurrent-result');
expect(loader).toHaveBeenCalledTimes(1);
});

it('handles invalidate while load is in flight', async () => {
const cache = new TtlCache<string>(5000);
let resolveLoader!: (val: string) => void;
const loader = jest.fn(
() =>
new Promise<string>((resolve) => {
resolveLoader = resolve;
}),
);

const promise1 = cache.getOrLoad(loader);
cache.invalidate();

resolveLoader('loaded-data');
const res = await promise1;
expect(res).toBe('loaded-data');

// Invalidate should have cleared entry, or subsequent call fetches if expired/invalidated
cache.invalidate();
loader.mockResolvedValueOnce('fresh-data');
const res2 = await cache.getOrLoad(loader);
expect(res2).toBe('fresh-data');
expect(loader).toHaveBeenCalledTimes(2);
});
});
110 changes: 110 additions & 0 deletions src/auth/strategies/jwt.strategy.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing';
import { ExtractJwt } from 'passport-jwt';
import { Request } from 'express';
import { JwtStrategy, cookieExtractor } from './jwt.strategy';
import { UsersService } from '../../users/users.service';

describe('JwtStrategy', () => {
let strategy: JwtStrategy;
let usersService: { findOneRaw: jest.Mock };
let configService: { get: jest.Mock };

beforeEach(async () => {
usersService = {
findOneRaw: jest.fn().mockResolvedValue({ id: 'user-1' }),
};
configService = {
get: jest.fn().mockReturnValue({ secret: 'test-secret' }),
};

const module: TestingModule = await Test.createTestingModule({
providers: [
JwtStrategy,
{ provide: UsersService, useValue: usersService },
{ provide: ConfigService, useValue: configService },
],
}).compile();

strategy = module.get(JwtStrategy);
});

describe('cookieExtractor', () => {
it('returns token from req.cookies if populated', () => {
const req = {
cookies: { access_token: 'cookie-token' },
} as unknown as Request;
expect(cookieExtractor(req)).toBe('cookie-token');
});

it('extracts token from cookie header when req.cookies is not available', () => {
const req = {
headers: { cookie: 'other=val; access_token=header-cookie-token' },
} as unknown as Request;
expect(cookieExtractor(req)).toBe('header-cookie-token');
});

it('returns null on malformed encoded cookie value', () => {
const req = {
headers: { cookie: 'access_token=%invalid' },
} as unknown as Request;
expect(cookieExtractor(req)).toBeNull();
});

it('returns null if no cookie header or access_token present', () => {
const req = { headers: {} } as unknown as Request;
expect(cookieExtractor(req)).toBeNull();
});
});

describe('bearer vs cookie precedence', () => {
it('prefers Bearer token in Authorization header over cookie', () => {
const extractor = ExtractJwt.fromExtractors([
ExtractJwt.fromAuthHeaderAsBearerToken(),
cookieExtractor,
]);

const reqWithBoth = {
headers: {
authorization: 'Bearer bearer-token-123',
cookie: 'access_token=cookie-token-456',
},
} as unknown as Request;

expect(extractor(reqWithBoth)).toBe('bearer-token-123');

const reqWithCookieOnly = {
headers: {
cookie: 'access_token=cookie-token-456',
},
} as unknown as Request;

expect(extractor(reqWithCookieOnly)).toBe('cookie-token-456');
});
});

describe('validate', () => {
it('returns userId and username for valid payload', async () => {
const result = await strategy.validate({
sub: 'user-1',
username: 'alice',
});
expect(result).toEqual({ userId: 'user-1', username: 'alice' });
expect(usersService.findOneRaw).toHaveBeenCalledWith('user-1');
});

it('throws UnauthorizedException when sub is missing', async () => {
await expect(
strategy.validate({ sub: '', username: 'alice' } as any),
).rejects.toThrow(new UnauthorizedException('Invalid token payload'));
});

it('throws 401 UnauthorizedException when user no longer exists', async () => {
usersService.findOneRaw.mockRejectedValue(new Error('User not found'));
await expect(
strategy.validate({ sub: 'deleted-user', username: 'alice' }),
).rejects.toThrow(new UnauthorizedException('User no longer exists'));
});
});
});
2 changes: 1 addition & 1 deletion src/auth/strategies/jwt.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export interface JwtPayload {
username: string;
}

function cookieExtractor(req: Request): string | null {
export function cookieExtractor(req: Request): string | null {
if (!req) return null;
// If cookie-parser is installed, req.cookies will be populated.
const cookies = (req as unknown as { cookies?: Record<string, string> })
Expand Down
79 changes: 79 additions & 0 deletions src/maintenance-pool/maintenance-pool.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { Test, TestingModule } from '@nestjs/testing';
import { MaintenancePoolController } from './maintenance-pool.controller';
import { MaintenancePoolService } from './maintenance-pool.service';
import { Request } from 'express';

describe('MaintenancePoolController', () => {
let controller: MaintenancePoolController;
let poolService: {
create: jest.Mock;
list: jest.Mock;
findOne: jest.Mock;
deposit: jest.Mock;
assignReward: jest.Mock;
};

beforeEach(async () => {
poolService = {
create: jest.fn().mockResolvedValue({ id: 'pool-1' }),
list: jest.fn().mockResolvedValue([{ id: 'pool-1' }]),
findOne: jest.fn().mockResolvedValue({ id: 'pool-1' }),
deposit: jest.fn().mockResolvedValue({ id: 'pool-1' }),
assignReward: jest.fn().mockResolvedValue({ id: 'pool-1' }),
};

const module: TestingModule = await Test.createTestingModule({
controllers: [MaintenancePoolController],
providers: [{ provide: MaintenancePoolService, useValue: poolService }],
}).compile();

controller = module.get(MaintenancePoolController);
});

it('delegates create to poolService with caller userId', async () => {
const req = { user: { userId: 'user-1' } } as unknown as Request;
const dto = { repositoryId: 'repo-1', targetAmount: '100' } as any;

const res = await controller.create(dto, req);
expect(res).toEqual({ id: 'pool-1' });
expect(poolService.create).toHaveBeenCalledWith(dto, 'user-1');
});

it('delegates list to poolService', async () => {
const res = await controller.list();
expect(res).toEqual([{ id: 'pool-1' }]);
expect(poolService.list).toHaveBeenCalled();
});

it('delegates findOne to poolService', async () => {
const res = await controller.findOne('pool-1');
expect(res).toEqual({ id: 'pool-1' });
expect(poolService.findOne).toHaveBeenCalledWith('pool-1');
});

it('delegates deposit to poolService', async () => {
const res = await controller.deposit('pool-1', {
amount: '50.00',
funderAddress: 'GFUNDER',
});
expect(res).toEqual({ id: 'pool-1' });
expect(poolService.deposit).toHaveBeenCalledWith('pool-1', '50.00', 'GFUNDER');
});

it('delegates assignReward to poolService', async () => {
const res = await controller.assignReward('pool-1', {
issueId: 'issue-1',
amount: '20.00',
recipientAddress: 'GRECIPIENT',
recipientId: 'user-2',
});
expect(res).toEqual({ id: 'pool-1' });
expect(poolService.assignReward).toHaveBeenCalledWith(
'pool-1',
'issue-1',
'20.00',
'GRECIPIENT',
'user-2',
);
});
});
Loading