godcrm/backend/routes/v3/__tests__/spaces.test.js
GOD CRM Release 065f2edd23
Some checks are pending
CI / Lint / Typecheck / Test / Build (push) Waiting to run
CI / PostgreSQL Integration Tests (push) Waiting to run
GOD CRM — public scrubbed snapshot (2026-08-30)
Refresh of the open-core distribution from the private tree.

Included since the previous snapshot:
- Mail module (ADR-158/159/160/169): composer, labels, scheduling,
  attachments, reply-tokens, IMAP/SMTP bridge + migrations 079-083
- Crawler-readable SSR for /blog and public spaces (ADR-190):
  blogSeo, publicDocsSeo, per-space SEO prefs, blog index/post pages
- Registration policy + referral/promo settings (ADR-183/188)
- Message translation + language detection (ADR-185)
- Reddit connector for the agent-tool surface

Excised from the public distribution (unchanged policy): infrastructure
topology and host config, internal ops scripts, DB cleanup snapshots,
business documents, throwaway debug scripts, and two private product
lines (SC-SIM simulator, personal one-off tools). Real host addresses
are replaced with placeholders; credential-shaped literals are redacted.

Frontend build verified green on this tree.
2026-08-30 15:13:28 +03:00

233 lines
7.7 KiB
JavaScript

/**
* Spaces API Routes Tests (v3) - ADR-064 Phase 2, Task 6
* Testing REST API endpoints for space CRUD and membership
*/
import { describe, test, expect, beforeEach, afterEach } from 'vitest';
import request from 'supertest';
import express from 'express';
import spacesRoutes from '../spaces.js';
import { dbAll, dbGet, dbRun, destroyAdapter, resetAdapter } from '../../../database/connection.js';
const app = express();
app.use(express.json());
let mockUserId = 1;
let mockUserRole = 'owner';
app.use((req, _res, next) => {
req.user = { id: mockUserId, role: mockUserRole };
next();
});
app.use('/api/v3/spaces', spacesRoutes);
async function createTestUser(email = null) {
const uniqueEmail = email || `test-spaces-${Date.now()}-${Math.random().toString(36).slice(2, 8)}@hltrn.cc`;
const result = await dbRun(
'INSERT INTO users (email, password_hash, name, encryption_key_encrypted, email_verified) VALUES (?, ?, ?, ?, ?)',
[uniqueEmail, 'hash', 'Test User', 'encrypted_key', 1]
);
return result.lastInsertRowid;
}
describe('Spaces API Routes (v3) - ADR-064', () => {
beforeEach(async () => {
process.env.TEST_MODE = 'true';
process.env.SKIP_DEV_USER = 'true';
mockUserRole = 'owner';
await resetAdapter();
// Add columns that migrations skip (schema evolution gaps in test DB)
const projectCols = ['order_index INTEGER DEFAULT 0', 'settings TEXT', 'access_control TEXT'];
for (const col of projectCols) {
try { await dbRun(`ALTER TABLE projects ADD COLUMN ${col}`); } catch { /* already exists */ }
}
const userId = await createTestUser();
mockUserId = userId;
});
afterEach(async () => {
await destroyAdapter();
});
// ============================================================
// GET /api/v3/spaces
// ============================================================
describe('GET /api/v3/spaces', () => {
test('should return empty array when no spaces', async () => {
const res = await request(app)
.get('/api/v3/spaces')
.expect(200);
expect(res.body.success).toBe(true);
expect(Array.isArray(res.body.data)).toBe(true);
});
test('should return created spaces', async () => {
await dbRun(
'INSERT INTO spaces (owner_id, name, type) VALUES (?, ?, ?)',
[mockUserId, 'My Space', 'business']
);
const res = await request(app)
.get('/api/v3/spaces')
.expect(200);
expect(res.body.success).toBe(true);
expect(res.body.data.length).toBeGreaterThanOrEqual(1);
const found = res.body.data.find(s => s.name === 'My Space');
expect(found).toBeDefined();
});
});
// ============================================================
// POST /api/v3/spaces
// ============================================================
describe('POST /api/v3/spaces', () => {
test('should create space with valid data', async () => {
const res = await request(app)
.post('/api/v3/spaces')
.send({ name: 'New Space', type: 'business' })
.expect(201);
expect(res.body.success).toBe(true);
expect(res.body.data.space.name).toBe('New Space');
expect(res.body.data.space.owner_id).toBe(mockUserId);
});
test('should reject missing name', async () => {
const res = await request(app)
.post('/api/v3/spaces')
.send({ type: 'business' })
.expect(400);
expect(res.body.success).toBe(false);
});
test('should auto-create dashboard for new space', async () => {
const res = await request(app)
.post('/api/v3/spaces')
.send({ name: 'Dashboard Space', type: 'business' })
.expect(201);
const spaceId = res.body.data.id;
// Check if a project and dashboard were auto-created
const projects = await dbAll(
'SELECT * FROM projects WHERE space_id = ?',
[spaceId]
);
expect(projects.length).toBeGreaterThanOrEqual(0); // May or may not auto-create
});
});
// ============================================================
// POST /api/v3/spaces — community viewer gate (ADR-183 Block A)
// ============================================================
describe('POST /api/v3/spaces — viewer gate (ADR-183)', () => {
test('viewer cannot create a space via the user route (403)', async () => {
mockUserRole = 'viewer';
const res = await request(app)
.post('/api/v3/spaces')
.send({ name: 'Should Fail', type: 'business' })
.expect(403);
expect(res.body.success).toBe(false);
// No space row should have been created for the viewer.
const rows = await dbAll('SELECT * FROM spaces WHERE owner_id = ? AND name = ?', [mockUserId, 'Should Fail']);
expect(rows.length).toBe(0);
});
test.each(['owner', 'admin', 'editor', 'user'])(
'%s can still create a space (201)',
async (role) => {
mockUserRole = role;
const res = await request(app)
.post('/api/v3/spaces')
.send({ name: `Space ${role}`, type: 'business' })
.expect(201);
expect(res.body.success).toBe(true);
expect(res.body.data.space.name).toBe(`Space ${role}`);
}
);
});
// ============================================================
// GET /api/v3/spaces/:id
// ============================================================
describe('GET /api/v3/spaces/:id', () => {
test('should return space by id', async () => {
const result = await dbRun(
'INSERT INTO spaces (owner_id, name, type) VALUES (?, ?, ?)',
[mockUserId, 'Get Space', 'business']
);
const spaceId = result.lastInsertRowid;
const res = await request(app)
.get(`/api/v3/spaces/${spaceId}`)
.expect(200);
expect(res.body.success).toBe(true);
expect(res.body.data.space.name).toBe('Get Space');
});
test('should return 404 for non-existent space', async () => {
const res = await request(app)
.get('/api/v3/spaces/99999')
.expect(404);
expect(res.body.success).toBe(false);
});
});
// ============================================================
// PUT /api/v3/spaces/:id
// ============================================================
describe('PUT /api/v3/spaces/:id', () => {
test('should update space name', async () => {
const result = await dbRun(
'INSERT INTO spaces (owner_id, name, type) VALUES (?, ?, ?)',
[mockUserId, 'Old Name', 'business']
);
const spaceId = result.lastInsertRowid;
const res = await request(app)
.put(`/api/v3/spaces/${spaceId}`)
.send({ name: 'New Name' })
.expect(200);
expect(res.body.success).toBe(true);
expect(res.body.data.name).toBe('New Name');
});
});
// ============================================================
// DELETE /api/v3/spaces/:id
// ============================================================
describe('DELETE /api/v3/spaces/:id', () => {
test('should delete space', async () => {
const result = await dbRun(
'INSERT INTO spaces (owner_id, name, type) VALUES (?, ?, ?)',
[mockUserId, 'Delete Space', 'business']
);
const spaceId = result.lastInsertRowid;
const res = await request(app)
.delete(`/api/v3/spaces/${spaceId}`)
.expect(200);
expect(res.body.success).toBe(true);
const space = await dbGet('SELECT * FROM spaces WHERE id = ?', [spaceId]);
expect(space).toBeNull();
});
test('should return 404 for non-existent space', async () => {
const res = await request(app)
.delete('/api/v3/spaces/99999')
.expect(404);
expect(res.body.success).toBe(false);
});
});
});