/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see .
*/
import {
clearTestEmails,
createAuthHarness,
createTestAccount,
findLastTestEmail,
listTestEmails,
type TestAccount,
} from '@fluxer/api/src/auth/tests/AuthTestUtils';
import type {ApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
import {createBuilder} from '@fluxer/api/src/test/TestRequestBuilder';
import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi} from 'vitest';
interface EmailChangeStartResponse {
ticket: string;
require_original: boolean;
original_proof?: string;
original_code_expires_at?: string;
resend_available_at?: string;
}
interface EmailChangeVerifyOriginalResponse {
original_proof: string;
}
interface EmailChangeRequestNewResponse {
ticket: string;
new_email: string;
new_code_expires_at: string;
resend_available_at?: string;
}
async function startEmailChange(
harness: ApiTestHarness,
account: TestAccount,
password: string,
): Promise {
return createBuilder(harness, account.token)
.post('/users/@me/email-change/start')
.body({password})
.execute();
}
async function verifyOriginalEmailChange(
harness: ApiTestHarness,
account: TestAccount,
ticket: string,
code: string,
password: string,
): Promise {
const resp = await createBuilder(harness, account.token)
.post('/users/@me/email-change/verify-original')
.body({ticket, code, password})
.execute();
return resp.original_proof;
}
async function requestNewEmailChange(
harness: ApiTestHarness,
account: TestAccount,
ticket: string,
newEmail: string,
originalProof: string,
password: string,
): Promise {
return createBuilder(harness, account.token)
.post('/users/@me/email-change/request-new')
.body({
ticket,
new_email: newEmail,
original_proof: originalProof,
password,
})
.execute();
}
describe('Email change resend cooldown', () => {
let harness: ApiTestHarness;
beforeAll(async () => {
harness = await createAuthHarness();
});
beforeEach(async () => {
vi.useFakeTimers();
await harness.reset();
await clearTestEmails(harness);
});
afterEach(() => {
vi.useRealTimers();
});
afterAll(async () => {
await harness?.shutdown();
});
it('enforces cooldown period for resending new email verification', async () => {
const account = await createTestAccount(harness);
const startResp = await startEmailChange(harness, account, account.password);
let originalProof: string;
if (startResp.require_original) {
const emails = await listTestEmails(harness, {recipient: account.email});
const originalEmail = findLastTestEmail(emails, 'email_change_original');
expect(originalEmail?.metadata?.code).toBeDefined();
const originalCode = originalEmail!.metadata!.code!;
originalProof = await verifyOriginalEmailChange(
harness,
account,
startResp.ticket,
originalCode,
account.password,
);
} else {
expect(startResp.original_proof).toBeDefined();
originalProof = startResp.original_proof!;
}
const newEmail = `cooldown-${Date.now()}@example.com`;
await requestNewEmailChange(harness, account, startResp.ticket, newEmail, originalProof, account.password);
await createBuilder(harness, account.token)
.post('/users/@me/email-change/resend-new')
.body({
ticket: startResp.ticket,
})
.expect(429)
.execute();
await vi.advanceTimersByTimeAsync(31000);
await createBuilder(harness, account.token)
.post('/users/@me/email-change/resend-new')
.body({
ticket: startResp.ticket,
})
.expect(204)
.execute();
});
});