Replace next-auth with Better Auth, migrate DB columns to camelCase
- Replace next-auth v5 beta with better-auth v1.6.2 (stable releases)
- Add multi-provider OAuth support with admin UI configuration
- New oauthProviders table with encrypted secrets (AES-256-GCM)
- Env var bootstrap (OAUTH_*) syncs to DB, UI-created providers fully editable
- OAuth provider REST API: GET/POST/PUT/DELETE /api/v1/oauth-providers
- Settings page "Authentication Providers" section for admin management
- Account linking uses new accounts table (multi-provider per user)
- Username plugin for credentials sign-in (replaces email@localhost pattern)
- bcrypt password compatibility (existing hashes work)
- Database-backed sessions via Kysely adapter (bun:sqlite direct)
- Configurable rate limiting via AUTH_RATE_LIMIT_* env vars
- All DB columns migrated from snake_case to camelCase
- All TypeScript types/models migrated to camelCase properties
- Removed casing: "snake_case" from Drizzle config
- Callback URL format: {baseUrl}/api/auth/oauth2/callback/{providerId}
- package-lock.json removed and gitignored (using bun.lock)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -40,7 +40,7 @@ beforeEach(() => {
|
||||
describe('authenticateApiRequest', () => {
|
||||
it('authenticates via Bearer token', async () => {
|
||||
mockValidateToken.mockResolvedValue({
|
||||
token: { id: 1, name: 'test', created_by: 42, created_at: '', last_used_at: null, expires_at: null },
|
||||
token: { id: 1, name: 'test', createdBy: 42, createdAt: '', lastUsedAt: null, expiresAt: null },
|
||||
user: { id: 42, role: 'admin' },
|
||||
});
|
||||
|
||||
@@ -91,7 +91,7 @@ describe('authenticateApiRequest', () => {
|
||||
describe('requireApiAdmin', () => {
|
||||
it('allows admin users', async () => {
|
||||
mockValidateToken.mockResolvedValue({
|
||||
token: { id: 1, name: 'test', created_by: 1, created_at: '', last_used_at: null, expires_at: null },
|
||||
token: { id: 1, name: 'test', createdBy: 1, createdAt: '', lastUsedAt: null, expiresAt: null },
|
||||
user: { id: 1, role: 'admin' },
|
||||
});
|
||||
|
||||
@@ -101,7 +101,7 @@ describe('requireApiAdmin', () => {
|
||||
|
||||
it('rejects non-admin users with 403', async () => {
|
||||
mockValidateToken.mockResolvedValue({
|
||||
token: { id: 1, name: 'test', created_by: 2, created_at: '', last_used_at: null, expires_at: null },
|
||||
token: { id: 1, name: 'test', createdBy: 2, createdAt: '', lastUsedAt: null, expiresAt: null },
|
||||
user: { id: 2, role: 'user' },
|
||||
});
|
||||
|
||||
@@ -154,7 +154,7 @@ describe('requireApiUser', () => {
|
||||
|
||||
it('CSRF check skips for Bearer-authenticated POST', async () => {
|
||||
mockValidateToken.mockResolvedValue({
|
||||
token: { id: 1, name: 'test', created_by: 42, created_at: '', last_used_at: null, expires_at: null },
|
||||
token: { id: 1, name: 'test', createdBy: 42, createdAt: '', lastUsedAt: null, expiresAt: null },
|
||||
user: { id: 42, role: 'admin' },
|
||||
});
|
||||
|
||||
|
||||
@@ -53,8 +53,8 @@ const sampleUser = {
|
||||
name: 'Admin User',
|
||||
email: 'admin@example.com',
|
||||
role: 'admin',
|
||||
password_hash: '$2b$10$hashedpassword',
|
||||
created_at: '2026-01-01',
|
||||
passwordHash: '$2b$10$hashedpassword',
|
||||
createdAt: '2026-01-01',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -64,7 +64,7 @@ beforeEach(() => {
|
||||
});
|
||||
|
||||
describe('GET /api/v1/users', () => {
|
||||
it('returns list of users with password_hash stripped', async () => {
|
||||
it('returns list of users with passwordHash stripped', async () => {
|
||||
mockListUsers.mockResolvedValue([sampleUser] as any);
|
||||
|
||||
const response = await listGET(createMockRequest());
|
||||
@@ -72,7 +72,7 @@ describe('GET /api/v1/users', () => {
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data).toHaveLength(1);
|
||||
expect(data[0]).not.toHaveProperty('password_hash');
|
||||
expect(data[0]).not.toHaveProperty('passwordHash');
|
||||
expect(data[0].name).toBe('Admin User');
|
||||
expect(data[0].email).toBe('admin@example.com');
|
||||
});
|
||||
@@ -87,14 +87,14 @@ describe('GET /api/v1/users', () => {
|
||||
});
|
||||
|
||||
describe('GET /api/v1/users/[id]', () => {
|
||||
it('returns a user by id with password_hash stripped', async () => {
|
||||
it('returns a user by id with passwordHash stripped', async () => {
|
||||
mockGetUserById.mockResolvedValue(sampleUser as any);
|
||||
|
||||
const response = await getGET(createMockRequest(), { params: Promise.resolve({ id: '1' }) });
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data).not.toHaveProperty('password_hash');
|
||||
expect(data).not.toHaveProperty('passwordHash');
|
||||
expect(data.name).toBe('Admin User');
|
||||
});
|
||||
|
||||
@@ -128,7 +128,7 @@ describe('GET /api/v1/users/[id]', () => {
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.id).toBe(5);
|
||||
expect(data).not.toHaveProperty('password_hash');
|
||||
expect(data).not.toHaveProperty('passwordHash');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -144,7 +144,7 @@ describe('PUT /api/v1/users/[id]', () => {
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.name).toBe('Updated Name');
|
||||
expect(data).not.toHaveProperty('password_hash');
|
||||
expect(data).not.toHaveProperty('passwordHash');
|
||||
expect(mockUpdateUserProfile).toHaveBeenCalledWith(1, { name: 'Updated Name' });
|
||||
});
|
||||
|
||||
|
||||
@@ -20,10 +20,10 @@ import {
|
||||
|
||||
function makeRule(overrides: Partial<MtlsAccessRuleLike> = {}): MtlsAccessRuleLike {
|
||||
return {
|
||||
path_pattern: "/admin/*",
|
||||
allowed_role_ids: [],
|
||||
allowed_cert_ids: [],
|
||||
deny_all: false,
|
||||
pathPattern: "/admin/*",
|
||||
allowedRoleIds: [],
|
||||
allowedCertIds: [],
|
||||
denyAll: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
@@ -54,7 +54,7 @@ describe("resolveAllowedFingerprints", () => {
|
||||
]);
|
||||
const certFpMap = new Map<number, string>();
|
||||
|
||||
const rule = makeRule({ allowed_role_ids: [1, 2] });
|
||||
const rule = makeRule({ allowedRoleIds: [1, 2] });
|
||||
const result = resolveAllowedFingerprints(rule, roleFpMap, certFpMap);
|
||||
|
||||
expect(result).toEqual(new Set(["fp_a", "fp_b", "fp_c"]));
|
||||
@@ -67,7 +67,7 @@ describe("resolveAllowedFingerprints", () => {
|
||||
[20, "fp_y"],
|
||||
]);
|
||||
|
||||
const rule = makeRule({ allowed_cert_ids: [10, 20] });
|
||||
const rule = makeRule({ allowedCertIds: [10, 20] });
|
||||
const result = resolveAllowedFingerprints(rule, roleFpMap, certFpMap);
|
||||
|
||||
expect(result).toEqual(new Set(["fp_x", "fp_y"]));
|
||||
@@ -79,7 +79,7 @@ describe("resolveAllowedFingerprints", () => {
|
||||
]);
|
||||
const certFpMap = new Map<number, string>([[10, "fp_b"]]);
|
||||
|
||||
const rule = makeRule({ allowed_role_ids: [1], allowed_cert_ids: [10] });
|
||||
const rule = makeRule({ allowedRoleIds: [1], allowedCertIds: [10] });
|
||||
const result = resolveAllowedFingerprints(rule, roleFpMap, certFpMap);
|
||||
|
||||
expect(result).toEqual(new Set(["fp_a", "fp_b"]));
|
||||
@@ -91,7 +91,7 @@ describe("resolveAllowedFingerprints", () => {
|
||||
]);
|
||||
const certFpMap = new Map<number, string>([[10, "fp_a"]]);
|
||||
|
||||
const rule = makeRule({ allowed_role_ids: [1], allowed_cert_ids: [10] });
|
||||
const rule = makeRule({ allowedRoleIds: [1], allowedCertIds: [10] });
|
||||
const result = resolveAllowedFingerprints(rule, roleFpMap, certFpMap);
|
||||
|
||||
expect(result.size).toBe(1);
|
||||
@@ -102,7 +102,7 @@ describe("resolveAllowedFingerprints", () => {
|
||||
const roleFpMap = new Map<number, Set<string>>();
|
||||
const certFpMap = new Map<number, string>();
|
||||
|
||||
const rule = makeRule({ allowed_role_ids: [999], allowed_cert_ids: [999] });
|
||||
const rule = makeRule({ allowedRoleIds: [999], allowedCertIds: [999] });
|
||||
const result = resolveAllowedFingerprints(rule, roleFpMap, certFpMap);
|
||||
|
||||
expect(result.size).toBe(0);
|
||||
@@ -146,7 +146,7 @@ describe("buildMtlsRbacSubroutes", () => {
|
||||
const roleFpMap = new Map<number, Set<string>>([
|
||||
[1, new Set(["fp_admin"])],
|
||||
]);
|
||||
const rules = [makeRule({ allowed_role_ids: [1] })];
|
||||
const rules = [makeRule({ allowedRoleIds: [1] })];
|
||||
|
||||
const result = buildMtlsRbacSubroutes(rules, roleFpMap, new Map(), baseHandlers, reverseProxy);
|
||||
|
||||
@@ -174,8 +174,8 @@ describe("buildMtlsRbacSubroutes", () => {
|
||||
expect(catchAll.terminal).toBe(true);
|
||||
});
|
||||
|
||||
it("generates 403 for deny_all rule", () => {
|
||||
const rules = [makeRule({ deny_all: true })];
|
||||
it("generates 403 for denyAll rule", () => {
|
||||
const rules = [makeRule({ denyAll: true })];
|
||||
const result = buildMtlsRbacSubroutes(rules, new Map(), new Map(), baseHandlers, reverseProxy);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
@@ -188,7 +188,7 @@ describe("buildMtlsRbacSubroutes", () => {
|
||||
});
|
||||
|
||||
it("generates 403 when rule has no matching fingerprints", () => {
|
||||
const rules = [makeRule({ allowed_role_ids: [999] })]; // role doesn't exist
|
||||
const rules = [makeRule({ allowedRoleIds: [999] })]; // role doesn't exist
|
||||
const result = buildMtlsRbacSubroutes(rules, new Map(), new Map(), baseHandlers, reverseProxy);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
@@ -206,8 +206,8 @@ describe("buildMtlsRbacSubroutes", () => {
|
||||
[2, new Set(["fp_api"])],
|
||||
]);
|
||||
const rules = [
|
||||
makeRule({ path_pattern: "/admin/*", allowed_role_ids: [1] }),
|
||||
makeRule({ path_pattern: "/api/*", allowed_role_ids: [1, 2] }),
|
||||
makeRule({ pathPattern: "/admin/*", allowedRoleIds: [1] }),
|
||||
makeRule({ pathPattern: "/api/*", allowedRoleIds: [1, 2] }),
|
||||
];
|
||||
|
||||
const result = buildMtlsRbacSubroutes(rules, roleFpMap, new Map(), baseHandlers, reverseProxy);
|
||||
@@ -219,7 +219,7 @@ describe("buildMtlsRbacSubroutes", () => {
|
||||
|
||||
it("uses direct cert fingerprints as overrides", () => {
|
||||
const certFpMap = new Map<number, string>([[42, "fp_special"]]);
|
||||
const rules = [makeRule({ allowed_cert_ids: [42] })];
|
||||
const rules = [makeRule({ allowedCertIds: [42] })];
|
||||
|
||||
const result = buildMtlsRbacSubroutes(rules, new Map(), certFpMap, baseHandlers, reverseProxy);
|
||||
|
||||
@@ -230,7 +230,7 @@ describe("buildMtlsRbacSubroutes", () => {
|
||||
});
|
||||
|
||||
it("catch-all route includes base handlers + reverse proxy", () => {
|
||||
const rules = [makeRule({ deny_all: true })];
|
||||
const rules = [makeRule({ denyAll: true })];
|
||||
const result = buildMtlsRbacSubroutes(rules, new Map(), new Map(), baseHandlers, reverseProxy);
|
||||
|
||||
const catchAll = result![result!.length - 1] as Record<string, unknown>;
|
||||
@@ -242,7 +242,7 @@ describe("buildMtlsRbacSubroutes", () => {
|
||||
|
||||
it("allow route includes base handlers + reverse proxy", () => {
|
||||
const roleFpMap = new Map<number, Set<string>>([[1, new Set(["fp"])]]);
|
||||
const rules = [makeRule({ allowed_role_ids: [1] })];
|
||||
const rules = [makeRule({ allowedRoleIds: [1] })];
|
||||
const result = buildMtlsRbacSubroutes(rules, roleFpMap, new Map(), baseHandlers, reverseProxy);
|
||||
|
||||
const allowRoute = result![0] as Record<string, unknown>;
|
||||
@@ -252,17 +252,17 @@ describe("buildMtlsRbacSubroutes", () => {
|
||||
});
|
||||
|
||||
it("deny route body is 'mTLS access denied'", () => {
|
||||
const rules = [makeRule({ deny_all: true })];
|
||||
const rules = [makeRule({ denyAll: true })];
|
||||
const result = buildMtlsRbacSubroutes(rules, new Map(), new Map(), baseHandlers, reverseProxy);
|
||||
const denyHandler = (result![0] as any).handle[0];
|
||||
expect(denyHandler.body).toBe("mTLS access denied");
|
||||
});
|
||||
|
||||
it("handles mixed deny_all and role-based rules", () => {
|
||||
it("handles mixed denyAll and role-based rules", () => {
|
||||
const roleFpMap = new Map<number, Set<string>>([[1, new Set(["fp"])]]);
|
||||
const rules = [
|
||||
makeRule({ path_pattern: "/secret/*", deny_all: true }),
|
||||
makeRule({ path_pattern: "/api/*", allowed_role_ids: [1] }),
|
||||
makeRule({ pathPattern: "/secret/*", denyAll: true }),
|
||||
makeRule({ pathPattern: "/api/*", allowedRoleIds: [1] }),
|
||||
];
|
||||
const result = buildMtlsRbacSubroutes(rules, roleFpMap, new Map(), baseHandlers, reverseProxy);
|
||||
|
||||
@@ -281,7 +281,7 @@ describe("buildMtlsRbacSubroutes", () => {
|
||||
it("handles rule with both roles and certs combined", () => {
|
||||
const roleFpMap = new Map<number, Set<string>>([[1, new Set(["fp_role"])]]);
|
||||
const certFpMap = new Map<number, string>([[42, "fp_cert"]]);
|
||||
const rules = [makeRule({ allowed_role_ids: [1], allowed_cert_ids: [42] })];
|
||||
const rules = [makeRule({ allowedRoleIds: [1], allowedCertIds: [42] })];
|
||||
|
||||
const result = buildMtlsRbacSubroutes(rules, roleFpMap, certFpMap, baseHandlers, reverseProxy);
|
||||
const match = (result![0] as any).match[0];
|
||||
@@ -292,7 +292,7 @@ describe("buildMtlsRbacSubroutes", () => {
|
||||
it("preserves base handlers order in generated routes", () => {
|
||||
const multiHandlers = [{ handler: "waf" }, { handler: "headers" }, { handler: "auth" }];
|
||||
const roleFpMap = new Map<number, Set<string>>([[1, new Set(["fp"])]]);
|
||||
const rules = [makeRule({ allowed_role_ids: [1] })];
|
||||
const rules = [makeRule({ allowedRoleIds: [1] })];
|
||||
|
||||
const result = buildMtlsRbacSubroutes(rules, roleFpMap, new Map(), multiHandlers, reverseProxy);
|
||||
const allowHandlers = (result![0] as any).handle;
|
||||
@@ -344,14 +344,14 @@ describe("buildFingerprintCelExpression edge cases", () => {
|
||||
|
||||
describe("resolveAllowedFingerprints edge cases", () => {
|
||||
it("handles empty arrays in rule", () => {
|
||||
const rule = makeRule({ allowed_role_ids: [], allowed_cert_ids: [] });
|
||||
const rule = makeRule({ allowedRoleIds: [], allowedCertIds: [] });
|
||||
const result = resolveAllowedFingerprints(rule, new Map(), new Map());
|
||||
expect(result.size).toBe(0);
|
||||
});
|
||||
|
||||
it("handles role with empty fingerprint set", () => {
|
||||
const roleFpMap = new Map<number, Set<string>>([[1, new Set()]]);
|
||||
const rule = makeRule({ allowed_role_ids: [1] });
|
||||
const rule = makeRule({ allowedRoleIds: [1] });
|
||||
const result = resolveAllowedFingerprints(rule, roleFpMap, new Map());
|
||||
expect(result.size).toBe(0);
|
||||
});
|
||||
@@ -362,7 +362,7 @@ describe("resolveAllowedFingerprints edge cases", () => {
|
||||
[2, new Set(["b", "c"])],
|
||||
[3, new Set(["c", "d"])],
|
||||
]);
|
||||
const rule = makeRule({ allowed_role_ids: [1, 2, 3] });
|
||||
const rule = makeRule({ allowedRoleIds: [1, 2, 3] });
|
||||
const result = resolveAllowedFingerprints(rule, roleFpMap, new Map());
|
||||
expect(result).toEqual(new Set(["a", "b", "c", "d"]));
|
||||
});
|
||||
|
||||
@@ -68,7 +68,7 @@ describe('L4 proxy host create validation', () => {
|
||||
const input: L4ProxyHostInput = {
|
||||
name: '',
|
||||
protocol: 'tcp',
|
||||
listen_address: ':5432',
|
||||
listenAddress: ':5432',
|
||||
upstreams: ['10.0.0.1:5432'],
|
||||
};
|
||||
await expect(createL4ProxyHost(input, 1)).rejects.toThrow('Name is required');
|
||||
@@ -78,7 +78,7 @@ describe('L4 proxy host create validation', () => {
|
||||
const input: L4ProxyHostInput = {
|
||||
name: 'Test',
|
||||
protocol: 'sctp' as any,
|
||||
listen_address: ':5432',
|
||||
listenAddress: ':5432',
|
||||
upstreams: ['10.0.0.1:5432'],
|
||||
};
|
||||
await expect(createL4ProxyHost(input, 1)).rejects.toThrow("Protocol must be 'tcp' or 'udp'");
|
||||
@@ -88,7 +88,7 @@ describe('L4 proxy host create validation', () => {
|
||||
const input: L4ProxyHostInput = {
|
||||
name: 'Test',
|
||||
protocol: 'tcp',
|
||||
listen_address: '',
|
||||
listenAddress: '',
|
||||
upstreams: ['10.0.0.1:5432'],
|
||||
};
|
||||
await expect(createL4ProxyHost(input, 1)).rejects.toThrow('Listen address is required');
|
||||
@@ -98,7 +98,7 @@ describe('L4 proxy host create validation', () => {
|
||||
const input: L4ProxyHostInput = {
|
||||
name: 'Test',
|
||||
protocol: 'tcp',
|
||||
listen_address: '10.0.0.1',
|
||||
listenAddress: '10.0.0.1',
|
||||
upstreams: ['10.0.0.1:5432'],
|
||||
};
|
||||
await expect(createL4ProxyHost(input, 1)).rejects.toThrow("Listen address must be in format ':PORT' or 'HOST:PORT'");
|
||||
@@ -108,7 +108,7 @@ describe('L4 proxy host create validation', () => {
|
||||
const input: L4ProxyHostInput = {
|
||||
name: 'Test',
|
||||
protocol: 'tcp',
|
||||
listen_address: ':0',
|
||||
listenAddress: ':0',
|
||||
upstreams: ['10.0.0.1:5432'],
|
||||
};
|
||||
await expect(createL4ProxyHost(input, 1)).rejects.toThrow('Port must be between 1 and 65535');
|
||||
@@ -118,7 +118,7 @@ describe('L4 proxy host create validation', () => {
|
||||
const input: L4ProxyHostInput = {
|
||||
name: 'Test',
|
||||
protocol: 'tcp',
|
||||
listen_address: ':99999',
|
||||
listenAddress: ':99999',
|
||||
upstreams: ['10.0.0.1:5432'],
|
||||
};
|
||||
await expect(createL4ProxyHost(input, 1)).rejects.toThrow('Port must be between 1 and 65535');
|
||||
@@ -128,7 +128,7 @@ describe('L4 proxy host create validation', () => {
|
||||
const input: L4ProxyHostInput = {
|
||||
name: 'Test',
|
||||
protocol: 'tcp',
|
||||
listen_address: ':5432',
|
||||
listenAddress: ':5432',
|
||||
upstreams: [],
|
||||
};
|
||||
await expect(createL4ProxyHost(input, 1)).rejects.toThrow('At least one upstream must be specified');
|
||||
@@ -138,7 +138,7 @@ describe('L4 proxy host create validation', () => {
|
||||
const input: L4ProxyHostInput = {
|
||||
name: 'Test',
|
||||
protocol: 'tcp',
|
||||
listen_address: ':5432',
|
||||
listenAddress: ':5432',
|
||||
upstreams: ['10.0.0.1'],
|
||||
};
|
||||
await expect(createL4ProxyHost(input, 1)).rejects.toThrow("must be in 'host:port' format");
|
||||
@@ -148,9 +148,9 @@ describe('L4 proxy host create validation', () => {
|
||||
const input: L4ProxyHostInput = {
|
||||
name: 'Test',
|
||||
protocol: 'udp',
|
||||
listen_address: ':5353',
|
||||
listenAddress: ':5353',
|
||||
upstreams: ['8.8.8.8:53'],
|
||||
tls_termination: true,
|
||||
tlsTermination: true,
|
||||
};
|
||||
await expect(createL4ProxyHost(input, 1)).rejects.toThrow('TLS termination is only supported with TCP');
|
||||
});
|
||||
@@ -159,10 +159,10 @@ describe('L4 proxy host create validation', () => {
|
||||
const input: L4ProxyHostInput = {
|
||||
name: 'Test',
|
||||
protocol: 'tcp',
|
||||
listen_address: ':5432',
|
||||
listenAddress: ':5432',
|
||||
upstreams: ['10.0.0.1:5432'],
|
||||
matcher_type: 'tls_sni',
|
||||
matcher_value: [],
|
||||
matcherType: 'tls_sni',
|
||||
matcherValue: [],
|
||||
};
|
||||
await expect(createL4ProxyHost(input, 1)).rejects.toThrow('Matcher value is required');
|
||||
});
|
||||
@@ -171,10 +171,10 @@ describe('L4 proxy host create validation', () => {
|
||||
const input: L4ProxyHostInput = {
|
||||
name: 'Test',
|
||||
protocol: 'tcp',
|
||||
listen_address: ':8080',
|
||||
listenAddress: ':8080',
|
||||
upstreams: ['10.0.0.1:8080'],
|
||||
matcher_type: 'http_host',
|
||||
matcher_value: [],
|
||||
matcherType: 'http_host',
|
||||
matcherValue: [],
|
||||
};
|
||||
await expect(createL4ProxyHost(input, 1)).rejects.toThrow('Matcher value is required');
|
||||
});
|
||||
@@ -183,9 +183,9 @@ describe('L4 proxy host create validation', () => {
|
||||
const input: L4ProxyHostInput = {
|
||||
name: 'Test',
|
||||
protocol: 'tcp',
|
||||
listen_address: ':5432',
|
||||
listenAddress: ':5432',
|
||||
upstreams: ['10.0.0.1:5432'],
|
||||
proxy_protocol_version: 'v3' as any,
|
||||
proxyProtocolVersion: 'v3' as any,
|
||||
};
|
||||
await expect(createL4ProxyHost(input, 1)).rejects.toThrow("Proxy protocol version must be 'v1' or 'v2'");
|
||||
});
|
||||
@@ -194,9 +194,9 @@ describe('L4 proxy host create validation', () => {
|
||||
const input: L4ProxyHostInput = {
|
||||
name: 'Test',
|
||||
protocol: 'tcp',
|
||||
listen_address: ':5432',
|
||||
listenAddress: ':5432',
|
||||
upstreams: ['10.0.0.1:5432'],
|
||||
matcher_type: 'invalid' as any,
|
||||
matcherType: 'invalid' as any,
|
||||
};
|
||||
await expect(createL4ProxyHost(input, 1)).rejects.toThrow('Matcher type must be one of');
|
||||
});
|
||||
@@ -205,33 +205,33 @@ describe('L4 proxy host create validation', () => {
|
||||
const input: L4ProxyHostInput = {
|
||||
name: 'Full Featured',
|
||||
protocol: 'tcp',
|
||||
listen_address: ':993',
|
||||
listenAddress: ':993',
|
||||
upstreams: ['localhost:143'],
|
||||
matcher_type: 'tls_sni',
|
||||
matcher_value: ['mail.example.com'],
|
||||
tls_termination: true,
|
||||
proxy_protocol_version: 'v1',
|
||||
proxy_protocol_receive: true,
|
||||
matcherType: 'tls_sni',
|
||||
matcherValue: ['mail.example.com'],
|
||||
tlsTermination: true,
|
||||
proxyProtocolVersion: 'v1',
|
||||
proxyProtocolReceive: true,
|
||||
enabled: true,
|
||||
};
|
||||
const result = await createL4ProxyHost(input, 1);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.name).toBe('Full Featured');
|
||||
expect(result.protocol).toBe('tcp');
|
||||
expect(result.listen_address).toBe(':993');
|
||||
expect(result.listenAddress).toBe(':993');
|
||||
expect(result.upstreams).toEqual(['localhost:143']);
|
||||
expect(result.matcher_type).toBe('tls_sni');
|
||||
expect(result.matcher_value).toEqual(['mail.example.com']);
|
||||
expect(result.tls_termination).toBe(true);
|
||||
expect(result.proxy_protocol_version).toBe('v1');
|
||||
expect(result.proxy_protocol_receive).toBe(true);
|
||||
expect(result.matcherType).toBe('tls_sni');
|
||||
expect(result.matcherValue).toEqual(['mail.example.com']);
|
||||
expect(result.tlsTermination).toBe(true);
|
||||
expect(result.proxyProtocolVersion).toBe('v1');
|
||||
expect(result.proxyProtocolReceive).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts valid UDP proxy', async () => {
|
||||
const input: L4ProxyHostInput = {
|
||||
name: 'DNS',
|
||||
protocol: 'udp',
|
||||
listen_address: ':5353',
|
||||
listenAddress: ':5353',
|
||||
upstreams: ['8.8.8.8:53'],
|
||||
};
|
||||
const result = await createL4ProxyHost(input, 1);
|
||||
@@ -243,54 +243,54 @@ describe('L4 proxy host create validation', () => {
|
||||
const input: L4ProxyHostInput = {
|
||||
name: 'Bound',
|
||||
protocol: 'tcp',
|
||||
listen_address: '0.0.0.0:5432',
|
||||
listenAddress: '0.0.0.0:5432',
|
||||
upstreams: ['10.0.0.1:5432'],
|
||||
};
|
||||
const result = await createL4ProxyHost(input, 1);
|
||||
expect(result.listen_address).toBe('0.0.0.0:5432');
|
||||
expect(result.listenAddress).toBe('0.0.0.0:5432');
|
||||
});
|
||||
|
||||
it('accepts none matcher without matcher_value', async () => {
|
||||
it('accepts none matcher without matcherValue', async () => {
|
||||
const input: L4ProxyHostInput = {
|
||||
name: 'Catch All',
|
||||
protocol: 'tcp',
|
||||
listen_address: ':5432',
|
||||
listenAddress: ':5432',
|
||||
upstreams: ['10.0.0.1:5432'],
|
||||
matcher_type: 'none',
|
||||
matcherType: 'none',
|
||||
};
|
||||
const result = await createL4ProxyHost(input, 1);
|
||||
expect(result.matcher_type).toBe('none');
|
||||
expect(result.matcherType).toBe('none');
|
||||
});
|
||||
|
||||
it('accepts proxy_protocol matcher without matcher_value', async () => {
|
||||
it('accepts proxy_protocol matcher without matcherValue', async () => {
|
||||
const input: L4ProxyHostInput = {
|
||||
name: 'PP Detect',
|
||||
protocol: 'tcp',
|
||||
listen_address: ':8443',
|
||||
listenAddress: ':8443',
|
||||
upstreams: ['10.0.0.1:443'],
|
||||
matcher_type: 'proxy_protocol',
|
||||
matcherType: 'proxy_protocol',
|
||||
};
|
||||
const result = await createL4ProxyHost(input, 1);
|
||||
expect(result.matcher_type).toBe('proxy_protocol');
|
||||
expect(result.matcherType).toBe('proxy_protocol');
|
||||
});
|
||||
|
||||
it('trims whitespace from name and listen_address', async () => {
|
||||
it('trims whitespace from name and listenAddress', async () => {
|
||||
const input: L4ProxyHostInput = {
|
||||
name: ' Spacey Name ',
|
||||
protocol: 'tcp',
|
||||
listen_address: ' :5432 ',
|
||||
listenAddress: ' :5432 ',
|
||||
upstreams: ['10.0.0.1:5432'],
|
||||
};
|
||||
const result = await createL4ProxyHost(input, 1);
|
||||
expect(result.name).toBe('Spacey Name');
|
||||
expect(result.listen_address).toBe(':5432');
|
||||
expect(result.listenAddress).toBe(':5432');
|
||||
});
|
||||
|
||||
it('deduplicates upstreams', async () => {
|
||||
const input: L4ProxyHostInput = {
|
||||
name: 'Dedup',
|
||||
protocol: 'tcp',
|
||||
listen_address: ':5432',
|
||||
listenAddress: ':5432',
|
||||
upstreams: ['10.0.0.1:5432', '10.0.0.1:5432', '10.0.0.2:5432'],
|
||||
};
|
||||
const result = await createL4ProxyHost(input, 1);
|
||||
|
||||
Reference in New Issue
Block a user