import { describe, expect, it } from 'vitest'; import { AuthAction, Resource } from '~/composables/gql/graphql'; import { decodeScopesToPermissions, encodePermissionsToScopes } from '~/utils/authorizationScopes'; describe('authorizationScopes', () => { describe('encodePermissionsToScopes', () => { describe('duplicate handling', () => { it('should deduplicate action verbs when multiple permissions have duplicate actions', () => { const permissions = [ { resource: Resource.DOCKER, actions: [AuthAction.READ_ANY, AuthAction.READ_OWN, AuthAction.UPDATE_ANY], }, ]; const scopes = encodePermissionsToScopes([], permissions); // Should produce "docker:read_any+read_own+update_any" with distinct actions preserved expect(scopes).toHaveLength(1); expect(scopes[0]).toBe('docker:read_any+read_own+update_any'); }); it('should deduplicate resource names when grouped', () => { const permissions = [ { resource: Resource.DOCKER, actions: [AuthAction.READ_ANY], }, { resource: Resource.DOCKER, actions: [AuthAction.READ_OWN], }, ]; const scopes = encodePermissionsToScopes([], permissions); // Should produce "docker:read_any+read_own" merging both permissions expect(scopes).toHaveLength(1); expect(scopes[0]).toBe('docker:read_any+read_own'); }); it('should handle multiple duplicate resources and actions correctly', () => { const permissions = [ { resource: Resource.DOCKER, actions: [AuthAction.READ_ANY, AuthAction.READ_OWN, AuthAction.UPDATE_ANY], }, { resource: Resource.VMS, actions: [AuthAction.READ_ANY, AuthAction.UPDATE_OWN, AuthAction.UPDATE_ANY], }, { resource: Resource.DOCKER, actions: [AuthAction.READ_OWN, AuthAction.UPDATE_OWN], }, ]; const scopes = encodePermissionsToScopes([], permissions); // Docker: READ_ANY, READ_OWN, UPDATE_ANY, UPDATE_OWN (merged from both) // VMS: READ_ANY, UPDATE_OWN, UPDATE_ANY // Different action sets, so separate scopes expect(scopes).toHaveLength(2); const scopeStrings = scopes.sort(); expect(scopeStrings).toContain('docker:read_any+read_own+update_any+update_own'); expect(scopeStrings).toContain('vms:read_any+update_any+update_own'); }); it('should maintain consistent sorting for actions and resources', () => { const permissions = [ { resource: Resource.VMS, actions: [AuthAction.UPDATE_ANY, AuthAction.READ_ANY], }, { resource: Resource.DOCKER, actions: [AuthAction.UPDATE_ANY, AuthAction.READ_ANY], }, ]; const scopes = encodePermissionsToScopes([], permissions); // Should sort resources (docker before vms) and actions alphabetically expect(scopes).toHaveLength(1); expect(scopes[0]).toBe('docker+vms:read_any+update_any'); }); it('should handle all CRUD actions without creating wildcard', () => { const permissions = [ { resource: Resource.DOCKER, actions: [ AuthAction.CREATE_ANY, AuthAction.CREATE_OWN, AuthAction.READ_ANY, AuthAction.READ_OWN, AuthAction.UPDATE_ANY, AuthAction.UPDATE_OWN, AuthAction.DELETE_ANY, AuthAction.DELETE_OWN, ], }, ]; const scopes = encodePermissionsToScopes([], permissions); // Should just list all actions, no wildcard conversion expect(scopes).toHaveLength(1); expect(scopes[0]).toBe( 'docker:create_any+create_own+delete_any+delete_own+read_any+read_own+update_any+update_own' ); }); it('should handle edge case with empty actions after deduplication', () => { const permissions = [ { resource: Resource.DOCKER, actions: [], }, ]; const scopes = encodePermissionsToScopes([], permissions); // Should not produce any scope for empty actions expect(scopes).toHaveLength(0); }); it('should deduplicate resources across multiple permissions with same action set', () => { const permissions = [ { resource: Resource.DOCKER, actions: [AuthAction.READ_ANY], }, { resource: Resource.VMS, actions: [AuthAction.READ_OWN], }, { resource: Resource.DOCKER, actions: [AuthAction.READ_OWN], }, { resource: Resource.VMS, actions: [AuthAction.READ_ANY], }, ]; const scopes = encodePermissionsToScopes([], permissions); // Both DOCKER and VMS have READ_ANY and READ_OWN, should group without duplicates expect(scopes).toHaveLength(1); expect(scopes[0]).toBe('docker+vms:read_any+read_own'); }); it('should produce deterministic output for same input regardless of order', () => { const permissions1 = [ { resource: Resource.VMS, actions: [AuthAction.UPDATE_ANY, AuthAction.READ_ANY] }, { resource: Resource.DOCKER, actions: [AuthAction.READ_ANY, AuthAction.UPDATE_ANY] }, ]; const permissions2 = [ { resource: Resource.DOCKER, actions: [AuthAction.UPDATE_ANY, AuthAction.READ_ANY] }, { resource: Resource.VMS, actions: [AuthAction.READ_ANY, AuthAction.UPDATE_ANY] }, ]; const scopes1 = encodePermissionsToScopes([], permissions1); const scopes2 = encodePermissionsToScopes([], permissions2); expect(scopes1).toEqual(scopes2); expect(scopes1[0]).toBe('docker+vms:read_any+update_any'); }); }); describe('roundtrip encoding/decoding', () => { it('should maintain permissions through encode/decode cycle with duplicates', () => { const originalPermissions = [ { resource: Resource.DOCKER, actions: [AuthAction.READ_ANY, AuthAction.READ_OWN, AuthAction.UPDATE_ANY], }, { resource: Resource.VMS, actions: [AuthAction.READ_OWN, AuthAction.UPDATE_OWN, AuthAction.UPDATE_ANY], }, ]; const scopes = encodePermissionsToScopes([], originalPermissions); const { permissions: decoded } = decodeScopesToPermissions(scopes); // Now possession is preserved in the encoding expect(decoded).toHaveLength(2); const dockerPerm = decoded.find((p) => p.resource === Resource.DOCKER); const vmsPerm = decoded.find((p) => p.resource === Resource.VMS); // Docker should have its specific actions preserved expect(dockerPerm?.actions).toContain(AuthAction.READ_ANY); expect(dockerPerm?.actions).toContain(AuthAction.READ_OWN); expect(dockerPerm?.actions).toContain(AuthAction.UPDATE_ANY); // VMS should have its specific actions preserved expect(vmsPerm?.actions).toContain(AuthAction.READ_OWN); expect(vmsPerm?.actions).toContain(AuthAction.UPDATE_OWN); expect(vmsPerm?.actions).toContain(AuthAction.UPDATE_ANY); // The scopes should be separate since they have different action sets expect(scopes).toHaveLength(2); }); }); }); });