import { suite } from '../lib/context.js' suite('RBAC', { tags: ['rbac', 'permissions'] }, (t) => { // Helper: register a user with no roles (restricted) async function createRestrictedUser() { const email = `restricted-${Date.now()}@test.com` const password = 'testpassword1234' // Register via raw fetch (needs x-company-id) const registerRes = await fetch(`${t.baseUrl}/v1/auth/register`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-company-id': 'a0000000-0000-0000-0000-000000000001' }, body: JSON.stringify({ email, password, firstName: 'Restricted', lastName: 'User', role: 'staff' }), }) const registerData = await registerRes.json() as { token: string } // Login (no roles assigned, so no permissions) const loginRes = await fetch(`${t.baseUrl}/v1/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), }) const loginData = await loginRes.json() as { token: string } return loginData.token } // Helper: register a user and assign a specific role async function createUserWithRole(roleSlug: string) { const email = `${roleSlug}-${Date.now()}@test.com` const password = 'testpassword1234' const registerRes = await fetch(`${t.baseUrl}/v1/auth/register`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-company-id': 'a0000000-0000-0000-0000-000000000001' }, body: JSON.stringify({ email, password, firstName: roleSlug, lastName: 'User', role: 'staff' }), }) const registerData = await registerRes.json() as { user: { id: string } } // Get the role and assign it const rolesRes = await t.api.get<{ data: { id: string; slug: string }[] }>('/v1/roles') const role = rolesRes.data.data.find((r: { slug: string }) => r.slug === roleSlug) if (role) { await t.api.post(`/v1/users/${registerData.user.id}/roles`, { roleId: role.id }) } // Login to get token with permissions const loginRes = await fetch(`${t.baseUrl}/v1/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), }) const loginData = await loginRes.json() as { token: string } return loginData.token } async function fetchAs(token: string, method: string, path: string, body?: unknown) { const headers: Record = { Authorization: `Bearer ${token}` } if (body !== undefined) headers['Content-Type'] = 'application/json' const res = await fetch(`${t.baseUrl}${path}`, { method, headers, body: body !== undefined ? JSON.stringify(body) : undefined, }) return { status: res.status, data: await res.json() } } t.test('user with no roles gets 403 on all routes', { tags: ['deny'] }, async () => { const token = await createRestrictedUser() const res = await fetchAs(token, 'GET', '/v1/accounts') t.assert.equal(res.status, 403) }) t.test('viewer role can read accounts but not create', { tags: ['viewer'] }, async () => { const token = await createUserWithRole('viewer') const readRes = await fetchAs(token, 'GET', '/v1/accounts') t.assert.equal(readRes.status, 200) const createRes = await fetchAs(token, 'POST', '/v1/accounts', { name: 'Should Fail' }) t.assert.equal(createRes.status, 403) }) t.test('viewer cannot delete accounts', { tags: ['viewer'] }, async () => { const token = await createUserWithRole('viewer') // Try to delete — should be 403 even before 404 const res = await fetchAs(token, 'DELETE', '/v1/accounts/a0000000-0000-0000-0000-999999999999') t.assert.equal(res.status, 403) }) t.test('sales associate can create accounts but not delete', { tags: ['sales'] }, async () => { const token = await createUserWithRole('sales_associate') const createRes = await fetchAs(token, 'POST', '/v1/accounts', { name: 'Sales Test' }) t.assert.equal(createRes.status, 201) const deleteRes = await fetchAs(token, 'DELETE', `/v1/accounts/${createRes.data.id}`) t.assert.equal(deleteRes.status, 403) }) t.test('technician can view repairs but not accounts edit', { tags: ['technician'] }, async () => { const token = await createUserWithRole('technician') // Can view accounts (via accounts.view in technician role — wait, technician doesn't have accounts.view) const acctRes = await fetchAs(token, 'GET', '/v1/accounts') t.assert.equal(acctRes.status, 200) // technician has accounts.view // Cannot create accounts const createRes = await fetchAs(token, 'POST', '/v1/accounts', { name: 'Should Fail' }) t.assert.equal(createRes.status, 403) }) t.test('instructor cannot access inventory', { tags: ['instructor'] }, async () => { const token = await createUserWithRole('instructor') const res = await fetchAs(token, 'GET', '/v1/products') t.assert.equal(res.status, 403) }) t.test('admin role has access to everything', { tags: ['admin'] }, async () => { // The default test user is admin — just verify const res = await t.api.get('/v1/roles') t.assert.status(res, 200) const permsRes = await t.api.get('/v1/permissions') t.assert.status(permsRes, 200) const usersRes = await t.api.get('/v1/users') t.assert.status(usersRes, 200) }) t.test('permission inheritance: admin implies edit and view', { tags: ['inheritance'] }, async () => { // Create a custom role with only accounts.admin const roleRes = await t.api.post('/v1/roles', { name: 'Admin Only Test', slug: `admin_only_${Date.now()}`, permissionSlugs: ['accounts.admin'], }) t.assert.status(roleRes, 201) // Create user and assign this role const email = `inherit-${Date.now()}@test.com` const password = 'testpassword1234' const regRes = await fetch(`${t.baseUrl}/v1/auth/register`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-company-id': 'a0000000-0000-0000-0000-000000000001' }, body: JSON.stringify({ email, password, firstName: 'Inherit', lastName: 'Test', role: 'staff' }), }) const regData = await regRes.json() as { user: { id: string } } await t.api.post(`/v1/users/${regData.user.id}/roles`, { roleId: roleRes.data.id }) const loginRes = await fetch(`${t.baseUrl}/v1/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), }) const loginData = await loginRes.json() as { token: string } // Should be able to view (inherited from admin) const viewRes = await fetchAs(loginData.token, 'GET', '/v1/accounts') t.assert.equal(viewRes.status, 200) // Should be able to create (edit inherited from admin) const createRes = await fetchAs(loginData.token, 'POST', '/v1/accounts', { name: 'Inherited Edit' }) t.assert.equal(createRes.status, 201) // Should be able to delete (admin) const deleteRes = await fetchAs(loginData.token, 'DELETE', `/v1/accounts/${createRes.data.id}`) t.assert.equal(deleteRes.status, 200) }) t.test('roles list returns system roles', { tags: ['roles'] }, async () => { const res = await t.api.get('/v1/roles') t.assert.status(res, 200) const slugs = res.data.data.map((r: { slug: string }) => r.slug) t.assert.includes(slugs, 'admin') t.assert.includes(slugs, 'manager') t.assert.includes(slugs, 'sales_associate') t.assert.includes(slugs, 'technician') t.assert.includes(slugs, 'instructor') t.assert.includes(slugs, 'viewer') }) t.test('permissions list returns all system permissions', { tags: ['permissions'] }, async () => { const res = await t.api.get('/v1/permissions') t.assert.status(res, 200) t.assert.greaterThan(res.data.data.length, 30) }) t.test('cannot delete system role', { tags: ['roles'] }, async () => { const rolesRes = await t.api.get('/v1/roles') const adminRole = rolesRes.data.data.find((r: { slug: string }) => r.slug === 'admin') t.assert.ok(adminRole) const deleteRes = await t.api.del(`/v1/roles/${adminRole.id}`) t.assert.equal(deleteRes.status, 403) }) t.test('can create and delete custom role', { tags: ['roles'] }, async () => { const createRes = await t.api.post('/v1/roles', { name: 'Temp Role', slug: `temp_${Date.now()}`, permissionSlugs: ['accounts.view'], }) t.assert.status(createRes, 201) const deleteRes = await t.api.del(`/v1/roles/${createRes.data.id}`) t.assert.status(deleteRes, 200) }) })