@@ -14,6 +14,7 @@ const {
1414 mockUpdateTableLocks,
1515 mockFindActiveFolder,
1616 mockGetLimits,
17+ mockGetUserEntityPermissions,
1718} = vi . hoisted ( ( ) => ( {
1819 mockCheckAccess : vi . fn ( ) ,
1920 mockDeleteTable : vi . fn ( ) ,
@@ -23,6 +24,7 @@ const {
2324 mockUpdateTableLocks : vi . fn ( ) ,
2425 mockFindActiveFolder : vi . fn ( ) ,
2526 mockGetLimits : vi . fn ( ) ,
27+ mockGetUserEntityPermissions : vi . fn ( ) ,
2628} ) )
2729
2830vi . mock ( '@/lib/table' , ( ) => ( {
@@ -39,16 +41,16 @@ vi.mock('@/lib/core/config/feature-flags', () => ({ isFeatureEnabled: vi.fn() })
3941vi . mock ( '@/lib/posthog/server' , ( ) => ( { captureServerEvent : vi . fn ( ) } ) )
4042vi . mock ( '@/lib/workspaces/permissions/utils' , ( ) => ( {
4143 getWorkspaceWithOwner : vi . fn ( ) ,
42- getUserEntityPermissions : vi . fn ( ) ,
44+ getUserEntityPermissions : mockGetUserEntityPermissions ,
4345} ) )
4446vi . mock ( '@/app/api/table/utils' , ( ) => ( {
45- accessError : ( ) => new Response ( 'denied' , { status : 403 } ) ,
47+ accessError : ( result : { status : number } ) => new Response ( 'denied' , { status : result . status } ) ,
4648 checkAccess : mockCheckAccess ,
4749 normalizeColumn : ( column : unknown ) => column ,
4850 tableLockErrorResponse : ( ) => null ,
4951} ) )
5052
51- import { PATCH } from '@/app/api/table/[tableId]/route'
53+ import { DELETE , GET , PATCH } from '@/app/api/table/[tableId]/route'
5254
5355const TABLE = {
5456 id : 'tbl_1' ,
@@ -74,6 +76,74 @@ function patchRequest(body: unknown): NextRequest {
7476
7577const routeContext = { params : Promise . resolve ( { tableId : 'tbl_1' } ) }
7678
79+ describe ( 'GET /api/table/[tableId] Memory table' , ( ) => {
80+ beforeEach ( ( ) => {
81+ vi . clearAllMocks ( )
82+ hybridAuthMockFns . mockCheckSessionOrInternalAuth . mockResolvedValue ( {
83+ success : true ,
84+ userId : 'user-1' ,
85+ authType : 'session' ,
86+ } )
87+ mockGetUserEntityPermissions . mockResolvedValue ( 'read' )
88+ mockCheckAccess . mockResolvedValue ( {
89+ ok : true ,
90+ table : {
91+ ...TABLE ,
92+ id : 'system_memory_workspace-1' ,
93+ name : 'Memory' ,
94+ isVirtual : true ,
95+ rowCount : 1 ,
96+ maxRows : Number . MAX_SAFE_INTEGER ,
97+ createdBy : 'user-1' ,
98+ createdAt : new Date ( '2026-01-01T00:00:00.000Z' ) ,
99+ updatedAt : new Date ( '2026-01-02T00:00:00.000Z' ) ,
100+ locks : {
101+ schemaLocked : true ,
102+ insertLocked : true ,
103+ updateLocked : true ,
104+ deleteLocked : true ,
105+ } ,
106+ } ,
107+ } )
108+ mockGetLimits . mockResolvedValue ( { maxRowsPerTable : 10_000 } )
109+ } )
110+
111+ it ( 'returns the synthetic table to a workspace reader' , async ( ) => {
112+ const response = await GET (
113+ new NextRequest (
114+ 'http://localhost:3000/api/table/system_memory_workspace-1?workspaceId=workspace-1'
115+ ) ,
116+ { params : Promise . resolve ( { tableId : 'system_memory_workspace-1' } ) }
117+ )
118+ const json = await response . json ( )
119+
120+ expect ( response . status ) . toBe ( 200 )
121+ expect ( json . data . table ) . toMatchObject ( {
122+ id : 'system_memory_workspace-1' ,
123+ name : 'Memory' ,
124+ isVirtual : true ,
125+ rowCount : 1 ,
126+ maxRows : 10_000 ,
127+ } )
128+ expect ( mockCheckAccess ) . toHaveBeenCalledWith ( 'system_memory_workspace-1' , 'user-1' , 'read' )
129+ expect ( mockGetLimits ) . toHaveBeenCalledWith ( 'workspace-1' )
130+ } )
131+
132+ it ( 'does not expose the table to someone outside the workspace' , async ( ) => {
133+ mockCheckAccess . mockResolvedValue ( { ok : false , status : 403 } )
134+
135+ const response = await GET (
136+ new NextRequest (
137+ 'http://localhost:3000/api/table/system_memory_workspace-1?workspaceId=workspace-1'
138+ ) ,
139+ { params : Promise . resolve ( { tableId : 'system_memory_workspace-1' } ) }
140+ )
141+
142+ expect ( response . status ) . toBe ( 403 )
143+ expect ( mockGetLimits ) . not . toHaveBeenCalled ( )
144+ } )
145+ } )
146+
77147describe ( 'PATCH /api/table/[tableId] folder moves' , ( ) => {
78148 beforeEach ( ( ) => {
79149 vi . clearAllMocks ( )
@@ -149,4 +219,48 @@ describe('PATCH /api/table/[tableId] folder moves', () => {
149219 expect ( mockMoveTableToFolder ) . not . toHaveBeenCalled ( )
150220 expect ( mockRenameTable ) . not . toHaveBeenCalled ( )
151221 } )
222+
223+ it ( 'rejects synthetic Memory table writes with a read-only explanation' , async ( ) => {
224+ mockCheckAccess . mockResolvedValue ( { ok : false , status : 423 } )
225+
226+ const response = await PATCH (
227+ new NextRequest ( 'http://localhost:3000/api/table/system_memory_workspace-1' , {
228+ method : 'PATCH' ,
229+ headers : { 'content-type' : 'application/json' } ,
230+ body : JSON . stringify ( { workspaceId : 'workspace-1' , name : 'Renamed' } ) ,
231+ } ) ,
232+ { params : Promise . resolve ( { tableId : 'system_memory_workspace-1' } ) }
233+ )
234+
235+ expect ( response . status ) . toBe ( 423 )
236+ expect ( mockCheckAccess ) . toHaveBeenCalledWith ( 'system_memory_workspace-1' , 'user-1' , 'write' )
237+ expect ( mockRenameTable ) . not . toHaveBeenCalled ( )
238+ expect ( mockUpdateTableLocks ) . not . toHaveBeenCalled ( )
239+ } )
240+ } )
241+
242+ describe ( 'DELETE /api/table/[tableId] Memory table' , ( ) => {
243+ beforeEach ( ( ) => {
244+ vi . clearAllMocks ( )
245+ hybridAuthMockFns . mockCheckSessionOrInternalAuth . mockResolvedValue ( {
246+ success : true ,
247+ userId : 'user-1' ,
248+ authType : 'session' ,
249+ } )
250+ } )
251+
252+ it ( 'rejects deletion through shared access' , async ( ) => {
253+ mockCheckAccess . mockResolvedValue ( { ok : false , status : 423 } )
254+ const tableId = 'system_memory_workspace-1'
255+ const response = await DELETE (
256+ new NextRequest ( `http://localhost:3000/api/table/${ tableId } ?workspaceId=workspace-1` , {
257+ method : 'DELETE' ,
258+ } ) ,
259+ { params : Promise . resolve ( { tableId } ) }
260+ )
261+
262+ expect ( response . status ) . toBe ( 423 )
263+ expect ( mockCheckAccess ) . toHaveBeenCalledWith ( tableId , 'user-1' , 'write' )
264+ expect ( mockDeleteTable ) . not . toHaveBeenCalled ( )
265+ } )
152266} )
0 commit comments