Files
pantree/tests/auth.test.js

241 lines
8.1 KiB
JavaScript

'use strict';
const request = require('supertest');
const app = require('../../src/main/app');
const { setDb } = require('../../src/db/knex');
const { createTestDb } = require('../helpers/testDb');
describe('Auth Routes', () => {
let db;
beforeEach(() => {
db = createTestDb();
setDb(db);
});
// ── POST /v1/auth/signup ────────────────────────────────────────────────────
describe('POST /v1/auth/signup', () => {
it('creates a new user and returns token', async () => {
const res = await request(app)
.post('/v1/auth/signup')
.send({ email: 'jane@example.com', password: 'password1', name: 'Jane Doe' });
expect(res.status).toBe(201);
expect(res.body.user.email).toBe('jane@example.com');
expect(res.body.user.name).toBe('Jane Doe');
expect(res.body.token).toBeDefined();
expect(res.body.expires_at).toBeDefined();
// password_hash must never appear in response
expect(res.body.user.password_hash).toBeUndefined();
});
it('lowercases email on storage', async () => {
const res = await request(app)
.post('/v1/auth/signup')
.send({ email: 'JANE@EXAMPLE.COM', password: 'password1', name: 'Jane' });
expect(res.status).toBe(201);
expect(res.body.user.email).toBe('jane@example.com');
});
it('returns 409 when email already registered', async () => {
await request(app)
.post('/v1/auth/signup')
.send({ email: 'jane@example.com', password: 'password1', name: 'Jane' });
const res = await request(app)
.post('/v1/auth/signup')
.send({ email: 'jane@example.com', password: 'password1', name: 'Jane Again' });
expect(res.status).toBe(409);
expect(res.body.code).toBe('CONFLICT');
});
it('returns 409 for case-insensitive duplicate email', async () => {
await request(app)
.post('/v1/auth/signup')
.send({ email: 'jane@example.com', password: 'password1', name: 'Jane' });
const res = await request(app)
.post('/v1/auth/signup')
.send({ email: 'JANE@EXAMPLE.COM', password: 'password1', name: 'Jane' });
expect(res.status).toBe(409);
});
it('returns 400 for invalid email', async () => {
const res = await request(app)
.post('/v1/auth/signup')
.send({ email: 'not-an-email', password: 'password1', name: 'Jane' });
expect(res.status).toBe(400);
expect(res.body.code).toBe('VALIDATION_ERROR');
});
it('returns 400 for password shorter than 8 chars', async () => {
const res = await request(app)
.post('/v1/auth/signup')
.send({ email: 'jane@example.com', password: 'short', name: 'Jane' });
expect(res.status).toBe(400);
});
it('returns 400 for non-alphanumeric password', async () => {
const res = await request(app)
.post('/v1/auth/signup')
.send({ email: 'jane@example.com', password: 'p@ssword1', name: 'Jane' });
expect(res.status).toBe(400);
});
it('returns 400 when name is missing', async () => {
const res = await request(app)
.post('/v1/auth/signup')
.send({ email: 'jane@example.com', password: 'password1' });
expect(res.status).toBe(400);
});
it('returns 400 when body is empty', async () => {
const res = await request(app).post('/v1/auth/signup').send({});
expect(res.status).toBe(400);
});
});
// ── POST /v1/auth/signin ────────────────────────────────────────────────────
describe('POST /v1/auth/signin', () => {
beforeEach(async () => {
await request(app)
.post('/v1/auth/signup')
.send({ email: 'jane@example.com', password: 'password1', name: 'Jane' });
});
it('returns token on valid credentials', async () => {
const res = await request(app)
.post('/v1/auth/signin')
.send({ email: 'jane@example.com', password: 'password1' });
expect(res.status).toBe(200);
expect(res.body.token).toBeDefined();
expect(res.body.user.email).toBe('jane@example.com');
});
it('is case-insensitive for email', async () => {
const res = await request(app)
.post('/v1/auth/signin')
.send({ email: 'JANE@EXAMPLE.COM', password: 'password1' });
expect(res.status).toBe(200);
});
it('returns 401 for wrong password', async () => {
const res = await request(app)
.post('/v1/auth/signin')
.send({ email: 'jane@example.com', password: 'wrongpass' });
expect(res.status).toBe(401);
expect(res.body.code).toBe('UNAUTHORIZED');
});
it('returns 401 for unknown email', async () => {
const res = await request(app)
.post('/v1/auth/signin')
.send({ email: 'nobody@example.com', password: 'password1' });
expect(res.status).toBe(401);
});
it('returns 400 when fields are missing', async () => {
const res = await request(app)
.post('/v1/auth/signin')
.send({ email: 'jane@example.com' });
expect(res.status).toBe(400);
});
});
// ── POST /v1/auth/password-reset ────────────────────────────────────────────
describe('POST /v1/auth/password-reset', () => {
it('always returns 200 regardless of email existence', async () => {
const res = await request(app)
.post('/v1/auth/password-reset')
.send({ email: 'nobody@example.com' });
expect(res.status).toBe(200);
expect(res.body.message).toMatch(/reset link/i);
});
it('returns 400 for invalid email', async () => {
const res = await request(app)
.post('/v1/auth/password-reset')
.send({ email: 'not-an-email' });
expect(res.status).toBe(400);
});
});
// ── DELETE /v1/auth/account ─────────────────────────────────────────────────
describe('DELETE /v1/auth/account', () => {
it('soft-deletes the authenticated user account', async () => {
const signupRes = await request(app)
.post('/v1/auth/signup')
.send({ email: 'delete@example.com', password: 'password1', name: 'Delete Me' });
const token = signupRes.body.token;
const res = await request(app)
.delete('/v1/auth/account')
.set('Authorization', `Bearer ${token}`);
expect(res.status).toBe(204);
// Verify account is soft-deleted
const users = db.getTable('users');
const user = users.find((u) => u.email === 'delete@example.com');
expect(user.deleted_at).toBeDefined();
expect(user.deletion_scheduled_at).toBeDefined();
});
it('returns 401 without token', async () => {
const res = await request(app).delete('/v1/auth/account');
expect(res.status).toBe(401);
});
});
// ── POST /v1/auth/restore-account ──────────────────────────────────────────
describe('POST /v1/auth/restore-account', () => {
it('restores a soft-deleted account', async () => {
const signupRes = await request(app)
.post('/v1/auth/signup')
.send({ email: 'restore@example.com', password: 'password1', name: 'Restore Me' });
const token = signupRes.body.token;
await request(app)
.delete('/v1/auth/account')
.set('Authorization', `Bearer ${token}`);
const res = await request(app)
.post('/v1/auth/restore-account')
.send({ email: 'restore@example.com', password: 'password1' });
expect(res.status).toBe(200);
expect(res.body.user.deleted_at).toBeNull();
expect(res.body.message).toMatch(/restored/i);
});
it('returns 401 for wrong credentials', async () => {
const res = await request(app)
.post('/v1/auth/restore-account')
.send({ email: 'nobody@example.com', password: 'password1' });
expect(res.status).toBe(401);
});
});
});