feat(gateway): WorkspaceService + ProjectBootstrapService + TeamsService (P8-015)
- WorkspaceService: path resolution, git init/clone, directory lifecycle (create/delete/exists), user and team root provisioning - ProjectBootstrapService: orchestrates DB record creation (via Brain) + workspace directory init in a single call - TeamsService: isMember, canAccessProject, findAll, findById, listMembers via Drizzle DB queries - WorkspaceController: POST /api/workspaces — auth-guarded project bootstrap endpoint - TeamsController: GET /api/teams, /:teamId, /:teamId/members, /:teamId/members/:userId - WorkspaceModule wired into AppModule - workspace.service.spec.ts: 5 unit tests for resolvePath (user, team, fallback, env var, default) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
79
apps/gateway/src/workspace/workspace.service.spec.ts
Normal file
79
apps/gateway/src/workspace/workspace.service.spec.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { WorkspaceService } from './workspace.service.js';
|
||||
import path from 'node:path';
|
||||
|
||||
describe('WorkspaceService', () => {
|
||||
let service: WorkspaceService;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new WorkspaceService();
|
||||
});
|
||||
|
||||
describe('resolvePath', () => {
|
||||
it('resolves user workspace path', () => {
|
||||
const result = service.resolvePath({
|
||||
id: 'proj1',
|
||||
ownerType: 'user',
|
||||
userId: 'user1',
|
||||
teamId: null,
|
||||
});
|
||||
expect(result).toContain(path.join('users', 'user1', 'proj1'));
|
||||
});
|
||||
|
||||
it('resolves team workspace path', () => {
|
||||
const result = service.resolvePath({
|
||||
id: 'proj1',
|
||||
ownerType: 'team',
|
||||
userId: 'user1',
|
||||
teamId: 'team1',
|
||||
});
|
||||
expect(result).toContain(path.join('teams', 'team1', 'proj1'));
|
||||
});
|
||||
|
||||
it('falls back to user path when ownerType is team but teamId is null', () => {
|
||||
const result = service.resolvePath({
|
||||
id: 'proj1',
|
||||
ownerType: 'team',
|
||||
userId: 'user1',
|
||||
teamId: null,
|
||||
});
|
||||
expect(result).toContain(path.join('users', 'user1', 'proj1'));
|
||||
});
|
||||
|
||||
it('uses MOSAIC_ROOT env var as the base path', () => {
|
||||
const originalRoot = process.env['MOSAIC_ROOT'];
|
||||
process.env['MOSAIC_ROOT'] = '/custom/root';
|
||||
const customService = new WorkspaceService();
|
||||
const result = customService.resolvePath({
|
||||
id: 'proj1',
|
||||
ownerType: 'user',
|
||||
userId: 'user1',
|
||||
teamId: null,
|
||||
});
|
||||
expect(result).toMatch(/^\/custom\/root/);
|
||||
// Restore
|
||||
if (originalRoot === undefined) {
|
||||
delete process.env['MOSAIC_ROOT'];
|
||||
} else {
|
||||
process.env['MOSAIC_ROOT'] = originalRoot;
|
||||
}
|
||||
});
|
||||
|
||||
it('defaults to /opt/mosaic when MOSAIC_ROOT is unset', () => {
|
||||
const originalRoot = process.env['MOSAIC_ROOT'];
|
||||
delete process.env['MOSAIC_ROOT'];
|
||||
const defaultService = new WorkspaceService();
|
||||
const result = defaultService.resolvePath({
|
||||
id: 'proj2',
|
||||
ownerType: 'user',
|
||||
userId: 'user2',
|
||||
teamId: null,
|
||||
});
|
||||
expect(result).toMatch(/^\/opt\/mosaic/);
|
||||
// Restore
|
||||
if (originalRoot !== undefined) {
|
||||
process.env['MOSAIC_ROOT'] = originalRoot;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user