@@ -6,7 +6,6 @@ import { createRoot, type Root } from 'react-dom/client'
66import { afterEach , beforeEach , describe , expect , it , vi } from 'vitest'
77import { type TableLocks , UNLOCKED_TABLE_LOCKS } from '@/lib/table/types'
88import { LockSettingsModal } from '@/app/workspace/[workspaceId]/tables/[tableId]/components/lock-settings-modal/lock-settings-modal'
9- import { useTableSecurityStore } from '@/stores/table/security/store'
109
1110const { mutateAsync } = vi . hoisted ( ( ) => ( { mutateAsync : vi . fn ( ) } ) )
1211vi . mock ( '@/hooks/queries/tables' , ( ) => ( {
@@ -32,18 +31,6 @@ function render(locks: TableLocks = UNLOCKED_TABLE_LOCKS, isOpen = true) {
3231 } )
3332}
3433
35- function getSwitch ( label : string ) : HTMLButtonElement {
36- const element = document . querySelector < HTMLButtonElement > (
37- `button[role="switch"][aria-label="${ label } "]`
38- )
39- if ( ! element ) throw new Error ( `Missing switch: ${ label } ` )
40- return element
41- }
42-
43- function clickSwitch ( label : string ) {
44- act ( ( ) => getSwitch ( label ) . click ( ) )
45- }
46-
4734function getPermission ( label : string , choice : 'Deny' | 'Allow' ) : HTMLButtonElement {
4835 const group = document . querySelector ( `[role="radiogroup"][aria-label="${ label } "]` )
4936 const button = [
@@ -57,19 +44,22 @@ function selectPermission(label: string, choice: 'Deny' | 'Allow') {
5744 act ( ( ) => getPermission ( label , choice ) . click ( ) )
5845}
5946
60- function save ( ) {
47+ function getSave ( ) : HTMLButtonElement {
6148 const button = [ ...document . querySelectorAll < HTMLButtonElement > ( 'button' ) ] . find (
6249 ( element ) => element . textContent === 'Save'
6350 )
6451 if ( ! button ) throw new Error ( 'Missing Save button' )
65- act ( ( ) => button . click ( ) )
52+ return button
53+ }
54+
55+ function save ( ) {
56+ act ( ( ) => getSave ( ) . click ( ) )
6657}
6758
6859beforeEach ( ( ) => {
6960 globalThis . IS_REACT_ACT_ENVIRONMENT = true
7061 vi . clearAllMocks ( )
7162 mutateAsync . mockReturnValue ( new Promise ( ( ) => { } ) )
72- useTableSecurityStore . getState ( ) . reset ( )
7363 container = document . createElement ( 'div' )
7464 document . body . appendChild ( container )
7565 root = createRoot ( container )
@@ -81,75 +71,51 @@ afterEach(() => {
8171} )
8272
8373describe ( 'Table Security' , ( ) => {
84- it ( 'hides permissions while disabled and enables all four backend locks by default ' , ( ) => {
74+ it ( 'always shows the four rows and starts an unconfigured table on Allow ' , ( ) => {
8575 render ( )
86- expect ( getSwitch ( 'Enable Table Security' ) . getAttribute ( 'aria-checked' ) ) . toBe ( 'false' )
87- expect ( document . querySelector ( '[role="radiogroup"]' ) ) . toBeNull ( )
88-
89- clickSwitch ( 'Enable Table Security' )
9076 for ( const label of LABELS ) {
91- expect ( getPermission ( label , 'Deny' ) . disabled ) . toBe ( false )
92- expect ( getPermission ( label , 'Allow' ) . disabled ) . toBe ( false )
93- expect ( getPermission ( label , 'Deny' ) . getAttribute ( 'aria-checked' ) ) . toBe ( 'true' )
94- expect ( getPermission ( label , 'Allow' ) . getAttribute ( 'aria-checked' ) ) . toBe ( 'false' )
77+ expect ( getPermission ( label , 'Allow' ) . getAttribute ( 'aria-checked' ) ) . toBe ( 'true' )
78+ expect ( getPermission ( label , 'Deny' ) . getAttribute ( 'aria-checked' ) ) . toBe ( 'false' )
9579 }
96- save ( )
97-
98- expect ( mutateAsync . mock . calls [ 0 ] [ 0 ] ) . toEqual ( {
99- tableId : 'table-1' ,
100- locks : { insertLocked : true , updateLocked : true , deleteLocked : true , schemaLocked : true } ,
101- } )
80+ // Nothing staged yet, so there is nothing to save.
81+ expect ( getSave ( ) . disabled ) . toBe ( true )
10282 } )
10383
104- it ( 'inverts existing locks and remembers permissions after disabling, saving, and reopening' , async ( ) => {
105- render ( { insertLocked : true , updateLocked : true , deleteLocked : false , schemaLocked : true } )
106- expect ( getSwitch ( 'Enable Table Security' ) . getAttribute ( 'aria-checked' ) ) . toBe ( 'true' )
107- expect ( getPermission ( 'Deleting Rows' , 'Allow' ) . getAttribute ( 'aria-checked' ) ) . toBe ( 'true' )
108- expect ( getPermission ( 'Updating Rows' , 'Deny' ) . getAttribute ( 'aria-checked' ) ) . toBe ( 'true' )
109-
110- selectPermission ( 'Inserting Rows' , 'Allow' )
111- clickSwitch ( 'Enable Table Security' )
112- expect ( document . querySelector ( '[role="radiogroup"]' ) ) . toBeNull ( )
113- let resolveSave ! : ( ) => void
114- mutateAsync . mockReturnValueOnce (
115- new Promise < void > ( ( resolve ) => {
116- resolveSave = resolve
117- } )
84+ it ( 'mirrors the server locks, with Deny meaning a set lock' , ( ) => {
85+ render ( { insertLocked : true , updateLocked : false , deleteLocked : true , schemaLocked : false } )
86+ expect ( getPermission ( 'Inserting Rows' , 'Deny' ) . getAttribute ( 'aria-checked' ) ) . toBe ( 'true' )
87+ expect ( getPermission ( 'Deleting Rows' , 'Deny' ) . getAttribute ( 'aria-checked' ) ) . toBe ( 'true' )
88+ expect ( getPermission ( 'Updating Rows' , 'Allow' ) . getAttribute ( 'aria-checked' ) ) . toBe ( 'true' )
89+ expect ( getPermission ( 'Changing Table Schema' , 'Allow' ) . getAttribute ( 'aria-checked' ) ) . toBe (
90+ 'true'
11891 )
119- save ( )
120- expect ( mutateAsync . mock . calls [ 0 ] [ 0 ] ) . toEqual ( {
121- tableId : 'table-1' ,
122- locks : UNLOCKED_TABLE_LOCKS ,
123- } )
124- await act ( async ( ) => resolveSave ( ) )
92+ } )
12593
126- render ( UNLOCKED_TABLE_LOCKS , false )
94+ it ( 'saves the denied actions as locks' , ( ) => {
12795 render ( )
128- expect ( getSwitch ( 'Enable Table Security' ) . getAttribute ( 'aria-checked' ) ) . toBe ( 'false' )
129- expect ( document . querySelector ( '[role="radiogroup"]' ) ) . toBeNull ( )
130- clickSwitch ( 'Enable Table Security' )
131- expect ( getPermission ( 'Inserting Rows' , 'Allow' ) . getAttribute ( 'aria-checked' ) ) . toBe ( 'true' )
132- expect ( getPermission ( 'Updating Rows' , 'Deny' ) . getAttribute ( 'aria-checked' ) ) . toBe ( 'true' )
96+ selectPermission ( 'Inserting Rows' , 'Deny' )
97+ selectPermission ( 'Changing Table Schema' , 'Deny' )
98+ expect ( getSave ( ) . disabled ) . toBe ( false )
13399 save ( )
134- expect ( mutateAsync . mock . calls [ 1 ] [ 0 ] ) . toEqual ( {
100+
101+ expect ( mutateAsync . mock . calls [ 0 ] [ 0 ] ) . toEqual ( {
135102 tableId : 'table-1' ,
136- locks : { insertLocked : false , updateLocked : true , deleteLocked : false , schemaLocked : true } ,
103+ locks : { insertLocked : true , updateLocked : false , deleteLocked : false , schemaLocked : true } ,
137104 } )
138105 } )
139106
140- it ( 'does not remember unsuccessful changes and discards them on reopen' , ( ) => {
107+ it ( 'keeps the modal open when the save fails and discards the draft on reopen' , async ( ) => {
108+ mutateAsync . mockRejectedValueOnce ( new Error ( 'Admin access required to change table locks' ) )
141109 render ( )
142- clickSwitch ( 'Enable Table Security ')
143- selectPermission ( 'Inserting Rows' , 'Allow' )
144- save ( )
145- expect ( useTableSecurityStore . getState ( ) . preferences [ 'table-1' ] ) . toBeUndefined ( )
110+ selectPermission ( 'Updating Rows' , 'Deny ')
111+ await act ( async ( ) => {
112+ getSave ( ) . click ( )
113+ } )
146114 expect ( onClose ) . not . toHaveBeenCalled ( )
147115
148116 render ( UNLOCKED_TABLE_LOCKS , false )
149117 render ( )
150- expect ( getSwitch ( 'Enable Table Security' ) . getAttribute ( 'aria-checked' ) ) . toBe ( 'false' )
151- expect ( document . querySelector ( '[role="radiogroup"]' ) ) . toBeNull ( )
152- clickSwitch ( 'Enable Table Security' )
153- expect ( getPermission ( 'Inserting Rows' , 'Deny' ) . getAttribute ( 'aria-checked' ) ) . toBe ( 'true' )
118+ expect ( getPermission ( 'Updating Rows' , 'Allow' ) . getAttribute ( 'aria-checked' ) ) . toBe ( 'true' )
119+ expect ( getSave ( ) . disabled ) . toBe ( true )
154120 } )
155121} )
0 commit comments