|
| 1 | +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | + |
| 3 | +/** |
| 4 | + * [ADR-0131 D3/D10] The pin of `permissionSetDefinitionFromRow` — the mapping |
| 5 | + * the upgrade ceremony applies to turn a Setup-authored `sys_permission_set` |
| 6 | + * row into the definition it saves through the metadata door. |
| 7 | + * |
| 8 | + * The expected bodies are the outputs the module doc states, written out by |
| 9 | + * hand: this package cannot import the security plugin's copy, so the |
| 10 | + * byte-for-behaviour comparison with it was a one-time measurement, recorded on |
| 11 | + * the pull request that added this file. |
| 12 | + */ |
| 13 | + |
| 14 | +import { describe, expect, it } from 'vitest'; |
| 15 | +import { PermissionSetSchema } from '@objectstack/spec/security'; |
| 16 | +import { permissionSetDefinitionFromRow } from './permission-set-row-definition.js'; |
| 17 | + |
| 18 | +/** A row as `sys_permission_set` stores it: the facets are JSON text, and the state columns ride along. */ |
| 19 | +const storedRow = { |
| 20 | + id: 'ps_01', |
| 21 | + name: 'sales_ops', |
| 22 | + label: 'Sales Operations', |
| 23 | + description: 'Edits accounts and reads opportunities', |
| 24 | + object_permissions: JSON.stringify({ account: { allowRead: true, allowEdit: true }, opportunity: { allowRead: true } }), |
| 25 | + field_permissions: JSON.stringify({ 'account.annual_revenue': { readable: true, editable: false } }), |
| 26 | + system_permissions: JSON.stringify(['export_reports']), |
| 27 | + row_level_security: JSON.stringify([]), |
| 28 | + tab_permissions: JSON.stringify({ accounts: 'visible' }), |
| 29 | + admin_scope: JSON.stringify({ businessUnit: 'emea' }), |
| 30 | + active: false, |
| 31 | + managed_by: 'admin', |
| 32 | + customized: true, |
| 33 | + package_id: null, |
| 34 | + organization_id: 'org_1', |
| 35 | + created_at: '2026-01-01T00:00:00.000Z', |
| 36 | + updated_at: '2026-01-02T00:00:00.000Z', |
| 37 | +}; |
| 38 | + |
| 39 | +/** Every body key the mapping can emit, in the order it emits them. */ |
| 40 | +const EVERY_BODY_KEY = [ |
| 41 | + 'name', 'label', 'description', 'objects', 'fields', |
| 42 | + 'systemPermissions', 'rowLevelSecurity', 'tabPermissions', 'adminScope', |
| 43 | +]; |
| 44 | + |
| 45 | +describe('permissionSetDefinitionFromRow', () => { |
| 46 | + it('turns the snake_case JSON facet columns into the camelCase body, key for key and in order', () => { |
| 47 | + const body = permissionSetDefinitionFromRow(storedRow); |
| 48 | + expect(body).toStrictEqual({ |
| 49 | + name: 'sales_ops', |
| 50 | + label: 'Sales Operations', |
| 51 | + description: 'Edits accounts and reads opportunities', |
| 52 | + objects: { account: { allowRead: true, allowEdit: true }, opportunity: { allowRead: true } }, |
| 53 | + fields: { 'account.annual_revenue': { readable: true, editable: false } }, |
| 54 | + systemPermissions: ['export_reports'], |
| 55 | + rowLevelSecurity: [], |
| 56 | + tabPermissions: { accounts: 'visible' }, |
| 57 | + adminScope: { businessUnit: 'emea' }, |
| 58 | + }); |
| 59 | + expect(Object.keys(body)).toEqual(EVERY_BODY_KEY); |
| 60 | + }); |
| 61 | + |
| 62 | + it('every key it emits is one PermissionSetSchema declares, and none is filtered away (a spec rename reddens this)', () => { |
| 63 | + const declared = Object.keys((PermissionSetSchema as unknown as { shape: Record<string, unknown> }).shape); |
| 64 | + for (const key of EVERY_BODY_KEY) expect(declared, key).toContain(key); |
| 65 | + expect(Object.keys(permissionSetDefinitionFromRow(storedRow))).toHaveLength(EVERY_BODY_KEY.length); |
| 66 | + }); |
| 67 | + |
| 68 | + it('a well-formed row answers a body the permission-set schema accepts', () => { |
| 69 | + const parsed = PermissionSetSchema.safeParse(permissionSetDefinitionFromRow(storedRow)); |
| 70 | + expect(parsed.success, JSON.stringify(parsed.error?.issues)).toBe(true); |
| 71 | + }); |
| 72 | + |
| 73 | + it('never carries a row state, provenance or identity column', () => { |
| 74 | + const body = permissionSetDefinitionFromRow(storedRow); |
| 75 | + for (const column of ['id', 'active', 'managed_by', 'customized', 'package_id', 'organization_id', 'created_at', 'updated_at']) { |
| 76 | + expect(body, column).not.toHaveProperty(column); |
| 77 | + } |
| 78 | + }); |
| 79 | + |
| 80 | + it('takes a facet a driver already parsed as it is', () => { |
| 81 | + const body = permissionSetDefinitionFromRow({ |
| 82 | + name: 'parsed', |
| 83 | + object_permissions: { account: { allowRead: true } }, |
| 84 | + system_permissions: ['manage_users'], |
| 85 | + admin_scope: { businessUnit: 'apac' }, |
| 86 | + }); |
| 87 | + expect(body.objects).toEqual({ account: { allowRead: true } }); |
| 88 | + expect(body.systemPermissions).toEqual(['manage_users']); |
| 89 | + expect(body.adminScope).toEqual({ businessUnit: 'apac' }); |
| 90 | + }); |
| 91 | + |
| 92 | + it('an absent, null, empty, JSON-null or unparseable facet answers its default', () => { |
| 93 | + for (const stored of [undefined, null, '', 'null', '{not json']) { |
| 94 | + const body = permissionSetDefinitionFromRow({ |
| 95 | + name: 'defaults', |
| 96 | + object_permissions: stored, |
| 97 | + field_permissions: stored, |
| 98 | + system_permissions: stored, |
| 99 | + row_level_security: stored, |
| 100 | + tab_permissions: stored, |
| 101 | + }); |
| 102 | + expect(body, String(stored)).toStrictEqual({ |
| 103 | + name: 'defaults', |
| 104 | + label: 'defaults', |
| 105 | + objects: {}, |
| 106 | + fields: {}, |
| 107 | + systemPermissions: [], |
| 108 | + rowLevelSecurity: [], |
| 109 | + tabPermissions: {}, |
| 110 | + }); |
| 111 | + } |
| 112 | + }); |
| 113 | + |
| 114 | + it('answers a fresh default per call, never one shared object', () => { |
| 115 | + const first = permissionSetDefinitionFromRow({ name: 'a' }); |
| 116 | + const second = permissionSetDefinitionFromRow({ name: 'b' }); |
| 117 | + expect(first.objects).not.toBe(second.objects); |
| 118 | + expect(first.systemPermissions).not.toBe(second.systemPermissions); |
| 119 | + }); |
| 120 | + |
| 121 | + it('the label falls back to the name only when the column is null or absent', () => { |
| 122 | + expect(permissionSetDefinitionFromRow({ name: 'n', label: null }).label).toBe('n'); |
| 123 | + expect(permissionSetDefinitionFromRow({ name: 'n' }).label).toBe('n'); |
| 124 | + expect(permissionSetDefinitionFromRow({ name: 'n', label: '' }).label).toBe(''); |
| 125 | + }); |
| 126 | + |
| 127 | + it('the description travels unless the column is null or absent', () => { |
| 128 | + expect(permissionSetDefinitionFromRow({ name: 'n', description: null })).not.toHaveProperty('description'); |
| 129 | + expect(permissionSetDefinitionFromRow({ name: 'n' })).not.toHaveProperty('description'); |
| 130 | + expect(permissionSetDefinitionFromRow({ name: 'n', description: '' }).description).toBe(''); |
| 131 | + }); |
| 132 | + |
| 133 | + it('the admin scope travels only when the column is set and reads to a value', () => { |
| 134 | + for (const stored of [undefined, null, '', 'null', 'false', '0', '{not json']) { |
| 135 | + expect(permissionSetDefinitionFromRow({ name: 'n', admin_scope: stored }), String(stored)).not.toHaveProperty('adminScope'); |
| 136 | + } |
| 137 | + expect(permissionSetDefinitionFromRow({ name: 'n', admin_scope: '{"businessUnit":"emea"}' }).adminScope).toEqual({ businessUnit: 'emea' }); |
| 138 | + }); |
| 139 | + |
| 140 | + it('a null or absent row answers the body of a row with every column absent', () => { |
| 141 | + const empty = { |
| 142 | + name: undefined, |
| 143 | + label: undefined, |
| 144 | + objects: {}, |
| 145 | + fields: {}, |
| 146 | + systemPermissions: [], |
| 147 | + rowLevelSecurity: [], |
| 148 | + tabPermissions: {}, |
| 149 | + }; |
| 150 | + expect(permissionSetDefinitionFromRow(null)).toStrictEqual(empty); |
| 151 | + expect(permissionSetDefinitionFromRow(undefined)).toStrictEqual(empty); |
| 152 | + }); |
| 153 | + |
| 154 | + it('is pure: the row is read, never written, and the same row answers the same body', () => { |
| 155 | + const row = Object.freeze({ ...storedRow }); |
| 156 | + const first = permissionSetDefinitionFromRow(row); |
| 157 | + expect(permissionSetDefinitionFromRow(row)).toStrictEqual(first); |
| 158 | + expect(row).toStrictEqual(storedRow); |
| 159 | + }); |
| 160 | +}); |
0 commit comments