feat: roles SPA Users, Profile, API keys, Activity
This commit is contained in:
1 parent
3db490a615
commit
34dffe222f
98 files changed
+4957
-434
No files matched your search
@@ -0,0 +1,11 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { ACTIVITY_CATEGORIES, categoryIcon, categoryLabel } from '@/lib/activity'
|
||||
|
||||
describe('activity categories', () => {
|
||||
it('offers the users and roles categories with a name and an icon', () => {
|
||||
expect(ACTIVITY_CATEGORIES).toContain('users')
|
||||
expect(ACTIVITY_CATEGORIES).toContain('roles')
|
||||
expect(categoryLabel('roles')).toBe('Roles')
|
||||
expect(categoryIcon('roles')).toMatch(/^i-lucide-/)
|
||||
})
|
||||
})
|
||||
@@ -285,6 +285,12 @@ describe('The catalogue is complete in both languages', () => {
|
||||
'system.restart_requested',
|
||||
'system.activity_retention_changed',
|
||||
'system.activity_cleanup',
|
||||
'role.create',
|
||||
'role.edit',
|
||||
'role.delete',
|
||||
'role.reassign',
|
||||
'user.change_role',
|
||||
'user.transfer_ownership',
|
||||
].sort(),
|
||||
)
|
||||
})
|
||||
@@ -293,3 +299,84 @@ describe('The catalogue is complete in both languages', () => {
|
||||
expect(t('Revoked API key {0}', ['Mobile app'])).toBe('Revoked API key Mobile app')
|
||||
})
|
||||
})
|
||||
|
||||
describe('role and ownership sentences', () => {
|
||||
const roleTarget = (name: string) => ({ type: 'role', id: '4', name, exists: true })
|
||||
const userTarget = (name: string) => ({ type: 'user', id: '9', name, exists: true })
|
||||
|
||||
it.each([
|
||||
['role.create', 'Contributor', 'Role Contributor created'],
|
||||
['role.edit', 'Contributor', 'Role Contributor edited'],
|
||||
['role.delete', 'Intern', 'Role Intern deleted'],
|
||||
])('writes %s as a sentence', (action, name, sentence) => {
|
||||
const event = item({ action, category: 'roles', target: roleTarget(name) })
|
||||
|
||||
expect(sentenceText(event)).toBe(sentence)
|
||||
const strong = composeSentence(event).find((segment) => segment.strong)
|
||||
expect(strong?.text).toBe(name)
|
||||
})
|
||||
|
||||
it('writes user.change_role with the role before and after', () => {
|
||||
const event = item({
|
||||
action: 'user.change_role',
|
||||
category: 'users',
|
||||
target: userTarget('Ines Duarte'),
|
||||
changes: [{ field: 'role', kind: 'text', before: 'Writer', after: 'Content lead' }],
|
||||
})
|
||||
|
||||
expect(sentenceText(event)).toBe("Ines Duarte's role changed from Writer to Content lead")
|
||||
})
|
||||
|
||||
it('falls back for user.change_role without a role change', () => {
|
||||
const event = item({
|
||||
action: 'user.change_role',
|
||||
category: 'users',
|
||||
target: userTarget('Ines Duarte'),
|
||||
})
|
||||
|
||||
expect(sentenceText(event)).toContain('Ines Duarte')
|
||||
})
|
||||
|
||||
it('writes user.transfer_ownership from the actor to the target', () => {
|
||||
const event = item({
|
||||
action: 'user.transfer_ownership',
|
||||
category: 'users',
|
||||
security: true,
|
||||
target: userTarget('Jonas Weber'),
|
||||
})
|
||||
|
||||
expect(sentenceText(event)).toBe('Ownership transferred from Marta Alves to Jonas Weber')
|
||||
})
|
||||
|
||||
it('writes role.reassign and the failed templates without a crash', () => {
|
||||
const reassign = item({
|
||||
action: 'role.reassign',
|
||||
category: 'roles',
|
||||
target: roleTarget('Contributor'),
|
||||
params: { from: 4, to: 3, moved: 4 },
|
||||
})
|
||||
|
||||
expect(sentenceText(reassign)).toContain('Contributor')
|
||||
for (const action of [
|
||||
'role.create',
|
||||
'role.edit',
|
||||
'role.delete',
|
||||
'role.reassign',
|
||||
'user.change_role',
|
||||
'user.transfer_ownership',
|
||||
]) {
|
||||
const failed = item({
|
||||
action,
|
||||
outcome: 'failed',
|
||||
category: 'roles',
|
||||
target: roleTarget('Contributor'),
|
||||
})
|
||||
expect(sentenceText(failed)).toContain('Contributor')
|
||||
expect(sentenceText(failed)).not.toBe(sentenceText({ ...failed, outcome: 'success' }))
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps an unknown action as its own name', () => {
|
||||
expect(sentenceText(item({ action: 'role.unknown' }))).toBe('role.unknown')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,22 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { parseRoleAssignedBy } from '@/lib/permissions'
|
||||
|
||||
describe('parseRoleAssignedBy', () => {
|
||||
it('reads the object', () => {
|
||||
expect(parseRoleAssignedBy({ id: 3, name: 'Marta Alves' })).toEqual({
|
||||
id: 3,
|
||||
name: 'Marta Alves',
|
||||
})
|
||||
})
|
||||
|
||||
it.each([
|
||||
[null],
|
||||
[undefined],
|
||||
[{}],
|
||||
[{ id: 3, name: '' }],
|
||||
[{ id: '3', name: 'Marta' }],
|
||||
['x'],
|
||||
])('reads %j as no assigner', (value) => {
|
||||
expect(parseRoleAssignedBy(value)).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,61 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { ApiError } from '@/lib/apiError'
|
||||
import { inlineRefusal, refusalToast, userToast } from '@/lib/userRefusal'
|
||||
import { inviteWindow } from '@/lib/users'
|
||||
|
||||
describe('userToast', () => {
|
||||
it('builds a success toast with the check icon and lazy text', () => {
|
||||
let title = 'first'
|
||||
const build = userToast('id-1', 'success', () => title, 'fixed')
|
||||
|
||||
const toast = build()
|
||||
expect(toast).toMatchObject({
|
||||
id: 'id-1',
|
||||
color: 'success',
|
||||
icon: 'i-lucide-circle-check',
|
||||
title: 'first',
|
||||
description: 'fixed',
|
||||
})
|
||||
title = 'second'
|
||||
expect(toast.title).toBe('second')
|
||||
})
|
||||
|
||||
it('builds an error toast with the cross icon', () => {
|
||||
expect(userToast('id-2', 'error', 'Oops', 'Try again')()).toMatchObject({
|
||||
id: 'id-2',
|
||||
color: 'error',
|
||||
icon: 'i-lucide-circle-x',
|
||||
title: 'Oops',
|
||||
description: 'Try again',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('refusalToast', () => {
|
||||
it('shows the refusal copy of an inline refusal as an error toast', () => {
|
||||
const refusal = inlineRefusal(
|
||||
new ApiError(403, undefined, 'own_role', { code: 'own_role' }),
|
||||
)!
|
||||
|
||||
expect(refusalToast('refused-1', refusal)()).toMatchObject({
|
||||
id: 'refused-1',
|
||||
color: 'error',
|
||||
title: refusal.title,
|
||||
description: refusal.description,
|
||||
})
|
||||
expect(refusal.title).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('inviteWindow', () => {
|
||||
const created = '2026-10-01T09:00:00Z'
|
||||
|
||||
it.each([
|
||||
['2026-10-02T09:00:00Z', '1 day'],
|
||||
['2026-10-04T09:00:00Z', '3 days'],
|
||||
['2026-10-08T09:00:00Z', '7 days'],
|
||||
['2026-10-01T09:00:00Z', '1 day'],
|
||||
])('words an expiry at %s as %s', (expires, wording) => {
|
||||
expect(inviteWindow({ created_at: created, expires_at: expires })).toBe(wording)
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user