Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
409 lines
13 KiB
JavaScript
409 lines
13 KiB
JavaScript
/**
|
|
* WorkAdventure Admin API Tests - ADR-063
|
|
* Testing Admin API endpoints for room access control
|
|
*/
|
|
|
|
import { describe, test, expect, beforeEach, afterEach } from 'vitest';
|
|
import request from 'supertest';
|
|
import express from 'express';
|
|
import jwt from 'jsonwebtoken';
|
|
import { dbAll, dbGet, dbRun, destroyAdapter, resetAdapter } from '../../../../database/connection.js';
|
|
// Import routes (will be created)
|
|
import waRoutes from '../index.js';
|
|
|
|
// Create test app
|
|
const app = express();
|
|
app.use(express.json());
|
|
|
|
// Mock authenticate middleware for testing
|
|
app.use((req, res, next) => {
|
|
const authHeader = req.headers.authorization;
|
|
if (authHeader && authHeader.startsWith('Bearer ')) {
|
|
const token = authHeader.split(' ')[1];
|
|
try {
|
|
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'test-secret');
|
|
req.user = decoded;
|
|
} catch (e) {
|
|
// Invalid token - continue without user
|
|
}
|
|
}
|
|
next();
|
|
});
|
|
|
|
app.use('/api/v3/wa', waRoutes);
|
|
|
|
// Test JWT secret
|
|
const TEST_JWT_SECRET = 'test-jwt-secret-for-wa';
|
|
process.env.JWT_SECRET = TEST_JWT_SECRET;
|
|
|
|
// Helper functions
|
|
async function createTestUser(role = 'user') {
|
|
const uniqueEmail = `test-wa-${Date.now()}-${Math.random().toString(36).substring(7)}@hltrn.cc`;
|
|
const result = await dbRun(
|
|
'INSERT INTO users (email, password_hash, name, role, avatar, encryption_key_encrypted, email_verified) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
|
[uniqueEmail, 'hash', 'Test User', role, 'https://example.com/avatar.png', 'encrypted_key', 1]
|
|
);
|
|
return { id: result.lastInsertRowid, email: uniqueEmail, role };
|
|
}
|
|
|
|
async function createTestSpace(ownerId) {
|
|
const result = await dbRun(
|
|
'INSERT INTO spaces (owner_id, name, type) VALUES (?, ?, ?)',
|
|
[ownerId, 'Test Space', 'business']
|
|
);
|
|
return result.lastInsertRowid;
|
|
}
|
|
|
|
function createToken(user) {
|
|
return jwt.sign(
|
|
{ id: user.id, email: user.email, role: user.role },
|
|
TEST_JWT_SECRET,
|
|
{ expiresIn: '1h' }
|
|
);
|
|
}
|
|
|
|
describe('WorkAdventure Admin API - ADR-063', () => {
|
|
let testUser;
|
|
let adminUser;
|
|
let spaceId;
|
|
|
|
beforeEach(async () => {
|
|
process.env.TEST_MODE = 'true';
|
|
process.env.SKIP_DEV_USER = 'true';
|
|
await resetAdapter();
|
|
|
|
testUser = await createTestUser('user');
|
|
adminUser = await createTestUser('admin');
|
|
spaceId = await createTestSpace(adminUser.id);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await destroyAdapter();
|
|
});
|
|
|
|
// ============================================================
|
|
// GET /api/v3/wa/map
|
|
// ============================================================
|
|
describe('GET /api/v3/wa/map', () => {
|
|
test('should return map URL for authenticated user', async () => {
|
|
const token = createToken(testUser);
|
|
|
|
const response = await request(app)
|
|
.get('/api/v3/wa/map')
|
|
.set('Authorization', `Bearer ${token}`)
|
|
.query({
|
|
playUri: 'https://play.workadventure.localhost/@/crm/office/main',
|
|
userIdentifier: testUser.email
|
|
})
|
|
.expect(200);
|
|
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data.mapUrl).toBeDefined();
|
|
expect(response.body.data.mapUrl).toContain('.json');
|
|
});
|
|
|
|
test('should return 401 without authentication', async () => {
|
|
const response = await request(app)
|
|
.get('/api/v3/wa/map')
|
|
.query({
|
|
playUri: 'https://play.workadventure.localhost/@/crm/office/main'
|
|
})
|
|
.expect(401);
|
|
|
|
expect(response.body.success).toBe(false);
|
|
});
|
|
|
|
test('should return default map for unknown room', async () => {
|
|
const token = createToken(testUser);
|
|
|
|
const response = await request(app)
|
|
.get('/api/v3/wa/map')
|
|
.set('Authorization', `Bearer ${token}`)
|
|
.query({
|
|
playUri: 'https://play.workadventure.localhost/@/crm/unknown/room'
|
|
})
|
|
.expect(200);
|
|
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data.mapUrl).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ============================================================
|
|
// GET /api/v3/wa/room/access
|
|
// ============================================================
|
|
describe('GET /api/v3/wa/room/access', () => {
|
|
test('should grant access to public room', async () => {
|
|
const token = createToken(testUser);
|
|
|
|
const response = await request(app)
|
|
.get('/api/v3/wa/room/access')
|
|
.set('Authorization', `Bearer ${token}`)
|
|
.query({
|
|
playUri: 'https://play.workadventure.localhost/@/crm/public/lobby',
|
|
userIdentifier: testUser.email
|
|
})
|
|
.expect(200);
|
|
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data.access).toBe(true);
|
|
expect(response.body.data.tags).toBeDefined();
|
|
});
|
|
|
|
test('should deny access to restricted room for regular user', async () => {
|
|
const token = createToken(testUser);
|
|
|
|
const response = await request(app)
|
|
.get('/api/v3/wa/room/access')
|
|
.set('Authorization', `Bearer ${token}`)
|
|
.query({
|
|
playUri: 'https://play.workadventure.localhost/@/crm/admin/control-room',
|
|
userIdentifier: testUser.email
|
|
})
|
|
.expect(200);
|
|
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data.access).toBe(false);
|
|
expect(response.body.data.reason).toBeDefined();
|
|
});
|
|
|
|
test('should grant access to admin room for admin user', async () => {
|
|
const token = createToken(adminUser);
|
|
|
|
const response = await request(app)
|
|
.get('/api/v3/wa/room/access')
|
|
.set('Authorization', `Bearer ${token}`)
|
|
.query({
|
|
playUri: 'https://play.workadventure.localhost/@/crm/admin/control-room',
|
|
userIdentifier: adminUser.email
|
|
})
|
|
.expect(200);
|
|
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data.access).toBe(true);
|
|
});
|
|
|
|
test('should return 401 without authentication', async () => {
|
|
const response = await request(app)
|
|
.get('/api/v3/wa/room/access')
|
|
.query({
|
|
playUri: 'https://play.workadventure.localhost/@/crm/public/lobby'
|
|
})
|
|
.expect(401);
|
|
|
|
expect(response.body.success).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ============================================================
|
|
// GET /api/v3/wa/member
|
|
// ============================================================
|
|
describe('GET /api/v3/wa/member', () => {
|
|
test('should return member info with tags', async () => {
|
|
const token = createToken(testUser);
|
|
|
|
const response = await request(app)
|
|
.get('/api/v3/wa/member')
|
|
.set('Authorization', `Bearer ${token}`)
|
|
.query({
|
|
userIdentifier: testUser.email,
|
|
playUri: 'https://play.workadventure.localhost/@/crm/office/main'
|
|
})
|
|
.expect(200);
|
|
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data.email).toBe(testUser.email);
|
|
expect(response.body.data.tags).toBeDefined();
|
|
expect(Array.isArray(response.body.data.tags)).toBe(true);
|
|
expect(response.body.data.textures).toBeDefined();
|
|
});
|
|
|
|
test('should return admin tag for admin user', async () => {
|
|
const token = createToken(adminUser);
|
|
|
|
const response = await request(app)
|
|
.get('/api/v3/wa/member')
|
|
.set('Authorization', `Bearer ${token}`)
|
|
.query({
|
|
userIdentifier: adminUser.email,
|
|
playUri: 'https://play.workadventure.localhost/@/crm/office/main'
|
|
})
|
|
.expect(200);
|
|
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data.tags).toContain('admin');
|
|
});
|
|
|
|
test('should return 404 for unknown user', async () => {
|
|
const token = createToken(adminUser);
|
|
|
|
const response = await request(app)
|
|
.get('/api/v3/wa/member')
|
|
.set('Authorization', `Bearer ${token}`)
|
|
.query({
|
|
userIdentifier: 'unknown@example.com',
|
|
playUri: 'https://play.workadventure.localhost/@/crm/office/main'
|
|
})
|
|
.expect(404);
|
|
|
|
expect(response.body.success).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ============================================================
|
|
// POST /api/v3/wa/webhook
|
|
// ============================================================
|
|
describe('POST /api/v3/wa/webhook', () => {
|
|
test('should handle user join event', async () => {
|
|
const response = await request(app)
|
|
.post('/api/v3/wa/webhook')
|
|
.set('X-WA-Webhook-Secret', process.env.WA_WEBHOOK_SECRET || 'test-webhook-secret')
|
|
.send({
|
|
event: 'user.join',
|
|
data: {
|
|
userIdentifier: testUser.email,
|
|
roomId: '@/crm/office/main',
|
|
timestamp: new Date().toISOString()
|
|
}
|
|
})
|
|
.expect(200);
|
|
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data.processed).toBe(true);
|
|
|
|
// Verify presence was recorded
|
|
const presence = await dbGet(
|
|
'SELECT * FROM wa_presence WHERE user_id = ? AND room_id = ?',
|
|
[testUser.id, '@/crm/office/main']
|
|
);
|
|
expect(presence).toBeDefined();
|
|
expect(presence.status).toBe('online');
|
|
});
|
|
|
|
test('should handle user leave event', async () => {
|
|
// First, create a presence record
|
|
await dbRun(
|
|
'INSERT INTO wa_presence (user_id, room_id, status, joined_at) VALUES (?, ?, ?, ?)',
|
|
[testUser.id, '@/crm/office/main', 'online', new Date().toISOString()]
|
|
);
|
|
|
|
const response = await request(app)
|
|
.post('/api/v3/wa/webhook')
|
|
.set('X-WA-Webhook-Secret', process.env.WA_WEBHOOK_SECRET || 'test-webhook-secret')
|
|
.send({
|
|
event: 'user.leave',
|
|
data: {
|
|
userIdentifier: testUser.email,
|
|
roomId: '@/crm/office/main',
|
|
timestamp: new Date().toISOString()
|
|
}
|
|
})
|
|
.expect(200);
|
|
|
|
expect(response.body.success).toBe(true);
|
|
|
|
// Verify presence was updated
|
|
const presence = await dbGet(
|
|
'SELECT * FROM wa_presence WHERE user_id = ? AND room_id = ?',
|
|
[testUser.id, '@/crm/office/main']
|
|
);
|
|
expect(presence.left_at).toBeDefined();
|
|
});
|
|
|
|
test('should reject webhook without secret', async () => {
|
|
const response = await request(app)
|
|
.post('/api/v3/wa/webhook')
|
|
.send({
|
|
event: 'user.join',
|
|
data: {
|
|
userIdentifier: testUser.email,
|
|
roomId: '@/crm/office/main'
|
|
}
|
|
})
|
|
.expect(401);
|
|
|
|
expect(response.body.success).toBe(false);
|
|
});
|
|
|
|
test('should reject webhook with invalid secret', async () => {
|
|
const response = await request(app)
|
|
.post('/api/v3/wa/webhook')
|
|
.set('X-WA-Webhook-Secret', 'wrong-secret')
|
|
.send({
|
|
event: 'user.join',
|
|
data: {
|
|
userIdentifier: testUser.email,
|
|
roomId: '@/crm/office/main'
|
|
}
|
|
})
|
|
.expect(401);
|
|
|
|
expect(response.body.success).toBe(false);
|
|
});
|
|
|
|
test('should handle unknown event gracefully', async () => {
|
|
const response = await request(app)
|
|
.post('/api/v3/wa/webhook')
|
|
.set('X-WA-Webhook-Secret', process.env.WA_WEBHOOK_SECRET || 'test-webhook-secret')
|
|
.send({
|
|
event: 'unknown.event',
|
|
data: {}
|
|
})
|
|
.expect(200);
|
|
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data.processed).toBe(false);
|
|
expect(response.body.data.reason).toBe('unknown_event');
|
|
});
|
|
});
|
|
|
|
// ============================================================
|
|
// GET /api/v3/wa/presence
|
|
// ============================================================
|
|
describe('GET /api/v3/wa/presence', () => {
|
|
test('should return online users in a room', async () => {
|
|
const token = createToken(adminUser);
|
|
|
|
// Create presence records
|
|
await dbRun(
|
|
'INSERT INTO wa_presence (user_id, room_id, status, joined_at) VALUES (?, ?, ?, ?)',
|
|
[testUser.id, '@/crm/office/main', 'online', new Date().toISOString()]
|
|
);
|
|
|
|
const response = await request(app)
|
|
.get('/api/v3/wa/presence')
|
|
.set('Authorization', `Bearer ${token}`)
|
|
.query({ roomId: '@/crm/office/main' })
|
|
.expect(200);
|
|
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data.users).toBeDefined();
|
|
expect(response.body.data.users.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test('should return all online users without room filter', async () => {
|
|
const token = createToken(adminUser);
|
|
|
|
// Clean existing presence records to ensure test isolation
|
|
await dbRun('DELETE FROM wa_presence');
|
|
|
|
// Create presence records in different rooms
|
|
await dbRun(
|
|
'INSERT INTO wa_presence (user_id, room_id, status, joined_at) VALUES (?, ?, ?, ?)',
|
|
[testUser.id, '@/crm/office/main', 'online', new Date().toISOString()]
|
|
);
|
|
await dbRun(
|
|
'INSERT INTO wa_presence (user_id, room_id, status, joined_at) VALUES (?, ?, ?, ?)',
|
|
[adminUser.id, '@/crm/office/meeting', 'online', new Date().toISOString()]
|
|
);
|
|
|
|
const response = await request(app)
|
|
.get('/api/v3/wa/presence')
|
|
.set('Authorization', `Bearer ${token}`)
|
|
.expect(200);
|
|
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data.users.length).toBe(2);
|
|
});
|
|
});
|
|
});
|