godcrm/backend/routes/oauth/__tests__/oidc.test.js
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

309 lines
11 KiB
JavaScript

/**
* OIDC Provider Tests - ADR-063: WorkAdventure Integration
* Testing OIDC endpoints for WorkAdventure authentication
*/
import { describe, test, expect, beforeEach, afterEach } from 'vitest';
import request from 'supertest';
import express from 'express';
import jwt from 'jsonwebtoken';
import { dbGet, dbRun, destroyAdapter, resetAdapter } from '../../../database/connection.js';
// Import routes (will be created)
import oauthRoutes from '../index.js';
// Create test app
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use('/oauth', oauthRoutes);
app.use('/.well-known', oauthRoutes); // For OIDC discovery
// Test JWT secret
const TEST_JWT_SECRET = 'test-jwt-secret-for-oidc';
process.env.JWT_SECRET = TEST_JWT_SECRET;
process.env.OIDC_ISSUER = 'https://crm.hltrn.cc';
// Helper functions
async function createTestUser() {
const uniqueEmail = `test-oidc-${Date.now()}@hltrn.cc`;
const result = await dbRun(
'INSERT INTO users (email, password_hash, name, avatar, encryption_key_encrypted, email_verified) VALUES (?, ?, ?, ?, ?, ?)',
[uniqueEmail, 'hash', 'Test User', 'https://example.com/avatar.png', 'encrypted_key', 1]
);
return { id: result.lastInsertRowid, email: uniqueEmail };
}
async function createOidcClient() {
// Clean up existing client to avoid unique constraint violation
await dbRun('DELETE FROM oidc_clients WHERE client_id = $1', ['workadventure']);
const result = await dbRun(
`INSERT INTO oidc_clients (client_id, client_secret, redirect_uris, name)
VALUES (?, ?, ?, ?)`,
['workadventure', 'wa-secret-123', JSON.stringify(['https://play.workadventure.localhost/oauth/callback']), 'WorkAdventure']
);
return result.lastInsertRowid;
}
describe('OIDC Provider - ADR-063', () => {
let testUser;
beforeEach(async () => {
process.env.TEST_MODE = 'true';
process.env.SKIP_DEV_USER = 'true';
await resetAdapter();
testUser = await createTestUser();
await createOidcClient();
});
afterEach(async () => {
await destroyAdapter();
});
// ============================================================
// GET /.well-known/openid-configuration
// ============================================================
describe('GET /.well-known/openid-configuration', () => {
test('should return OIDC discovery document', async () => {
const response = await request(app)
.get('/.well-known/openid-configuration')
.expect(200);
expect(response.body.issuer).toBe('https://crm.hltrn.cc');
expect(response.body.authorization_endpoint).toContain('/oauth/authorize');
expect(response.body.token_endpoint).toContain('/oauth/token');
expect(response.body.userinfo_endpoint).toContain('/oauth/userinfo');
expect(response.body.response_types_supported).toContain('code');
expect(response.body.grant_types_supported).toContain('authorization_code');
expect(response.body.subject_types_supported).toContain('public');
expect(response.body.id_token_signing_alg_values_supported).toContain('RS256');
});
});
// ============================================================
// GET /oauth/authorize
// ============================================================
describe('GET /oauth/authorize', () => {
test('should redirect to login when not authenticated', async () => {
const response = await request(app)
.get('/oauth/authorize')
.query({
client_id: 'workadventure',
redirect_uri: 'https://play.workadventure.localhost/oauth/callback',
response_type: 'code',
scope: 'openid profile email',
state: 'random-state-123'
})
.expect(302);
expect(response.headers.location).toContain('/login');
expect(response.headers.location).toContain('redirect=');
});
test('should return error for invalid client_id', async () => {
const response = await request(app)
.get('/oauth/authorize')
.query({
client_id: 'invalid-client',
redirect_uri: 'https://example.com/callback',
response_type: 'code',
scope: 'openid'
})
.expect(400);
expect(response.body.error).toBe('invalid_client');
});
test('should return error for invalid redirect_uri', async () => {
const response = await request(app)
.get('/oauth/authorize')
.query({
client_id: 'workadventure',
redirect_uri: 'https://malicious.com/callback',
response_type: 'code',
scope: 'openid'
})
.expect(400);
expect(response.body.error).toBe('invalid_redirect_uri');
});
test('should return authorization code when authenticated', async () => {
// Create a valid session token
const sessionToken = jwt.sign(
{ id: testUser.id, email: testUser.email },
TEST_JWT_SECRET,
{ expiresIn: '1h' }
);
const response = await request(app)
.get('/oauth/authorize')
.set('Authorization', `Bearer ${sessionToken}`)
.query({
client_id: 'workadventure',
redirect_uri: 'https://play.workadventure.localhost/oauth/callback',
response_type: 'code',
scope: 'openid profile email',
state: 'random-state-123'
})
.expect(302);
expect(response.headers.location).toContain('https://play.workadventure.localhost/oauth/callback');
expect(response.headers.location).toContain('code=');
expect(response.headers.location).toContain('state=random-state-123');
});
});
// ============================================================
// POST /oauth/token
// ============================================================
describe('POST /oauth/token', () => {
test('should exchange authorization code for tokens', async () => {
// First, get an authorization code
const sessionToken = jwt.sign(
{ id: testUser.id, email: testUser.email },
TEST_JWT_SECRET,
{ expiresIn: '1h' }
);
const authResponse = await request(app)
.get('/oauth/authorize')
.set('Authorization', `Bearer ${sessionToken}`)
.query({
client_id: 'workadventure',
redirect_uri: 'https://play.workadventure.localhost/oauth/callback',
response_type: 'code',
scope: 'openid profile email',
state: 'test-state'
});
// Extract code from redirect URL
const redirectUrl = new URL(authResponse.headers.location);
const code = redirectUrl.searchParams.get('code');
// Exchange code for tokens
const tokenResponse = await request(app)
.post('/oauth/token')
.send({
grant_type: 'authorization_code',
code: code,
redirect_uri: 'https://play.workadventure.localhost/oauth/callback',
client_id: 'workadventure',
client_secret: 'wa-secret-123'
})
.expect(200);
expect(tokenResponse.body.access_token).toBeDefined();
expect(tokenResponse.body.token_type).toBe('Bearer');
expect(tokenResponse.body.expires_in).toBeGreaterThan(0);
expect(tokenResponse.body.id_token).toBeDefined();
});
test('should return error for invalid code', async () => {
const response = await request(app)
.post('/oauth/token')
.send({
grant_type: 'authorization_code',
code: 'invalid-code',
redirect_uri: 'https://play.workadventure.localhost/oauth/callback',
client_id: 'workadventure',
client_secret: 'wa-secret-123'
})
.expect(400);
expect(response.body.error).toBe('invalid_grant');
});
test('should return error for invalid client credentials', async () => {
const response = await request(app)
.post('/oauth/token')
.send({
grant_type: 'authorization_code',
code: 'some-code',
redirect_uri: 'https://play.workadventure.localhost/oauth/callback',
client_id: 'workadventure',
client_secret: 'wrong-secret'
})
.expect(401);
expect(response.body.error).toBe('invalid_client');
});
test('should return error for unsupported grant_type', async () => {
const response = await request(app)
.post('/oauth/token')
.send({
grant_type: 'password',
username: 'test@example.com',
password: 'password123'
})
.expect(400);
expect(response.body.error).toBe('unsupported_grant_type');
});
});
// ============================================================
// GET /oauth/userinfo
// ============================================================
describe('GET /oauth/userinfo', () => {
test('should return user info with valid access token', async () => {
// Get tokens first
const sessionToken = jwt.sign(
{ id: testUser.id, email: testUser.email },
TEST_JWT_SECRET,
{ expiresIn: '1h' }
);
const authResponse = await request(app)
.get('/oauth/authorize')
.set('Authorization', `Bearer ${sessionToken}`)
.query({
client_id: 'workadventure',
redirect_uri: 'https://play.workadventure.localhost/oauth/callback',
response_type: 'code',
scope: 'openid profile email'
});
const redirectUrl = new URL(authResponse.headers.location);
const code = redirectUrl.searchParams.get('code');
const tokenResponse = await request(app)
.post('/oauth/token')
.send({
grant_type: 'authorization_code',
code: code,
redirect_uri: 'https://play.workadventure.localhost/oauth/callback',
client_id: 'workadventure',
client_secret: 'wa-secret-123'
});
// Get userinfo
const userinfoResponse = await request(app)
.get('/oauth/userinfo')
.set('Authorization', `Bearer ${tokenResponse.body.access_token}`)
.expect(200);
expect(userinfoResponse.body.sub).toBe(String(testUser.id));
expect(userinfoResponse.body.email).toBe(testUser.email);
expect(userinfoResponse.body.name).toBe('Test User');
expect(userinfoResponse.body.picture).toBeDefined();
});
test('should return 401 without access token', async () => {
const response = await request(app)
.get('/oauth/userinfo')
.expect(401);
expect(response.body.error).toBe('invalid_token');
});
test('should return 401 with invalid access token', async () => {
const response = await request(app)
.get('/oauth/userinfo')
.set('Authorization', 'Bearer invalid-token')
.expect(401);
expect(response.body.error).toBe('invalid_token');
});
});
});