| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374 |
- import dayjs from 'dayjs'
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
- import type { BaseToken } from './AccessToken.types'
- import {
- filterAndSortTokens,
- formatAccessText,
- getExpirationDate,
- getRealAccess,
- getResourcePermissions,
- handleSortChange,
- mapPermissionToFGA,
- } from './AccessToken.utils'
- // Mock PERMISSION_LIST so tests are deterministic and don't break when shared-types updates
- vi.mock('./AccessToken.constants', () => ({
- PERMISSION_LIST: [
- {
- scope: 'organization',
- resource: 'billing',
- action: 'read',
- id: 'org-billing-read',
- title: 'Read Billing',
- },
- {
- scope: 'organization',
- resource: 'billing',
- action: 'write',
- id: 'org-billing-write',
- title: 'Manage Billing',
- },
- {
- scope: 'organization',
- resource: 'members',
- action: 'read',
- id: 'org-members-read',
- title: 'Read Members',
- },
- {
- scope: 'organization',
- resource: 'members',
- action: 'write',
- id: 'org-members-write',
- title: 'Manage Members',
- },
- {
- scope: 'organization',
- resource: 'members',
- action: 'create',
- id: 'org-members-create',
- title: 'Create Members',
- },
- {
- scope: 'organization',
- resource: 'members',
- action: 'delete',
- id: 'org-members-delete',
- title: 'Delete Members',
- },
- {
- scope: 'project',
- resource: 'database',
- action: 'read',
- id: 'proj-db-read',
- title: 'Read Database',
- },
- ],
- }))
- // --- handleSortChange ---
- describe('handleSortChange', () => {
- it('should toggle from asc to desc for the same column', () => {
- const setSort = vi.fn()
- handleSortChange('created_at:asc', 'created_at', setSort)
- expect(setSort).toHaveBeenCalledWith('created_at:desc')
- })
- it('should reset to created_at:desc when toggling off desc on a non-default column', () => {
- const setSort = vi.fn()
- handleSortChange('last_used_at:desc', 'last_used_at', setSort)
- expect(setSort).toHaveBeenCalledWith('created_at:desc')
- })
- it('should set new column to asc when switching columns', () => {
- const setSort = vi.fn()
- handleSortChange('created_at:desc', 'expires_at', setSort)
- expect(setSort).toHaveBeenCalledWith('expires_at:asc')
- })
- it('should reset to created_at:desc when toggling off desc on created_at itself', () => {
- const setSort = vi.fn()
- handleSortChange('created_at:desc', 'created_at', setSort)
- expect(setSort).toHaveBeenCalledWith('created_at:desc')
- })
- it('should set last_used_at to asc when switching from expires_at', () => {
- const setSort = vi.fn()
- handleSortChange('expires_at:asc', 'last_used_at', setSort)
- expect(setSort).toHaveBeenCalledWith('last_used_at:asc')
- })
- })
- // --- filterAndSortTokens ---
- const makeToken = (overrides: Partial<BaseToken> = {}): BaseToken => ({
- id: '1',
- name: 'Token',
- token_alias: 'alias',
- created_at: '2024-01-01T00:00:00Z',
- last_used_at: null,
- expires_at: null,
- ...overrides,
- })
- describe('filterAndSortTokens', () => {
- const tokens: BaseToken[] = [
- makeToken({ id: '1', name: 'Alpha', created_at: '2024-01-03T00:00:00Z' }),
- makeToken({ id: '2', name: 'Beta', created_at: '2024-01-01T00:00:00Z' }),
- makeToken({ id: '3', name: 'Gamma', created_at: '2024-01-02T00:00:00Z' }),
- ]
- it('should return undefined when tokens is undefined', () => {
- expect(filterAndSortTokens(undefined, '', 'created_at:desc')).toBeUndefined()
- })
- it('should return all tokens when search string is empty', () => {
- const result = filterAndSortTokens(tokens, '', 'created_at:asc')
- expect(result).toHaveLength(3)
- })
- it('should filter tokens by name case-insensitively', () => {
- const result = filterAndSortTokens(tokens, 'alpha', 'created_at:asc')
- expect(result).toHaveLength(1)
- expect(result![0].name).toBe('Alpha')
- })
- it('should return empty array when no tokens match', () => {
- const result = filterAndSortTokens(tokens, 'nonexistent', 'created_at:asc')
- expect(result).toHaveLength(0)
- })
- it('should sort by created_at ascending', () => {
- const result = filterAndSortTokens(tokens, '', 'created_at:asc')!
- expect(result.map((t) => t.id)).toEqual(['2', '3', '1'])
- })
- it('should sort by created_at descending', () => {
- const result = filterAndSortTokens(tokens, '', 'created_at:desc')!
- expect(result.map((t) => t.id)).toEqual(['1', '3', '2'])
- })
- it('should sort by last_used_at ascending with nulls last', () => {
- const tokensWithUsage: BaseToken[] = [
- makeToken({ id: '1', last_used_at: '2024-03-01T00:00:00Z' }),
- makeToken({ id: '2', last_used_at: null }),
- makeToken({ id: '3', last_used_at: '2024-01-01T00:00:00Z' }),
- ]
- const result = filterAndSortTokens(tokensWithUsage, '', 'last_used_at:asc')!
- expect(result.map((t) => t.id)).toEqual(['3', '1', '2'])
- })
- it('should sort by last_used_at descending with nulls last', () => {
- const tokensWithUsage: BaseToken[] = [
- makeToken({ id: '1', last_used_at: '2024-01-01T00:00:00Z' }),
- makeToken({ id: '2', last_used_at: null }),
- makeToken({ id: '3', last_used_at: '2024-03-01T00:00:00Z' }),
- ]
- const result = filterAndSortTokens(tokensWithUsage, '', 'last_used_at:desc')!
- expect(result.map((t) => t.id)).toEqual(['3', '1', '2'])
- })
- it('should keep order stable when both last_used_at values are null', () => {
- const tokensAllNull: BaseToken[] = [
- makeToken({ id: '1', last_used_at: null }),
- makeToken({ id: '2', last_used_at: null }),
- ]
- const result = filterAndSortTokens(tokensAllNull, '', 'last_used_at:asc')!
- expect(result).toHaveLength(2)
- })
- it('should sort by expires_at ascending with nulls last', () => {
- const tokensWithExpiry: BaseToken[] = [
- makeToken({ id: '1', expires_at: '2025-06-01T00:00:00Z' }),
- makeToken({ id: '2', expires_at: null }),
- makeToken({ id: '3', expires_at: '2025-01-01T00:00:00Z' }),
- ]
- const result = filterAndSortTokens(tokensWithExpiry, '', 'expires_at:asc')!
- expect(result.map((t) => t.id)).toEqual(['3', '1', '2'])
- })
- it('should sort by expires_at descending with nulls last', () => {
- const tokensWithExpiry: BaseToken[] = [
- makeToken({ id: '1', expires_at: '2025-01-01T00:00:00Z' }),
- makeToken({ id: '2', expires_at: null }),
- makeToken({ id: '3', expires_at: '2025-06-01T00:00:00Z' }),
- ]
- const result = filterAndSortTokens(tokensWithExpiry, '', 'expires_at:desc')!
- expect(result.map((t) => t.id)).toEqual(['3', '1', '2'])
- })
- it('should keep order stable when both expires_at values are null', () => {
- const tokensAllNull: BaseToken[] = [
- makeToken({ id: '1', expires_at: null }),
- makeToken({ id: '2', expires_at: null }),
- ]
- const result = filterAndSortTokens(tokensAllNull, '', 'expires_at:asc')!
- expect(result).toHaveLength(2)
- })
- it('should not mutate the original tokens array', () => {
- const original = [...tokens]
- filterAndSortTokens(tokens, '', 'created_at:asc')
- expect(tokens).toEqual(original)
- })
- })
- // --- mapPermissionToFGA ---
- describe('mapPermissionToFGA', () => {
- it('should return the permission id for a matching scope:resource + action', () => {
- const result = mapPermissionToFGA('organization:billing', 'read')
- expect(result).toEqual(['org-billing-read'])
- })
- it('should return empty array when no permission matches', () => {
- const result = mapPermissionToFGA('nonexistent:resource', 'read')
- expect(result).toEqual([])
- })
- it('should return empty array when action does not match', () => {
- const result = mapPermissionToFGA('organization:billing', 'delete')
- expect(result).toEqual([])
- })
- })
- // --- getResourcePermissions ---
- describe('getResourcePermissions', () => {
- it('should always include "no access" with an empty array', () => {
- const result = getResourcePermissions('organization:billing')
- expect(result['no access']).toEqual([])
- })
- it('should include individual actions from PERMISSION_LIST', () => {
- const result = getResourcePermissions('organization:billing')
- expect(result['read']).toEqual(['org-billing-read'])
- expect(result['write']).toEqual(['org-billing-write'])
- })
- it('should include combined "read-write" when both read and write exist', () => {
- const result = getResourcePermissions('organization:billing')
- expect(result['read-write']).toEqual(['org-billing-read', 'org-billing-write'])
- })
- it('should return only "no access" for an unknown resource', () => {
- const result = getResourcePermissions('fake:resource')
- expect(Object.keys(result)).toEqual(['no access'])
- })
- it('should include all actions for a resource with many permissions', () => {
- const result = getResourcePermissions('organization:members')
- expect(result['read']).toEqual(['org-members-read'])
- expect(result['write']).toEqual(['org-members-write'])
- expect(result['create']).toEqual(['org-members-create'])
- expect(result['delete']).toEqual(['org-members-delete'])
- expect(result['read-write']).toEqual(['org-members-read', 'org-members-write'])
- })
- })
- // --- getRealAccess ---
- describe('getRealAccess', () => {
- it('should return "no access" when token has no matching permissions', () => {
- expect(getRealAccess('organization:billing', [])).toBe('no access')
- })
- it('should return "no access" when permissions do not match the resource', () => {
- expect(getRealAccess('organization:billing', ['unrelated-id'])).toBe('no access')
- })
- it('should return the single action when only one matches', () => {
- expect(getRealAccess('organization:billing', ['org-billing-read'])).toBe('read')
- })
- it('should return "read-write" when both read and write permissions match', () => {
- expect(getRealAccess('organization:billing', ['org-billing-read', 'org-billing-write'])).toBe(
- 'read-write'
- )
- })
- it('should join multiple granted actions with hyphens when not exactly read+write', () => {
- expect(
- getRealAccess('organization:members', [
- 'org-members-read',
- 'org-members-write',
- 'org-members-create',
- ])
- ).toBe('read-write-create')
- })
- it('should return single action for write-only access', () => {
- expect(getRealAccess('organization:billing', ['org-billing-write'])).toBe('write')
- })
- })
- // --- formatAccessText ---
- describe('formatAccessText', () => {
- it('should return "No access" for "no access"', () => {
- expect(formatAccessText('no access')).toBe('No access')
- })
- it('should capitalize a single word', () => {
- expect(formatAccessText('read')).toBe('Read')
- })
- it('should capitalize each hyphen-separated word', () => {
- expect(formatAccessText('read-write')).toBe('Read-Write')
- })
- it('should handle multi-segment actions', () => {
- expect(formatAccessText('read-write-delete')).toBe('Read-Write-Delete')
- })
- })
- // --- getExpirationDate ---
- describe('getExpirationDate', () => {
- const FIXED_DATE = new Date('2025-06-15T12:00:00.000Z')
- beforeEach(() => {
- vi.useFakeTimers()
- vi.setSystemTime(FIXED_DATE)
- })
- afterEach(() => {
- vi.useRealTimers()
- })
- it('should return a date exactly 1 hour from now for "hour"', () => {
- const result = getExpirationDate('hour')!
- expect(result).toBe(dayjs(FIXED_DATE).add(1, 'hours').toISOString())
- })
- it('should return a date exactly 1 day from now for "day"', () => {
- const result = getExpirationDate('day')!
- expect(result).toBe(dayjs(FIXED_DATE).add(1, 'day').toISOString())
- })
- it('should return a date exactly 7 days from now for "week"', () => {
- const result = getExpirationDate('week')!
- expect(result).toBe(dayjs(FIXED_DATE).add(7, 'days').toISOString())
- })
- it('should return a date exactly 30 days from now for "month"', () => {
- const result = getExpirationDate('month')!
- expect(result).toBe(dayjs(FIXED_DATE).add(30, 'days').toISOString())
- })
- it('should return undefined for "never"', () => {
- expect(getExpirationDate('never')).toBeUndefined()
- })
- it('should return undefined for an unknown key', () => {
- expect(getExpirationDate('unknown')).toBeUndefined()
- })
- it('should return a valid ISO string', () => {
- const result = getExpirationDate('hour')!
- expect(dayjs(result).isValid()).toBe(true)
- expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T/)
- })
- })
|