Some checks failed
Build and Push Docker Images (Trusted) / build-and-push (., docker/caddy/Dockerfile, caddy) (push) Has been cancelled
Build and Push Docker Images (Trusted) / build-and-push (., docker/l4-port-manager/Dockerfile, l4-port-manager) (push) Has been cancelled
Build and Push Docker Images (Trusted) / build-and-push (., docker/web/Dockerfile, web) (push) Has been cancelled
Tests / test (push) Has been cancelled
284 lines
8.0 KiB
TypeScript
Executable File
284 lines
8.0 KiB
TypeScript
Executable File
import { describe, it, expect, beforeEach } from 'vitest';
|
|
import { createHash, randomBytes } from 'node:crypto';
|
|
import { createTestDb, type TestDb } from '../helpers/db';
|
|
import {
|
|
forwardAuthSessions,
|
|
forwardAuthExchanges,
|
|
forwardAuthAccess,
|
|
groups,
|
|
users,
|
|
proxyHosts
|
|
} from '@/src/lib/db/schema';
|
|
import { eq } from 'drizzle-orm';
|
|
|
|
let db: TestDb;
|
|
|
|
beforeEach(() => {
|
|
db = createTestDb();
|
|
});
|
|
|
|
function nowIso() {
|
|
return new Date().toISOString();
|
|
}
|
|
|
|
function futureIso(seconds: number) {
|
|
return new Date(Date.now() + seconds * 1000).toISOString();
|
|
}
|
|
|
|
function hashToken(raw: string): string {
|
|
return createHash('sha256').update(raw).digest('hex');
|
|
}
|
|
|
|
async function insertUser(overrides: Partial<typeof users.$inferInsert> = {}) {
|
|
const now = nowIso();
|
|
const [user] = await db.insert(users).values({
|
|
email: `user${Math.random().toString(36).slice(2)}@localhost`,
|
|
name: 'Test User',
|
|
role: 'user',
|
|
provider: 'credentials',
|
|
subject: `test-${Date.now()}-${Math.random()}`,
|
|
status: 'active',
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
...overrides,
|
|
}).returning();
|
|
return user;
|
|
}
|
|
|
|
async function insertProxyHost(overrides: Partial<typeof proxyHosts.$inferInsert> = {}) {
|
|
const now = nowIso();
|
|
const [host] = await db.insert(proxyHosts).values({
|
|
name: 'Test Host',
|
|
domains: JSON.stringify(['app.example.com']),
|
|
upstreams: JSON.stringify(['backend:8080']),
|
|
sslForced: true,
|
|
hstsEnabled: true,
|
|
hstsSubdomains: false,
|
|
allowWebsocket: true,
|
|
preserveHostHeader: true,
|
|
skipHttpsHostnameValidation: false,
|
|
enabled: true,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
...overrides,
|
|
}).returning();
|
|
return host;
|
|
}
|
|
|
|
describe('forward auth sessions', () => {
|
|
it('creates a session with hashed token', async () => {
|
|
const user = await insertUser();
|
|
const rawToken = randomBytes(32).toString('hex');
|
|
const tokenHash = hashToken(rawToken);
|
|
const now = nowIso();
|
|
|
|
const [session] = await db.insert(forwardAuthSessions).values({
|
|
userId: user.id,
|
|
tokenHash,
|
|
expiresAt: futureIso(3600),
|
|
createdAt: now,
|
|
}).returning();
|
|
|
|
expect(session.tokenHash).toBe(tokenHash);
|
|
expect(session.userId).toBe(user.id);
|
|
});
|
|
|
|
it('enforces unique token hashes', async () => {
|
|
const user = await insertUser();
|
|
const tokenHash = hashToken('same-token');
|
|
const now = nowIso();
|
|
|
|
await db.insert(forwardAuthSessions).values({
|
|
userId: user.id, tokenHash, expiresAt: futureIso(3600), createdAt: now,
|
|
});
|
|
|
|
await expect(
|
|
db.insert(forwardAuthSessions).values({
|
|
userId: user.id, tokenHash, expiresAt: futureIso(3600), createdAt: now,
|
|
})
|
|
).rejects.toThrow();
|
|
});
|
|
|
|
it('cascades user deletion to sessions', async () => {
|
|
const user = await insertUser();
|
|
const now = nowIso();
|
|
|
|
await db.insert(forwardAuthSessions).values({
|
|
userId: user.id,
|
|
tokenHash: hashToken('token1'),
|
|
expiresAt: futureIso(3600),
|
|
createdAt: now,
|
|
});
|
|
|
|
await db.delete(users).where(eq(users.id, user.id));
|
|
|
|
const sessions = await db.query.forwardAuthSessions.findMany();
|
|
expect(sessions).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('forward auth exchanges', () => {
|
|
it('creates an exchange code linked to a session', async () => {
|
|
const user = await insertUser();
|
|
const now = nowIso();
|
|
|
|
const [session] = await db.insert(forwardAuthSessions).values({
|
|
userId: user.id,
|
|
tokenHash: hashToken('session-token'),
|
|
expiresAt: futureIso(3600),
|
|
createdAt: now,
|
|
}).returning();
|
|
|
|
const rawCode = randomBytes(32).toString('hex');
|
|
const [exchange] = await db.insert(forwardAuthExchanges).values({
|
|
sessionId: session.id,
|
|
codeHash: hashToken(rawCode),
|
|
sessionToken: 'raw-session-token',
|
|
redirectUri: 'https://app.example.com/path',
|
|
expiresAt: futureIso(60),
|
|
used: false,
|
|
createdAt: now,
|
|
}).returning();
|
|
|
|
expect(exchange.sessionId).toBe(session.id);
|
|
expect(exchange.sessionToken).toBe('raw-session-token');
|
|
expect(exchange.used).toBe(false);
|
|
});
|
|
|
|
it('cascades session deletion to exchanges', async () => {
|
|
const user = await insertUser();
|
|
const now = nowIso();
|
|
|
|
const [session] = await db.insert(forwardAuthSessions).values({
|
|
userId: user.id,
|
|
tokenHash: hashToken('session2'),
|
|
expiresAt: futureIso(3600),
|
|
createdAt: now,
|
|
}).returning();
|
|
|
|
await db.insert(forwardAuthExchanges).values({
|
|
sessionId: session.id,
|
|
codeHash: hashToken('code1'),
|
|
sessionToken: 'raw-token',
|
|
redirectUri: 'https://app.example.com/',
|
|
expiresAt: futureIso(60),
|
|
used: false,
|
|
createdAt: now,
|
|
});
|
|
|
|
await db.delete(forwardAuthSessions).where(eq(forwardAuthSessions.id, session.id));
|
|
|
|
const exchanges = await db.query.forwardAuthExchanges.findMany();
|
|
expect(exchanges).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('forward auth access', () => {
|
|
it('creates user-level access for a proxy host', async () => {
|
|
const user = await insertUser();
|
|
const host = await insertProxyHost();
|
|
const now = nowIso();
|
|
|
|
const [access] = await db.insert(forwardAuthAccess).values({
|
|
proxyHostId: host.id,
|
|
userId: user.id,
|
|
groupId: null,
|
|
createdAt: now,
|
|
}).returning();
|
|
|
|
expect(access.proxyHostId).toBe(host.id);
|
|
expect(access.userId).toBe(user.id);
|
|
expect(access.groupId).toBeNull();
|
|
});
|
|
|
|
it('creates group-level access for a proxy host', async () => {
|
|
const host = await insertProxyHost();
|
|
const now = nowIso();
|
|
|
|
const [group] = await db.insert(groups).values({
|
|
name: 'Devs',
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
}).returning();
|
|
|
|
const [access] = await db.insert(forwardAuthAccess).values({
|
|
proxyHostId: host.id,
|
|
userId: null,
|
|
groupId: group.id,
|
|
createdAt: now,
|
|
}).returning();
|
|
|
|
expect(access.groupId).toBe(group.id);
|
|
expect(access.userId).toBeNull();
|
|
});
|
|
|
|
it('prevents duplicate user access per host', async () => {
|
|
const user = await insertUser();
|
|
const host = await insertProxyHost();
|
|
const now = nowIso();
|
|
|
|
await db.insert(forwardAuthAccess).values({
|
|
proxyHostId: host.id, userId: user.id, groupId: null, createdAt: now,
|
|
});
|
|
|
|
await expect(
|
|
db.insert(forwardAuthAccess).values({
|
|
proxyHostId: host.id, userId: user.id, groupId: null, createdAt: now,
|
|
})
|
|
).rejects.toThrow();
|
|
});
|
|
|
|
it('cascades proxy host deletion to access entries', async () => {
|
|
const user = await insertUser();
|
|
const host = await insertProxyHost();
|
|
const now = nowIso();
|
|
|
|
await db.insert(forwardAuthAccess).values({
|
|
proxyHostId: host.id, userId: user.id, groupId: null, createdAt: now,
|
|
});
|
|
|
|
await db.delete(proxyHosts).where(eq(proxyHosts.id, host.id));
|
|
|
|
const access = await db.query.forwardAuthAccess.findMany();
|
|
expect(access).toHaveLength(0);
|
|
});
|
|
|
|
it('cascades group deletion to access entries', async () => {
|
|
const host = await insertProxyHost();
|
|
const now = nowIso();
|
|
|
|
const [group] = await db.insert(groups).values({
|
|
name: 'Team', createdAt: now, updatedAt: now,
|
|
}).returning();
|
|
|
|
await db.insert(forwardAuthAccess).values({
|
|
proxyHostId: host.id, userId: null, groupId: group.id, createdAt: now,
|
|
});
|
|
|
|
await db.delete(groups).where(eq(groups.id, group.id));
|
|
|
|
const access = await db.query.forwardAuthAccess.findMany();
|
|
expect(access).toHaveLength(0);
|
|
});
|
|
|
|
it('allows both user and group access on same host', async () => {
|
|
const user = await insertUser();
|
|
const host = await insertProxyHost();
|
|
const now = nowIso();
|
|
|
|
const [group] = await db.insert(groups).values({
|
|
name: 'Group', createdAt: now, updatedAt: now,
|
|
}).returning();
|
|
|
|
await db.insert(forwardAuthAccess).values([
|
|
{ proxyHostId: host.id, userId: user.id, groupId: null, createdAt: now },
|
|
{ proxyHostId: host.id, userId: null, groupId: group.id, createdAt: now },
|
|
]);
|
|
|
|
const access = await db.query.forwardAuthAccess.findMany({
|
|
where: (t, { eq }) => eq(t.proxyHostId, host.id),
|
|
});
|
|
expect(access).toHaveLength(2);
|
|
});
|
|
});
|