mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-08 18:05:10 +00:00
797623e56f
Rename internal variant values from 'personal'/'team' to 'skipper'/'admiral', aligning code with user-facing tier names. Variant type is now resolved once at activation/validation and stored in DB via license_variant_type, instead of string-matching the Lemon Squeezy variant_name on every read. Also captures variant_id for future lookups. Pre-existing installs auto-migrate on first getVariant() call.
124 lines
4.4 KiB
TypeScript
124 lines
4.4 KiB
TypeScript
/**
|
|
* Tests for authentication: login, rate limiting, and auth middleware.
|
|
*/
|
|
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
|
|
import request from 'supertest';
|
|
import jwt from 'jsonwebtoken';
|
|
import { setupTestDb, cleanupTestDb, TEST_USERNAME, TEST_PASSWORD, TEST_JWT_SECRET } from './helpers/setupTestDb';
|
|
|
|
let tmpDir: string;
|
|
let app: import('express').Express;
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = await setupTestDb();
|
|
({ app } = await import('../index'));
|
|
});
|
|
|
|
afterAll(() => {
|
|
cleanupTestDb(tmpDir);
|
|
});
|
|
|
|
// ─── Login ───────────────────────────────────────────────────────────────────
|
|
|
|
describe('POST /api/auth/login', () => {
|
|
it('returns 200 and sets a cookie on valid credentials', async () => {
|
|
const res = await request(app)
|
|
.post('/api/auth/login')
|
|
.send({ username: TEST_USERNAME, password: TEST_PASSWORD });
|
|
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.success).toBe(true);
|
|
expect(res.headers['set-cookie']).toBeDefined();
|
|
});
|
|
|
|
it('returns 401 on wrong password', async () => {
|
|
const res = await request(app)
|
|
.post('/api/auth/login')
|
|
.send({ username: TEST_USERNAME, password: 'wrong-password' });
|
|
|
|
expect(res.status).toBe(401);
|
|
});
|
|
|
|
it('returns 401 on unknown username', async () => {
|
|
const res = await request(app)
|
|
.post('/api/auth/login')
|
|
.send({ username: 'nobody', password: 'anything' });
|
|
|
|
expect(res.status).toBe(401);
|
|
});
|
|
|
|
it('returns 400 when credentials are missing', async () => {
|
|
const res = await request(app).post('/api/auth/login').send({});
|
|
expect(res.status).toBe(400);
|
|
});
|
|
});
|
|
|
|
// ─── Auth middleware ──────────────────────────────────────────────────────────
|
|
|
|
describe('authMiddleware', () => {
|
|
it('rejects requests with no token (401)', async () => {
|
|
const res = await request(app).get('/api/stacks');
|
|
expect(res.status).toBe(401);
|
|
});
|
|
|
|
it('rejects requests with an invalid token (401)', async () => {
|
|
const res = await request(app)
|
|
.get('/api/stacks')
|
|
.set('Authorization', 'Bearer this.is.not.valid');
|
|
expect(res.status).toBe(401);
|
|
});
|
|
|
|
it('accepts a valid Bearer token', async () => {
|
|
// Issue a real token using the known test secret
|
|
const token = jwt.sign({ username: TEST_USERNAME }, TEST_JWT_SECRET, { expiresIn: '1m' });
|
|
const res = await request(app)
|
|
.get('/api/stacks')
|
|
.set('Authorization', `Bearer ${token}`);
|
|
// Will succeed (200) or fail with a docker/fs error (500) - but NOT 401
|
|
expect(res.status).not.toBe(401);
|
|
});
|
|
|
|
it('accepts a valid cookie token', async () => {
|
|
// First login to get the cookie
|
|
const loginRes = await request(app)
|
|
.post('/api/auth/login')
|
|
.send({ username: TEST_USERNAME, password: TEST_PASSWORD });
|
|
const cookies = loginRes.headers['set-cookie'] as string | string[];
|
|
const cookieHeader = Array.isArray(cookies) ? cookies[0] : cookies;
|
|
|
|
const res = await request(app)
|
|
.get('/api/stacks')
|
|
.set('Cookie', cookieHeader);
|
|
expect(res.status).not.toBe(401);
|
|
});
|
|
});
|
|
|
|
// ─── Protected endpoint: console-token ───────────────────────────────────────
|
|
|
|
describe('POST /api/system/console-token', () => {
|
|
// Console-token requires Admiral tier — mock LicenseService for the happy-path test
|
|
beforeAll(async () => {
|
|
const { LicenseService } = await import('../services/LicenseService');
|
|
vi.spyOn(LicenseService.getInstance(), 'getTier').mockReturnValue('paid');
|
|
vi.spyOn(LicenseService.getInstance(), 'getVariant').mockReturnValue('admiral');
|
|
});
|
|
|
|
afterAll(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('returns 401 without authentication (was a security bug - C1 fix)', async () => {
|
|
const res = await request(app).post('/api/system/console-token');
|
|
expect(res.status).toBe(401);
|
|
});
|
|
|
|
it('returns a token when authenticated', async () => {
|
|
const token = jwt.sign({ username: TEST_USERNAME }, TEST_JWT_SECRET, { expiresIn: '1m' });
|
|
const res = await request(app)
|
|
.post('/api/system/console-token')
|
|
.set('Authorization', `Bearer ${token}`);
|
|
expect(res.status).toBe(200);
|
|
expect(typeof res.body.token).toBe('string');
|
|
});
|
|
});
|