// SPDX-License-Identifier: AGPL-3.0-or-later import { clearTestEmails, createAuthHarness, createUniqueEmail, createUniqueUsername, listTestEmails, registerUser, } from '@app/api/auth/tests/AuthTestUtils'; import type {ApiTestHarness} from '@app/api/test/ApiTestHarness'; import {HTTP_STATUS} from '@app/api/test/TestConstants'; import {createBuilderWithoutAuth} from '@app/api/test/TestRequestBuilder'; import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes'; import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest'; const HTTP_TOO_MANY_REQUESTS = 429; interface IpAuthorizationResponse { ticket?: string; ip_authorization_required?: boolean; email?: string; resend_available_in?: number; code?: string; message?: string; } interface ResendErrorResponse { code?: string; message?: string; resend_available_in?: number; retry_after?: number; } describe('Auth IP Authorization Resend', () => { let harness: ApiTestHarness; beforeAll(async () => { harness = await createAuthHarness(); }); beforeEach(async () => { await harness.reset(); await clearTestEmails(harness); }); afterAll(async () => { await harness?.shutdown(); }); async function triggerIpAuthorization(email: string, password: string, ip: string): Promise { const ipAuthResp = await createBuilderWithoutAuth(harness) .post('/auth/login') .body({email, password}) .header('x-forwarded-for', ip) .expect(HTTP_STATUS.FORBIDDEN) .execute(); expect(ipAuthResp.ip_authorization_required).toBe(true); expect(ipAuthResp.ticket).toBeTruthy(); return ipAuthResp; } describe('Resend rate limit enforcement', () => { it('returns resend_available_in and a matching Retry-After header when rate limited', async () => { const email = createUniqueEmail('ip-resend-cooldown'); const password = 'a-strong-password'; await registerUser(harness, { email, username: createUniqueUsername('resendcool'), global_name: 'Resend Cooldown User', password, date_of_birth: '2000-01-01', consent: true, }); const newIP = '10.230.240.250'; const ipAuthResp = await triggerIpAuthorization(email, password, newIP); const {response, json: errorResp} = await createBuilderWithoutAuth(harness) .post('/auth/ip-authorization/resend') .body({ticket: ipAuthResp.ticket}) .header('x-forwarded-for', newIP) .expect(HTTP_TOO_MANY_REQUESTS) .executeWithResponse(); expect(errorResp.code).toBe(APIErrorCodes.IP_AUTHORIZATION_RESEND_COOLDOWN); expect(typeof errorResp.resend_available_in).toBe('number'); expect(errorResp.resend_available_in).toBeGreaterThan(0); expect(response.headers.get('Retry-After')).toBe(String(errorResp.resend_available_in)); expect(errorResp.retry_after).toBe(errorResp.resend_available_in); }); }); describe('Resend with already-used ticket', () => { it('returns 400 when resend flag is already set on the ticket', async () => { const email = createUniqueEmail('ip-resend-used'); const password = 'a-strong-password'; const reg = await registerUser(harness, { email, username: createUniqueUsername('resendused'), global_name: 'Resend Used User', password, date_of_birth: '2000-01-01', consent: true, }); const ticket = `ticket-used-${Date.now()}`; const token = `token-used-${Date.now()}`; await createBuilderWithoutAuth(harness) .post('/test/auth/ip-authorization') .body({ ticket, token, user_id: reg.user_id, email, username: 'resend-used-user', client_ip: '203.0.113.10', user_agent: 'IntegrationTest/1.0', client_location: 'Testland', resend_used: true, created_at: Date.now() - 2 * 60 * 1000, ttl_seconds: 900, }) .execute(); const {response, json: errorResp} = await createBuilderWithoutAuth(harness) .post('/auth/ip-authorization/resend') .body({ticket}) .expect(HTTP_STATUS.BAD_REQUEST) .executeWithResponse(); expect(errorResp.code).toBe(APIErrorCodes.IP_AUTHORIZATION_RESEND_LIMIT_EXCEEDED); expect(response.headers.get('Retry-After')).toBeNull(); }); it('returns 400 after IP has already been authorized', async () => { const email = createUniqueEmail('ip-resend-after-auth'); const password = 'a-strong-password'; await registerUser(harness, { email, username: createUniqueUsername('resendafterauth'), global_name: 'Resend After Auth User', password, date_of_birth: '2000-01-01', consent: true, }); await clearTestEmails(harness); const newIP = '10.26.27.28'; const ipAuthResp = await triggerIpAuthorization(email, password, newIP); const emails = await listTestEmails(harness); const ipAuthEmail = emails.find((e) => e.type === 'ip_authorization' && e.to === email); expect(ipAuthEmail).toBeDefined(); const authToken = ipAuthEmail!.metadata['token']; expect(authToken).toBeTruthy(); await createBuilderWithoutAuth(harness) .post('/auth/authorize-ip') .body({token: authToken}) .header('x-forwarded-for', newIP) .expect(HTTP_STATUS.NO_CONTENT) .execute(); await createBuilderWithoutAuth(harness) .post('/auth/ip-authorization/resend') .body({ticket: ipAuthResp.ticket}) .header('x-forwarded-for', newIP) .expect(HTTP_STATUS.BAD_REQUEST) .execute(); }); }); describe('Resend with invalid ticket', () => { it('returns 400 when ticket has expired', async () => { const email = createUniqueEmail('ip-ticket-expired'); const password = 'a-strong-password'; await registerUser(harness, { email, username: createUniqueUsername('ticketexp'), global_name: 'Ticket Expired User', password, date_of_birth: '2000-01-01', consent: true, }); const newIP = '10.70.80.90'; const ipAuthResp = await triggerIpAuthorization(email, password, newIP); await createBuilderWithoutAuth(harness) .post('/test/auth/ip-authorization/expire') .body({ticket: ipAuthResp.ticket}) .execute(); await createBuilderWithoutAuth(harness) .post('/auth/ip-authorization/resend') .body({ticket: ipAuthResp.ticket}) .header('x-forwarded-for', newIP) .expect(HTTP_STATUS.BAD_REQUEST) .execute(); }); }); });