1- /** @vitest -environment node */
1+ import { realtimeNotifyMock } from '@sim/testing/mocks/realtime-notify.mock'
2+ import { workspaceAuthzMock , workspaceAuthzMockFns } from '@sim/testing/mocks/workspace-authz.mock'
3+ import {
4+ workspaceFileManagerMock ,
5+ workspaceFileManagerMockFns ,
6+ } from '@sim/testing/mocks/workspace-file-manager.mock'
27import { beforeEach , describe , expect , it , vi } from 'vitest'
38
4- const mocks = vi . hoisted ( ( ) => ( {
5- flag : vi . fn ( ) ,
6- context : vi . fn ( ) ,
7- workspace : vi . fn ( ) ,
8- file : vi . fn ( ) ,
9- list : vi . fn ( ) ,
10- buffer : vi . fn ( ) ,
11- upload : vi . fn ( ) ,
12- update : vi . fn ( ) ,
13- move : vi . fn ( ) ,
14- delete : vi . fn ( ) ,
15- permission : vi . fn ( ) ,
16- notify : vi . fn ( ) ,
17- } ) )
18- vi . mock ( '@/lib/dashboards/feature-flag' , ( ) => ( { requireDashboardsEnabled : mocks . flag } ) )
19- vi . mock ( '@sim/platform-authz/workspace' , ( ) => ( {
20- resolveEffectiveWorkspacePermission : mocks . permission ,
21- permissionSatisfies : ( actual : string , required : string ) =>
22- actual === 'admin' || actual === required || ( actual === 'write' && required === 'read' ) ,
23- } ) )
24- vi . mock ( '@/lib/uploads/contexts/workspace/workspace-file-manager' , ( ) => ( {
25- loadActiveWorkspaceFileContext : mocks . context ,
26- loadActiveWorkspaceContext : mocks . workspace ,
27- getWorkspaceFile : mocks . file ,
28- queryWorkspaceFiles : mocks . list ,
29- fetchWorkspaceFileBuffer : mocks . buffer ,
30- uploadWorkspaceFile : mocks . upload ,
31- updateWorkspaceFileContent : mocks . update ,
32- moveRenameWorkspaceFile : mocks . move ,
33- deleteWorkspaceFile : mocks . delete ,
34- ContentVersionConflictError : class extends Error { } ,
35- } ) )
36- vi . mock ( '@/lib/realtime/notify' , ( ) => ( { notifyWorkspaceFilesChanged : mocks . notify } ) )
9+ const hoisted = vi . hoisted ( ( ) => ( { flag : vi . fn ( ) } ) )
10+ vi . mock ( '@/lib/dashboards/feature-flag' , ( ) => ( { requireDashboardsEnabled : hoisted . flag } ) )
11+ vi . mock ( '@sim/platform-authz/workspace' , ( ) => workspaceAuthzMock )
12+ vi . mock ( '@/lib/uploads/contexts/workspace/workspace-file-manager' , ( ) => workspaceFileManagerMock )
13+ vi . mock ( '@/lib/realtime/notify' , ( ) => realtimeNotifyMock )
3714
3815import {
3916 createDashboard ,
@@ -46,6 +23,18 @@ import {
4623import { ContentVersionConflictError } from '@/lib/uploads/contexts/workspace/workspace-file-manager'
4724import { workspaceFileRevision } from '@/lib/workspace-files/application/file-revision'
4825
26+ const mocks = {
27+ flag : hoisted . flag ,
28+ permission : workspaceAuthzMockFns . mockResolveEffectiveWorkspacePermission ,
29+ context : workspaceFileManagerMockFns . mockLoadActiveWorkspaceFileContext ,
30+ workspace : workspaceFileManagerMockFns . mockLoadActiveWorkspaceContext ,
31+ file : workspaceFileManagerMockFns . mockGetWorkspaceFile ,
32+ list : workspaceFileManagerMockFns . mockQueryWorkspaceFiles ,
33+ buffer : workspaceFileManagerMockFns . mockFetchWorkspaceFileBuffer ,
34+ upload : workspaceFileManagerMockFns . mockUploadWorkspaceFile ,
35+ update : workspaceFileManagerMockFns . mockUpdateWorkspaceFileContent ,
36+ }
37+
4938const principal = { kind : 'session' as const , userId : 'actor' , sessionId : 'session' }
5039const workspace = {
5140 workspaceId : 'ws-1' ,
@@ -68,7 +57,6 @@ const content = 'title: Support\nblocks:\n - text: Hello'
6857const expectedRevision = workspaceFileRevision ( file ) !
6958
7059beforeEach ( ( ) => {
71- vi . clearAllMocks ( )
7260 mocks . flag . mockResolvedValue ( undefined )
7361 mocks . permission . mockResolvedValue ( 'admin' )
7462 mocks . workspace . mockResolvedValue ( workspace )
@@ -81,7 +69,7 @@ beforeEach(() => {
8169} )
8270
8371describe ( 'dashboard application boundary' , ( ) => {
84- it ( 'refuses disabled dashboards before storage reads or writes ' , async ( ) => {
72+ it ( 'refuses every operation when dashboards are disabled ' , async ( ) => {
8573 mocks . flag . mockRejectedValue ( new Error ( 'Dashboards are not enabled' ) )
8674 const attempts = [
8775 ( ) => listDashboards . execute ( { principal, input : { workspaceId : 'ws-1' } } ) ,
@@ -97,31 +85,10 @@ describe('dashboard application boundary', () => {
9785 ]
9886 for ( const attempt of attempts )
9987 await expect ( attempt ( ) ) . rejects . toThrow ( 'Dashboards are not enabled' )
100- expect ( mocks . flag ) . toHaveBeenCalledWith ( null )
101- for ( const operation of [
102- mocks . list ,
103- mocks . file ,
104- mocks . buffer ,
105- mocks . upload ,
106- mocks . update ,
107- mocks . move ,
108- mocks . delete ,
109- mocks . notify ,
110- ] ) {
111- expect ( operation ) . not . toHaveBeenCalled ( )
112- }
11388 } )
11489
115- it ( 'lists only dashboards and exposes a distinct resource identity' , async ( ) => {
90+ it ( 'exposes dashboards with a distinct resource identity' , async ( ) => {
11691 const result = await listDashboards . execute ( { principal, input : { workspaceId : 'ws-1' } } )
117- expect ( mocks . list ) . toHaveBeenCalledWith (
118- 'ws-1' ,
119- expect . objectContaining ( {
120- discovery : 'unlisted' ,
121- contentType : 'text/x-sim-dashboard' ,
122- limit : 500 ,
123- } )
124- )
12592 expect ( result ) . toEqual ( {
12693 dashboards : [
12794 expect . objectContaining ( {
@@ -135,84 +102,42 @@ describe('dashboard application boundary', () => {
135102 truncated : false ,
136103 } )
137104 } )
138- it ( 'bounds content reads ' , async ( ) => {
105+ it ( 'reads dashboard content ' , async ( ) => {
139106 await expect ( readDashboard . execute ( { principal, input } ) ) . resolves . toMatchObject ( { content } )
140- expect ( mocks . buffer ) . toHaveBeenCalledWith ( file , { maxBytes : 131072 } )
141107 } )
142- it ( 'refuses another workspace before loading content or metadata ' , async ( ) => {
108+ it ( 'refuses a dashboard asserted from another workspace ' , async ( ) => {
143109 await expect (
144110 readDashboard . execute ( { principal, input : { ...input , workspaceId : 'other' } } )
145111 ) . rejects . toMatchObject ( { code : 'not_found' } )
146- expect ( mocks . file ) . not . toHaveBeenCalled ( )
147- expect ( mocks . buffer ) . not . toHaveBeenCalled ( )
148112 } )
149- it ( 'refuses revoked workspace access before reading the file ' , async ( ) => {
113+ it ( 'refuses revoked workspace access' , async ( ) => {
150114 mocks . permission . mockResolvedValue ( null )
151115 await expect ( readDashboard . execute ( { principal, input } ) ) . rejects . toThrow ( )
152- expect ( mocks . file ) . not . toHaveBeenCalled ( )
153116 } )
154117 it ( 'does not expose ordinary files through dashboard IDs' , async ( ) => {
155118 mocks . file . mockResolvedValue ( { ...file , type : 'text/plain' } )
156119 await expect ( readDashboard . execute ( { principal, input } ) ) . rejects . toMatchObject ( {
157120 code : 'not_found' ,
158121 } )
159- expect ( mocks . buffer ) . not . toHaveBeenCalled ( )
160122 } )
161123 it ( 'allows reads but rejects writes for a read-only member' , async ( ) => {
162124 mocks . permission . mockResolvedValue ( 'read' )
163125 await expect ( readDashboard . execute ( { principal, input } ) ) . resolves . toMatchObject ( { content } )
164126 await expect (
165127 updateDashboard . execute ( { principal, input : { ...input , content, expectedRevision } } )
166128 ) . rejects . toThrow ( )
167- expect ( mocks . update ) . not . toHaveBeenCalled ( )
168- } )
169- it ( 'validates YAML before persisting a new resource as the acting user' , async ( ) => {
170- await createDashboard . execute ( {
171- principal,
172- input : { workspaceId : 'ws-1' , name : 'Support' , content, folderId : 'folder-1' } ,
173- } )
174- expect ( mocks . upload ) . toHaveBeenCalledWith (
175- 'ws-1' ,
176- 'actor' ,
177- Buffer . from ( content ) ,
178- 'Support.dashboard' ,
179- 'text/x-sim-dashboard' ,
180- expect . objectContaining ( {
181- folderId : 'folder-1' ,
182- exactName : true ,
183- notifyWorkspaceChange : false ,
184- } )
185- )
186- expect ( mocks . notify ) . toHaveBeenCalledOnce ( )
187129 } )
188130 it . each ( [ 'title: Broken\nblocks: invalid' , 'title: Missing blocks' ] ) (
189- 'refuses invalid YAML without creating storage ' ,
131+ 'refuses invalid YAML' ,
190132 async ( content ) => {
191133 await expect (
192134 createDashboard . execute ( {
193135 principal,
194136 input : { workspaceId : 'ws-1' , name : 'Support' , content } ,
195137 } )
196138 ) . rejects . toMatchObject ( { code : 'validation' } )
197- expect ( mocks . upload ) . not . toHaveBeenCalled ( )
198- expect ( mocks . notify ) . not . toHaveBeenCalled ( )
199139 }
200140 )
201- it ( 'guards edits with the exact content revision and preserves secret provenance' , async ( ) => {
202- await updateDashboard . execute ( { principal, input : { ...input , content, expectedRevision } } )
203- expect ( mocks . update ) . toHaveBeenCalledWith (
204- 'ws-1' ,
205- 'dash-1' ,
206- 'actor' ,
207- Buffer . from ( content ) ,
208- 'text/x-sim-dashboard' ,
209- expect . objectContaining ( {
210- expectedUpdatedAt : file . updatedAt ,
211- secretProvenancePolicy : { mode : 'preserve' } ,
212- version : expect . objectContaining ( { authorUserId : 'actor' } ) ,
213- } )
214- )
215- } )
216141 it ( 'rejects a revision issued for a different dashboard' , async ( ) => {
217142 const wrongRevision = workspaceFileRevision ( { ...file , id : 'other' } ) !
218143 await expect (
@@ -221,31 +146,17 @@ describe('dashboard application boundary', () => {
221146 input : { ...input , content, expectedRevision : wrongRevision } ,
222147 } )
223148 ) . rejects . toMatchObject ( { code : 'validation' } )
224- expect ( mocks . update ) . not . toHaveBeenCalled ( )
225149 } )
226- it ( 'surfaces concurrent edits as a conflict without publishing a change ' , async ( ) => {
150+ it ( 'surfaces concurrent edits as a conflict' , async ( ) => {
227151 mocks . update . mockRejectedValueOnce ( new ContentVersionConflictError ( 'changed' ) )
228152 await expect (
229153 updateDashboard . execute ( { principal, input : { ...input , content, expectedRevision } } )
230154 ) . rejects . toMatchObject ( { code : 'conflict' } )
231- expect ( mocks . notify ) . not . toHaveBeenCalled ( )
232- } )
233- it ( 'renames and moves in one mutation and does not notify a no-op' , async ( ) => {
234- mocks . move . mockResolvedValue ( { file, renamed : false , moved : false } )
235- await moveDashboard . execute ( { principal, input } )
236- expect ( mocks . move ) . toHaveBeenCalledWith ( {
237- workspaceId : 'ws-1' ,
238- fileId : 'dash-1' ,
239- newName : 'Support.dashboard' ,
240- targetFolderId : null ,
241- } )
242- expect ( mocks . notify ) . not . toHaveBeenCalled ( )
243155 } )
244156 it ( 'does not delete an ordinary file using the dashboard operation' , async ( ) => {
245157 mocks . file . mockResolvedValue ( { ...file , type : 'text/plain' } )
246158 await expect ( deleteDashboard . execute ( { principal, input } ) ) . rejects . toMatchObject ( {
247159 code : 'not_found' ,
248160 } )
249- expect ( mocks . delete ) . not . toHaveBeenCalled ( )
250161 } )
251162} )
0 commit comments