PolicyTableRow.utils.test.ts184 lines · main
1import { describe, expect, it } from 'vitest'
2
3import { getTableAdmonitionMessage, getTableDataApiStatus } from './PolicyTableRow.utils'
4import type { TableApiAccessData } from '@/data/privileges/table-api-access-query'
5import type { ApiPrivilegesByRole } from '@/lib/data-api-types'
6
7const FULL_PRIVILEGES: ApiPrivilegesByRole = {
8 anon: ['SELECT', 'INSERT', 'UPDATE', 'DELETE'],
9 authenticated: ['SELECT', 'INSERT', 'UPDATE', 'DELETE'],
10 service_role: ['SELECT', 'INSERT', 'UPDATE', 'DELETE'],
11}
12
13const PARTIAL_PRIVILEGES: ApiPrivilegesByRole = {
14 anon: ['SELECT'],
15 authenticated: [],
16 service_role: ['SELECT', 'INSERT', 'UPDATE', 'DELETE'],
17}
18
19const grantedAccess: TableApiAccessData = {
20 apiAccessType: 'access',
21 grantStatus: 'granted',
22 privileges: FULL_PRIVILEGES,
23}
24
25const customAccess: TableApiAccessData = {
26 apiAccessType: 'access',
27 grantStatus: 'custom',
28 privileges: PARTIAL_PRIVILEGES,
29}
30
31const noGrants: TableApiAccessData = { apiAccessType: 'exposed-schema-no-grants' }
32const schemaNotExposedData: TableApiAccessData = { apiAccessType: 'none' }
33
34describe('getTableDataApiStatus', () => {
35 it('returns schema-not-exposed when the schema is not in the exposed list', () => {
36 const status = getTableDataApiStatus({
37 isSchemaExposed: false,
38 apiAccessData: grantedAccess,
39 isRLSEnabled: true,
40 policiesCount: 1,
41 })
42 expect(status).toBe('schema-not-exposed')
43 })
44
45 it('returns no-grants when schema is exposed but no API roles have privileges', () => {
46 const status = getTableDataApiStatus({
47 isSchemaExposed: true,
48 apiAccessData: noGrants,
49 isRLSEnabled: true,
50 policiesCount: 0,
51 })
52 expect(status).toBe('no-grants')
53 })
54
55 it('returns custom-grants for partial/non-standard grants — even if RLS is off', () => {
56 const status = getTableDataApiStatus({
57 isSchemaExposed: true,
58 apiAccessData: customAccess,
59 isRLSEnabled: false,
60 policiesCount: 0,
61 })
62 expect(status).toBe('custom-grants')
63 })
64
65 it('returns publicly-readable when fully granted and RLS is off', () => {
66 const status = getTableDataApiStatus({
67 isSchemaExposed: true,
68 apiAccessData: grantedAccess,
69 isRLSEnabled: false,
70 policiesCount: 3,
71 })
72 expect(status).toBe('publicly-readable')
73 })
74
75 it('returns locked-by-rls when fully granted + RLS on + no policies', () => {
76 const status = getTableDataApiStatus({
77 isSchemaExposed: true,
78 apiAccessData: grantedAccess,
79 isRLSEnabled: true,
80 policiesCount: 0,
81 })
82 expect(status).toBe('locked-by-rls')
83 })
84
85 it('returns secured when fully granted + RLS on + policies exist', () => {
86 const status = getTableDataApiStatus({
87 isSchemaExposed: true,
88 apiAccessData: grantedAccess,
89 isRLSEnabled: true,
90 policiesCount: 2,
91 })
92 expect(status).toBe('secured')
93 })
94
95 it('returns unknown when apiAccessData is still loading or errored', () => {
96 // apiAccessData is undefined during loading AND on query error (isPending flips false
97 // but data stays undefined). We must not fall through to 'schema-not-exposed' — that
98 // would tell the user to reconfigure API settings for a schema that is in fact exposed.
99 const status = getTableDataApiStatus({
100 isSchemaExposed: true,
101 apiAccessData: undefined,
102 isRLSEnabled: true,
103 policiesCount: 0,
104 })
105 expect(status).toBe('unknown')
106 })
107
108 it('returns unknown when apiAccessData reports apiAccessType=none on an exposed schema', () => {
109 // Defensive: the query shouldn't emit apiAccessType=none when schema is exposed,
110 // but if it does we still don't want the false "schema not exposed" admonition.
111 const status = getTableDataApiStatus({
112 isSchemaExposed: true,
113 apiAccessData: schemaNotExposedData,
114 isRLSEnabled: true,
115 policiesCount: 0,
116 })
117 expect(status).toBe('unknown')
118 })
119
120 it('isSchemaExposed=false wins over any apiAccessData value', () => {
121 const status = getTableDataApiStatus({
122 isSchemaExposed: false,
123 apiAccessData: noGrants,
124 isRLSEnabled: true,
125 policiesCount: 0,
126 })
127 expect(status).toBe('schema-not-exposed')
128 })
129
130 it('custom-grants wins over RLS state — we never claim public-readable for partial grants', () => {
131 const rlsOff = getTableDataApiStatus({
132 isSchemaExposed: true,
133 apiAccessData: customAccess,
134 isRLSEnabled: false,
135 policiesCount: 0,
136 })
137 const rlsOnNoPolicies = getTableDataApiStatus({
138 isSchemaExposed: true,
139 apiAccessData: customAccess,
140 isRLSEnabled: true,
141 policiesCount: 0,
142 })
143 expect(rlsOff).toBe('custom-grants')
144 expect(rlsOnNoPolicies).toBe('custom-grants')
145 })
146})
147
148describe('getTableAdmonitionMessage', () => {
149 it('returns the custom-grants copy', () => {
150 expect(getTableAdmonitionMessage('custom-grants')).toBe(
151 'This table has custom Data API permissions — access may be restricted for some roles or operations.'
152 )
153 })
154
155 it('returns the no-grants copy', () => {
156 expect(getTableAdmonitionMessage('no-grants')).toBe(
157 'This table cannot be accessed via the Data API. Enable access in your project’s Data API settings.'
158 )
159 })
160
161 it('returns the publicly-readable copy', () => {
162 expect(getTableAdmonitionMessage('publicly-readable')).toBe(
163 'This table can be accessed by anyone via the Data API as RLS is disabled.'
164 )
165 })
166
167 it('returns the locked-by-rls copy', () => {
168 expect(getTableAdmonitionMessage('locked-by-rls')).toBe(
169 'No data will be returned via the Data API as no RLS policies exist on this table.'
170 )
171 })
172
173 it('returns null for secured — no admonition needed', () => {
174 expect(getTableAdmonitionMessage('secured')).toBeNull()
175 })
176
177 it('returns null for schema-not-exposed — handled by a separate admonition with a link', () => {
178 expect(getTableAdmonitionMessage('schema-not-exposed')).toBeNull()
179 })
180
181 it('returns null for unknown — caller should stay silent during loading/errored state', () => {
182 expect(getTableAdmonitionMessage('unknown')).toBeNull()
183 })
184})