feat: implement full backend API for Pantree Phase 1 MVP
This commit is contained in:
240
tests/auth.test.js
Normal file
240
tests/auth.test.js
Normal file
@@ -0,0 +1,240 @@
|
||||
'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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user