205 lines
7.8 KiB
TypeScript
205 lines
7.8 KiB
TypeScript
/**
|
|
* Sequential auth + profile + API key flow against the running hosted-gateway
|
|
* stack. Mirrors cipherdolls/core/test/auth.ts shape: one `describeAuth()` that
|
|
* exercises each persona end-to-end, with state shared across `it()` blocks
|
|
* via module-level `let` bindings.
|
|
*
|
|
* What this covers:
|
|
* - Email + password signup (with verification token echoed back in dev/test).
|
|
* - Email verification → account activation → 50 xBZZ welcome grant credited.
|
|
* - JWT-authed /users/me and /accounts/me reads.
|
|
* - API key creation, listing, JWT-or-key auth interchange, deletion.
|
|
* - /api-keys returns the persona's keys only.
|
|
* - Login round-trip returns a fresh JWT for the same user.
|
|
*/
|
|
import { describe, it, expect } from 'bun:test';
|
|
import {
|
|
api,
|
|
get,
|
|
post,
|
|
del,
|
|
auth,
|
|
signupEmail,
|
|
verifyEmail,
|
|
loginEmail,
|
|
createApiKey,
|
|
waitForQueuesEmpty,
|
|
} from './helpers';
|
|
|
|
const WELCOME_XBZZ_WEI = (50n * 10n ** 18n).toString();
|
|
|
|
export function describeAuth(): void {
|
|
describe('auth + profile + api-keys (e2e)', () => {
|
|
|
|
/* ── Alice: full signup → verify → welcome grant ────────────── */
|
|
|
|
it('signs up Alice via email/password', async () => {
|
|
const email = `alice-${Date.now()}@hosted.test`;
|
|
const password = 'correct-horse-battery-staple';
|
|
const { jwt, userId, verificationToken } = await signupEmail(email, password);
|
|
expect(jwt).toBeTruthy();
|
|
expect(userId).toBeTruthy();
|
|
expect(verificationToken).toBeTruthy();
|
|
auth.alice = { email, password, jwt, userId };
|
|
(auth.alice as any).verificationToken = verificationToken;
|
|
});
|
|
|
|
it('Alice can read /users/me with the issued JWT (email unverified)', async () => {
|
|
const { status, body } = await get('/users/me', auth.alice.jwt!);
|
|
expect(status).toBe(200);
|
|
expect(body).toMatchObject({
|
|
id: auth.alice.userId,
|
|
email: auth.alice.email,
|
|
role: 'USER',
|
|
emailVerified: false,
|
|
authProvider: 'email',
|
|
});
|
|
});
|
|
|
|
it('Alice has no welcome grant yet (account pending email verify)', async () => {
|
|
await waitForQueuesEmpty();
|
|
const { status, body } = await get('/accounts/me', auth.alice.jwt!);
|
|
expect(status).toBe(200);
|
|
expect(body.balanceXbzzWei).toBe('0');
|
|
expect(body.reservedXbzzWei).toBe('0');
|
|
});
|
|
|
|
it('Alice verifies her email → balance credited with 50 xBZZ', async () => {
|
|
const token = (auth.alice as any).verificationToken as string;
|
|
await verifyEmail(token);
|
|
await waitForQueuesEmpty();
|
|
|
|
const { status, body } = await get('/accounts/me', auth.alice.jwt!);
|
|
expect(status).toBe(200);
|
|
expect(body.balanceXbzzWei).toBe(WELCOME_XBZZ_WEI);
|
|
expect(body.reservedXbzzWei).toBe('0');
|
|
expect(body.availableXbzzWei).toBe(WELCOME_XBZZ_WEI);
|
|
|
|
const me = await get('/users/me', auth.alice.jwt!);
|
|
expect(me.body.emailVerified).toBe(true);
|
|
});
|
|
|
|
it('Alice can list her ledger and see the welcomeGrant row', async () => {
|
|
const { status, body } = await get('/accounts/me/ledger', auth.alice.jwt!);
|
|
expect(status).toBe(200);
|
|
expect(Array.isArray(body)).toBe(true);
|
|
expect(body.length).toBe(1);
|
|
expect(body[0].kind).toBe('welcomeGrant');
|
|
expect(body[0].deltaXbzzWei).toBe(WELCOME_XBZZ_WEI);
|
|
});
|
|
|
|
/* ── API keys ─────────────────────────────────────────────── */
|
|
|
|
it('Alice creates an API key', async () => {
|
|
const { id, key } = await createApiKey(auth.alice.jwt!, 'alice-key');
|
|
expect(id).toBeTruthy();
|
|
expect(key.startsWith('t4t-live-')).toBe(true);
|
|
auth.alice.apiKey = key;
|
|
auth.alice.apiKeyId = id;
|
|
});
|
|
|
|
it('Alice can read /users/me with her API key (no JWT)', async () => {
|
|
const { status, body } = await get('/users/me', auth.alice.apiKey);
|
|
expect(status).toBe(200);
|
|
expect(body.id).toBe(auth.alice.userId);
|
|
});
|
|
|
|
it('Alice lists /api-keys and sees only her key', async () => {
|
|
const { status, body } = await get('/api-keys', auth.alice.jwt!);
|
|
expect(status).toBe(200);
|
|
expect(body.length).toBe(1);
|
|
expect(body[0].id).toBe(auth.alice.apiKeyId);
|
|
expect(body[0].name).toBe('alice-key');
|
|
// Secret value is never echoed after creation.
|
|
expect(body[0].key).toBeUndefined();
|
|
});
|
|
|
|
/* ── Bob: isolation ───────────────────────────────────────── */
|
|
|
|
it('Bob signs up, verifies, and gets his own welcome grant', async () => {
|
|
const email = `bob-${Date.now()}@hosted.test`;
|
|
const password = 'tr0ub4dor&3';
|
|
const signup = await signupEmail(email, password);
|
|
await verifyEmail(signup.verificationToken);
|
|
await waitForQueuesEmpty();
|
|
const { id: apiKeyId, key: apiKey } = await createApiKey(signup.jwt, 'bob-key');
|
|
auth.bob = {
|
|
email,
|
|
password,
|
|
jwt: signup.jwt,
|
|
userId: signup.userId,
|
|
apiKey,
|
|
apiKeyId,
|
|
};
|
|
|
|
const accountRes = await get('/accounts/me', signup.jwt);
|
|
expect(accountRes.body.balanceXbzzWei).toBe(WELCOME_XBZZ_WEI);
|
|
});
|
|
|
|
it("Bob cannot see Alice's API keys", async () => {
|
|
const { status, body } = await get('/api-keys', auth.bob.jwt!);
|
|
expect(status).toBe(200);
|
|
expect(body.length).toBe(1);
|
|
expect(body[0].id).toBe(auth.bob.apiKeyId);
|
|
});
|
|
|
|
it("Bob cannot delete Alice's API key", async () => {
|
|
const { status } = await del(`/api-keys/${auth.alice.apiKeyId}`, auth.bob.jwt!);
|
|
expect(status).toBe(404);
|
|
});
|
|
|
|
/* ── Login round-trip ─────────────────────────────────────── */
|
|
|
|
it('Alice logs in with email + password and gets a fresh JWT', async () => {
|
|
const { jwt, userId } = await loginEmail(auth.alice.email!, auth.alice.password!);
|
|
expect(userId).toBe(auth.alice.userId);
|
|
// The new JWT still resolves to the same user.
|
|
const { status, body } = await get('/users/me', jwt);
|
|
expect(status).toBe(200);
|
|
expect(body.id).toBe(auth.alice.userId);
|
|
});
|
|
|
|
it('login with wrong password is rejected', async () => {
|
|
const { status } = await post('/auth/email/login', {
|
|
email: auth.alice.email,
|
|
password: 'WRONG',
|
|
});
|
|
expect(status).toBe(401);
|
|
});
|
|
|
|
it('signup with an existing email is rejected (409)', async () => {
|
|
const { status } = await post('/auth/email/signup', {
|
|
email: auth.alice.email,
|
|
password: 'another-password-123',
|
|
});
|
|
expect(status).toBe(409);
|
|
});
|
|
|
|
/* ── Unauthed access ──────────────────────────────────────── */
|
|
|
|
it('rejects /users/me with no token (401)', async () => {
|
|
const { status } = await api('GET', '/users/me');
|
|
expect(status).toBe(401);
|
|
});
|
|
|
|
it('rejects /accounts/me with garbage token (401)', async () => {
|
|
const { status } = await get('/accounts/me', 'not-a-real-token');
|
|
expect(status).toBe(401);
|
|
});
|
|
|
|
/* ── Key deletion ─────────────────────────────────────────── */
|
|
|
|
it('Alice can delete her API key, and it stops working', async () => {
|
|
const del1 = await del(`/api-keys/${auth.alice.apiKeyId}`, auth.alice.jwt!);
|
|
expect(del1.status).toBe(200);
|
|
|
|
const meAfter = await get('/users/me', auth.alice.apiKey);
|
|
expect(meAfter.status).toBe(401);
|
|
|
|
// JWT still works — we only revoked the key.
|
|
const meJwt = await get('/users/me', auth.alice.jwt!);
|
|
expect(meJwt.status).toBe(200);
|
|
});
|
|
});
|
|
}
|