2026-02-21 05:16:57 +00:00
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
feat: granular RBAC with resource/operation bindings, users, groups
- Replace admin role with granular roles: view, create, delete, edit, run
- Two binding types: resource bindings (role+resource+optional name) and
operation bindings (role:run + action like backup, logs, impersonate)
- Name-scoped resource bindings for per-instance access control
- Remove role from project members (all permissions via RBAC)
- Add users, groups, RBAC CRUD endpoints and CLI commands
- describe user/group shows all RBAC access (direct + inherited)
- create rbac supports --subject, --binding, --operation flags
- Backup/restore handles users, groups, RBAC definitions
- mcplocal project-based MCP endpoint discovery
- Full test coverage for all new functionality
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 11:05:19 +00:00
|
|
|
import { createCreateCommand } from '../../src/commands/create.js';
|
|
|
|
|
import { createGetCommand } from '../../src/commands/get.js';
|
|
|
|
|
import { createDescribeCommand } from '../../src/commands/describe.js';
|
|
|
|
|
import { type ApiClient, ApiError } from '../../src/api-client.js';
|
2026-02-21 05:16:57 +00:00
|
|
|
|
|
|
|
|
function mockClient(): ApiClient {
|
|
|
|
|
return {
|
|
|
|
|
get: vi.fn(async () => []),
|
feat: granular RBAC with resource/operation bindings, users, groups
- Replace admin role with granular roles: view, create, delete, edit, run
- Two binding types: resource bindings (role+resource+optional name) and
operation bindings (role:run + action like backup, logs, impersonate)
- Name-scoped resource bindings for per-instance access control
- Remove role from project members (all permissions via RBAC)
- Add users, groups, RBAC CRUD endpoints and CLI commands
- describe user/group shows all RBAC access (direct + inherited)
- create rbac supports --subject, --binding, --operation flags
- Backup/restore handles users, groups, RBAC definitions
- mcplocal project-based MCP endpoint discovery
- Full test coverage for all new functionality
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 11:05:19 +00:00
|
|
|
post: vi.fn(async () => ({ id: 'new-id', name: 'test' })),
|
2026-02-21 05:16:57 +00:00
|
|
|
put: vi.fn(async () => ({})),
|
|
|
|
|
delete: vi.fn(async () => {}),
|
|
|
|
|
} as unknown as ApiClient;
|
|
|
|
|
}
|
|
|
|
|
|
feat: granular RBAC with resource/operation bindings, users, groups
- Replace admin role with granular roles: view, create, delete, edit, run
- Two binding types: resource bindings (role+resource+optional name) and
operation bindings (role:run + action like backup, logs, impersonate)
- Name-scoped resource bindings for per-instance access control
- Remove role from project members (all permissions via RBAC)
- Add users, groups, RBAC CRUD endpoints and CLI commands
- describe user/group shows all RBAC access (direct + inherited)
- create rbac supports --subject, --binding, --operation flags
- Backup/restore handles users, groups, RBAC definitions
- mcplocal project-based MCP endpoint discovery
- Full test coverage for all new functionality
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 11:05:19 +00:00
|
|
|
describe('project with new fields', () => {
|
2026-02-21 05:16:57 +00:00
|
|
|
let client: ReturnType<typeof mockClient>;
|
|
|
|
|
let output: string[];
|
|
|
|
|
const log = (...args: unknown[]) => output.push(args.map(String).join(' '));
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
client = mockClient();
|
|
|
|
|
output = [];
|
|
|
|
|
});
|
|
|
|
|
|
feat: granular RBAC with resource/operation bindings, users, groups
- Replace admin role with granular roles: view, create, delete, edit, run
- Two binding types: resource bindings (role+resource+optional name) and
operation bindings (role:run + action like backup, logs, impersonate)
- Name-scoped resource bindings for per-instance access control
- Remove role from project members (all permissions via RBAC)
- Add users, groups, RBAC CRUD endpoints and CLI commands
- describe user/group shows all RBAC access (direct + inherited)
- create rbac supports --subject, --binding, --operation flags
- Backup/restore handles users, groups, RBAC definitions
- mcplocal project-based MCP endpoint discovery
- Full test coverage for all new functionality
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 11:05:19 +00:00
|
|
|
describe('create project with enhanced options', () => {
|
|
|
|
|
it('creates project with proxy mode and servers', async () => {
|
|
|
|
|
const cmd = createCreateCommand({ client, log });
|
|
|
|
|
await cmd.parseAsync([
|
|
|
|
|
'project', 'smart-home',
|
|
|
|
|
'-d', 'Smart home project',
|
|
|
|
|
'--proxy-mode', 'filtered',
|
|
|
|
|
'--llm-provider', 'gemini-cli',
|
|
|
|
|
'--llm-model', 'gemini-2.0-flash',
|
|
|
|
|
'--server', 'my-grafana',
|
|
|
|
|
'--server', 'my-ha',
|
|
|
|
|
], { from: 'user' });
|
|
|
|
|
|
|
|
|
|
expect(client.post).toHaveBeenCalledWith('/api/v1/projects', expect.objectContaining({
|
|
|
|
|
name: 'smart-home',
|
|
|
|
|
description: 'Smart home project',
|
|
|
|
|
proxyMode: 'filtered',
|
|
|
|
|
llmProvider: 'gemini-cli',
|
|
|
|
|
llmModel: 'gemini-2.0-flash',
|
|
|
|
|
servers: ['my-grafana', 'my-ha'],
|
|
|
|
|
}));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('creates project with members', async () => {
|
|
|
|
|
const cmd = createCreateCommand({ client, log });
|
|
|
|
|
await cmd.parseAsync([
|
|
|
|
|
'project', 'team-project',
|
|
|
|
|
'--member', 'alice@test.com',
|
|
|
|
|
'--member', 'bob@test.com',
|
|
|
|
|
], { from: 'user' });
|
|
|
|
|
|
|
|
|
|
expect(client.post).toHaveBeenCalledWith('/api/v1/projects', expect.objectContaining({
|
|
|
|
|
name: 'team-project',
|
|
|
|
|
members: ['alice@test.com', 'bob@test.com'],
|
|
|
|
|
}));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('defaults proxy mode to direct', async () => {
|
|
|
|
|
const cmd = createCreateCommand({ client, log });
|
|
|
|
|
await cmd.parseAsync(['project', 'basic'], { from: 'user' });
|
|
|
|
|
|
|
|
|
|
expect(client.post).toHaveBeenCalledWith('/api/v1/projects', expect.objectContaining({
|
|
|
|
|
proxyMode: 'direct',
|
|
|
|
|
}));
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('get projects shows new columns', () => {
|
|
|
|
|
it('shows MODE, SERVERS, MEMBERS columns', async () => {
|
|
|
|
|
const deps = {
|
|
|
|
|
output: [] as string[],
|
|
|
|
|
fetchResource: vi.fn(async () => [{
|
|
|
|
|
id: 'proj-1',
|
|
|
|
|
name: 'smart-home',
|
|
|
|
|
description: 'Test',
|
|
|
|
|
proxyMode: 'filtered',
|
|
|
|
|
ownerId: 'user-1',
|
|
|
|
|
servers: [{ server: { name: 'grafana' } }, { server: { name: 'ha' } }],
|
|
|
|
|
members: [{ user: { email: 'alice@test.com' } }],
|
|
|
|
|
}]),
|
|
|
|
|
log: (...args: string[]) => deps.output.push(args.join(' ')),
|
|
|
|
|
};
|
|
|
|
|
const cmd = createGetCommand(deps);
|
|
|
|
|
await cmd.parseAsync(['node', 'test', 'projects']);
|
|
|
|
|
|
|
|
|
|
const text = deps.output.join('\n');
|
|
|
|
|
expect(text).toContain('MODE');
|
|
|
|
|
expect(text).toContain('SERVERS');
|
|
|
|
|
expect(text).toContain('MEMBERS');
|
|
|
|
|
expect(text).toContain('smart-home');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('describe project shows full detail', () => {
|
|
|
|
|
it('shows servers and members', async () => {
|
|
|
|
|
const deps = {
|
|
|
|
|
output: [] as string[],
|
|
|
|
|
client: mockClient(),
|
|
|
|
|
fetchResource: vi.fn(async () => ({
|
|
|
|
|
id: 'proj-1',
|
|
|
|
|
name: 'smart-home',
|
|
|
|
|
description: 'Smart home',
|
|
|
|
|
proxyMode: 'filtered',
|
|
|
|
|
llmProvider: 'gemini-cli',
|
|
|
|
|
llmModel: 'gemini-2.0-flash',
|
|
|
|
|
ownerId: 'user-1',
|
|
|
|
|
servers: [
|
|
|
|
|
{ server: { name: 'my-grafana' } },
|
|
|
|
|
{ server: { name: 'my-ha' } },
|
|
|
|
|
],
|
|
|
|
|
members: [
|
|
|
|
|
{ user: { email: 'alice@test.com' } },
|
|
|
|
|
{ user: { email: 'bob@test.com' } },
|
|
|
|
|
],
|
|
|
|
|
createdAt: '2025-01-01',
|
|
|
|
|
updatedAt: '2025-01-01',
|
|
|
|
|
})),
|
|
|
|
|
log: (...args: string[]) => deps.output.push(args.join(' ')),
|
|
|
|
|
};
|
|
|
|
|
const cmd = createDescribeCommand(deps);
|
|
|
|
|
await cmd.parseAsync(['node', 'test', 'project', 'proj-1']);
|
|
|
|
|
|
|
|
|
|
const text = deps.output.join('\n');
|
|
|
|
|
expect(text).toContain('=== Project: smart-home ===');
|
|
|
|
|
expect(text).toContain('filtered');
|
|
|
|
|
expect(text).toContain('gemini-cli');
|
|
|
|
|
expect(text).toContain('my-grafana');
|
|
|
|
|
expect(text).toContain('my-ha');
|
|
|
|
|
expect(text).toContain('alice@test.com');
|
|
|
|
|
expect(text).toContain('bob@test.com');
|
|
|
|
|
});
|
2026-02-21 05:16:57 +00:00
|
|
|
});
|
|
|
|
|
});
|