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
84 changes: 84 additions & 0 deletions src/auth/auth.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { AuthService } from './auth.service';
import { JwtService } from '@nestjs/jwt';
import { PrismaService } from '../prisma/prisma.service';

jest.mock('ethers', () => ({
verifyMessage: jest.fn(),
}));

import { verifyMessage } from 'ethers';

describe('AuthService Nonce behaviour', () => {
let authService: AuthService;
let jwtService: Partial<JwtService>;
let prisma: Partial<PrismaService>;

beforeEach(() => {
jwtService = {
sign: jest.fn().mockReturnValue('signed-token'),
};

prisma = {
wallet: {
findFirst: jest.fn().mockResolvedValue(null),
} as any,
} as Partial<PrismaService>;

authService = new AuthService(prisma as PrismaService, jwtService as JwtService);
});

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

it('generates a challenge and stores NonceRecord with camelCase fields', () => {
const address = '0xAbCd';
const message = authService.generateChallenge(address);
expect(typeof message).toBe('string');

const record = (authService as any).nonces.get(address.toLowerCase());
expect(record).toBeDefined();
expect(record.nonce).toBeDefined();
expect(record.createdAt).toBeDefined();
expect(typeof record.nonce).toBe('string');
expect(typeof record.createdAt).toBe('number');
});

it('cleans up expired nonces (cleanupNonces removes old createdAt entries)', () => {
const address = '0xdead';
authService.generateChallenge(address);
const map = (authService as any).nonces;
const record = map.get(address.toLowerCase());
// simulate expiry by setting createdAt far in the past

Check warning on line 52 in src/auth/auth.service.spec.ts

View workflow job for this annotation

GitHub Actions / Lint

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
record.createdAt = Date.now() - ((authService as any).NONCE_TTL + 1000);

// call private cleanup
(authService as any).cleanupNonces();
expect(map.has(address.toLowerCase())).toBe(false);
});

it('allows login and deletes nonce (single-use) and is case-insensitive', async () => {
const address = '0xAaBbCc';
const lower = address.toLowerCase();

// generate challenge
const challenge = authService.generateChallenge(address);
const record = (authService as any).nonces.get(lower);
const nonce = record.nonce;

// Prepare login DTO
const message = `Sign in to TruthBounty: ${nonce}`;
const signature = '0xsig';

// mock verifyMessage to return mixed-case recovered address
(verifyMessage as jest.Mock).mockReturnValue(address);

// call login
const result = await authService.login({ address: lower, signature, message } as any);
expect(result).toBeDefined();
expect(result.accessToken).toBe('signed-token');

Check warning on line 80 in src/auth/auth.service.spec.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe argument of type `any` assigned to a parameter of type `LoginDto`
// ensure nonce deleted (single-use)
expect((authService as any).nonces.has(lower)).toBe(false);
});
});
5 changes: 5 additions & 0 deletions src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import { JwtService } from '@nestjs/jwt';
import { verifyMessage } from 'ethers';
import { PrismaService } from '../prisma/prisma.service';
import { LoginDto } from './dto/login.dto';

export interface NonceRecord {
nonce: string;
createdAt: number;
}
import { RedisService } from '../redis/redis.service';

@Injectable()
Expand Down
Loading