@@ -800,71 +800,83 @@ describe('Public file shares against PostgreSQL and private storage', () => {
800800 '@/lib/public-shares/application'
801801 )
802802 const { encryptSecret } = await import ( '@/lib/core/security/encryption' )
803- for ( const change of [ 'disabled' , 'password' , 'archived' , 'provenance' ] as const ) {
804- const f = await fixture ( )
805- const { file } = await createProjectFile . execute ( {
806- principal : f . principal ,
807- input : createInput ( f . projectId ) ,
808- } )
809- const { share } = await updateProjectFileShare . execute ( {
810- principal : f . principal ,
811- input : {
812- projectId : f . projectId ,
813- fileId : file . id ,
814- isActive : true ,
815- authType : 'password' ,
816- password : 'initial-password' ,
817- } ,
818- } )
819- const access = await authorizePublicFileShare ( {
820- token : share . token ,
821- credential : { method : 'POST' , password : 'initial-password' } ,
822- } )
823- if ( ! access . authorized ) throw new Error ( 'Initial credential rejected' )
824- const applyChange = async ( ) => {
825- if ( change === 'disabled' )
826- await db
827- . update ( publicShare )
828- . set ( { isActive : false } )
829- . where ( eq ( publicShare . id , share . id ) )
830- if ( change === 'password' )
831- await db
832- . update ( publicShare )
833- . set ( { password : ( await encryptSecret ( 'replacement-password' ) ) . encrypted } )
834- . where ( eq ( publicShare . id , share . id ) )
835- if ( change === 'provenance' )
836- await db
837- . update ( workspaceFileSecretProvenance )
838- . set ( { status : 'unknown' } )
839- . where ( eq ( workspaceFileSecretProvenance . fileId , file . id ) )
840- if ( change === 'archived' )
841- await db
842- . update ( workspaceFiles )
843- . set ( { deletedAt : new Date ( ) } )
844- . where ( eq ( workspaceFiles . id , file . id ) )
845- }
846- // The change lands while the object is being read, buffered or streamed alike.
847- const download = storage . downloadFile
848- const downloadStream = storage . downloadFileStream
849- const spies = [
850- vi . spyOn ( storage , 'downloadFile' ) . mockImplementation ( async ( options ) => {
851- const result = await download ( options )
852- if ( options . key === file . key ) await applyChange ( )
853- return result
854- } ) ,
855- vi . spyOn ( storage , 'downloadFileStream' ) . mockImplementation ( async ( options ) => {
856- const result = await downloadStream ( options )
857- if ( options . key === file . key ) await applyChange ( )
858- return result
859- } ) ,
860- ]
861- await expect (
862- sharedBytes ( readPublicFileShareContent ( { grant : access . grant } ) )
863- ) . rejects . toMatchObject ( {
864- code : change === 'provenance' ? 'forbidden' : 'not_found' ,
865- } )
866- for ( const spy of spies ) spy . mockRestore ( )
803+ // A Markdown file streams from storage; a page is rendered whole. Both reads must refuse.
804+ const shared = {
805+ streamed : ( projectId : string ) => createInput ( projectId ) ,
806+ rendered : ( projectId : string ) => ( {
807+ ...createInput ( projectId , '---\ntitle: Architecture\n---\n\nShared page' ) ,
808+ name : 'Architecture' ,
809+ contentType : 'text/x-sim-page' ,
810+ } ) ,
867811 }
812+ for ( const [ read , input ] of Object . entries ( shared ) )
813+ for ( const change of [ 'disabled' , 'password' , 'archived' , 'provenance' ] as const ) {
814+ const f = await fixture ( )
815+ const { file } = await createProjectFile . execute ( {
816+ principal : f . principal ,
817+ input : input ( f . projectId ) ,
818+ } )
819+ const { share } = await updateProjectFileShare . execute ( {
820+ principal : f . principal ,
821+ input : {
822+ projectId : f . projectId ,
823+ fileId : file . id ,
824+ isActive : true ,
825+ authType : 'password' ,
826+ password : 'initial-password' ,
827+ } ,
828+ } )
829+ const access = await authorizePublicFileShare ( {
830+ token : share . token ,
831+ credential : { method : 'POST' , password : 'initial-password' } ,
832+ } )
833+ if ( ! access . authorized ) throw new Error ( 'Initial credential rejected' )
834+ const applyChange = async ( ) => {
835+ if ( change === 'disabled' )
836+ await db
837+ . update ( publicShare )
838+ . set ( { isActive : false } )
839+ . where ( eq ( publicShare . id , share . id ) )
840+ if ( change === 'password' )
841+ await db
842+ . update ( publicShare )
843+ . set ( { password : ( await encryptSecret ( 'replacement-password' ) ) . encrypted } )
844+ . where ( eq ( publicShare . id , share . id ) )
845+ if ( change === 'provenance' )
846+ await db
847+ . update ( workspaceFileSecretProvenance )
848+ . set ( { status : 'unknown' } )
849+ . where ( eq ( workspaceFileSecretProvenance . fileId , file . id ) )
850+ if ( change === 'archived' )
851+ await db
852+ . update ( workspaceFiles )
853+ . set ( { deletedAt : new Date ( ) } )
854+ . where ( eq ( workspaceFiles . id , file . id ) )
855+ }
856+ // The change lands while the object is being read, buffered or streamed alike.
857+ const download = storage . downloadFile
858+ const downloadStream = storage . downloadFileStream
859+ const spies = [
860+ vi . spyOn ( storage , 'downloadFile' ) . mockImplementation ( async ( options ) => {
861+ const result = await download ( options )
862+ if ( options . key === file . key ) await applyChange ( )
863+ return result
864+ } ) ,
865+ vi . spyOn ( storage , 'downloadFileStream' ) . mockImplementation ( async ( options ) => {
866+ const result = await downloadStream ( options )
867+ if ( options . key === file . key ) await applyChange ( )
868+ return result
869+ } ) ,
870+ ]
871+ const delivered = readPublicFileShareContent ( { grant : access . grant } )
872+ await expect ( sharedBytes ( delivered ) ) . rejects . toMatchObject ( {
873+ code : change === 'provenance' ? 'forbidden' : 'not_found' ,
874+ } )
875+ // Each fixture took the path it names: the stream refuses on open, the page as it renders.
876+ if ( read === 'streamed' ) expect ( await delivered ) . toMatchObject ( { kind : 'stream' } )
877+ else await expect ( delivered ) . rejects . toBeDefined ( )
878+ for ( const spy of spies ) spy . mockRestore ( )
879+ }
868880 }
869881 )
870882
0 commit comments