@@ -104,7 +104,7 @@ describe('CloudManagerClient', () => {
104104 const rmSyncStub = sinon . stub ( ) ;
105105 const statfsSyncStub = sinon . stub ( ) ;
106106 const writeSyncStub = sinon . stub ( ) ;
107- const archiveFolderStub = sinon . stub ( ) . resolves ( ) ;
107+ const archiveFolderStub = sinon . stub ( ) . resolves ( Buffer . from ( 'zip-content' ) ) ;
108108 const extractStub = sinon . stub ( ) . resolves ( ) ;
109109
110110 // esmock's initial module resolution can exceed mocha's default 2s timeout
@@ -151,7 +151,7 @@ describe('CloudManagerClient', () => {
151151 statfsSyncStub . returns ( { bsize : 4096 , blocks : 131072 , bfree : 65536 } ) ;
152152 writeSyncStub . reset ( ) ;
153153 archiveFolderStub . reset ( ) ;
154- archiveFolderStub . resolves ( ) ;
154+ archiveFolderStub . resolves ( Buffer . from ( 'zip-content' ) ) ;
155155 extractStub . reset ( ) ;
156156 extractStub . resolves ( ) ;
157157 createFromStub . reset ( ) ;
@@ -916,37 +916,23 @@ describe('CloudManagerClient', () => {
916916
917917 describe ( 'unzipRepository' , ( ) => {
918918 const expectedExtractPath = `${ path . join ( os . tmpdir ( ) , 'cm-repo-' ) } XXXXXX` ;
919- const expectedZipDir = `${ path . join ( os . tmpdir ( ) , 'cm-zip-' ) } XXXXXX` ;
920- const expectedZipFile = path . join ( expectedZipDir , 'repo.zip' ) ;
921919
922920 it ( 'extracts ZIP buffer to a temp directory' , async ( ) => {
923921 const client = CloudManagerClient . createFrom ( createContext ( ) ) ;
924922 const zipBuffer = Buffer . from ( 'fake-zip-content' ) ;
925923
926924 const extractPath = await client . unzipRepository ( zipBuffer ) ;
927925
928- // Should have created temp dirs for extract (cm-repo-) and zip file (cm-zip-)
929- expect ( mkdtempSyncStub ) . to . have . been . calledTwice ;
926+ // Should create a temp dir for extraction
927+ expect ( mkdtempSyncStub ) . to . have . been . calledOnce ;
930928 expect ( mkdtempSyncStub . firstCall . args [ 0 ] ) . to . match ( / c m - r e p o - $ / ) ;
931- expect ( mkdtempSyncStub . secondCall . args [ 0 ] ) . to . match ( / c m - z i p - $ / ) ;
932929
933- // Should write buffer to temp zip file, then extract
934- expect ( writeSyncStub ) . to . have . been . calledOnce ;
935- expect ( writeSyncStub . firstCall . args [ 0 ] ) . to . equal ( expectedZipFile ) ;
936- expect ( writeSyncStub . firstCall . args [ 1 ] ) . to . equal ( zipBuffer ) ;
930+ // Should pass buffer directly to extract with safeSymlinksOnly
937931 expect ( extractStub ) . to . have . been . calledOnce ;
938- expect ( extractStub . firstCall . args [ 0 ] ) . to . equal ( expectedZipFile ) ;
932+ expect ( extractStub . firstCall . args [ 0 ] ) . to . equal ( zipBuffer ) ;
939933 expect ( extractStub . firstCall . args [ 1 ] ) . to . equal ( expectedExtractPath ) ;
934+ expect ( extractStub . firstCall . args [ 2 ] ) . to . deep . equal ( { safeSymlinksOnly : true } ) ;
940935
941- // Should validate symlinks after extraction
942- expect ( readdirSyncStub ) . to . have . been . calledOnce ;
943- expect ( readdirSyncStub . firstCall . args [ 0 ] ) . to . equal ( expectedExtractPath ) ;
944-
945- // Should clean up the temp zip directory
946- expect ( rmSyncStub ) . to . have . been . calledOnce ;
947- expect ( rmSyncStub . firstCall . args [ 0 ] ) . to . equal ( expectedZipDir ) ;
948-
949- // Should return the extract path
950936 expect ( extractPath ) . to . equal ( expectedExtractPath ) ;
951937 } ) ;
952938
@@ -958,78 +944,10 @@ describe('CloudManagerClient', () => {
958944 await expect ( client . unzipRepository ( zipBuffer ) )
959945 . to . be . rejectedWith ( 'Failed to unzip repository' ) ;
960946
961- // Should have cleaned up both the extraction directory and the temp zip directory
962- expect ( rmSyncStub ) . to . have . been . calledTwice ;
963- const rmPaths = rmSyncStub . getCalls ( ) . map ( ( c ) => c . args [ 0 ] ) ;
964- expect ( rmPaths . some ( ( p ) => p . includes ( 'cm-repo-' ) ) ) . to . be . true ;
965- expect ( rmPaths . some ( ( p ) => p . includes ( 'cm-zip-' ) ) ) . to . be . true ;
966- } ) ;
967-
968- it ( 'cleans up extractPath when second mkdtempSync fails' , async ( ) => {
969- mkdtempSyncStub . onFirstCall ( ) . returns ( expectedExtractPath ) ;
970- mkdtempSyncStub . onSecondCall ( ) . throws ( new Error ( 'ENOSPC: no space left on device' ) ) ;
971-
972- const client = CloudManagerClient . createFrom ( createContext ( ) ) ;
973-
974- await expect ( client . unzipRepository ( Buffer . from ( 'zip' ) ) )
975- . to . be . rejectedWith ( 'Failed to unzip repository: ENOSPC: no space left on device' ) ;
976-
977- // extractPath should be cleaned up even though zipDir was never created
947+ // Should clean up the extraction directory
978948 expect ( rmSyncStub ) . to . have . been . calledOnce ;
979949 expect ( rmSyncStub . firstCall . args [ 0 ] ) . to . equal ( expectedExtractPath ) ;
980950 } ) ;
981-
982- it ( 'rejects when extracted symlink points outside repository root' , async ( ) => {
983- // Simulate a directory with a symlink that escapes the root
984- readdirSyncStub . withArgs ( expectedExtractPath , { withFileTypes : true } ) . returns ( [ {
985- name : 'evil-link' ,
986- isSymbolicLink : ( ) => true ,
987- isDirectory : ( ) => false ,
988- } ] ) ;
989- readlinkSyncStub . returns ( '/etc/shadow' ) ;
990-
991- const client = CloudManagerClient . createFrom ( createContext ( ) ) ;
992-
993- await expect ( client . unzipRepository ( Buffer . from ( 'zip' ) ) )
994- . to . be . rejectedWith ( 'Symlink escapes repository root: evil-link -> /etc/shadow' ) ;
995-
996- // extractPath should be cleaned up
997- const rmPaths = rmSyncStub . getCalls ( ) . map ( ( c ) => c . args [ 0 ] ) ;
998- expect ( rmPaths . some ( ( p ) => p . includes ( 'cm-repo-' ) ) ) . to . be . true ;
999- } ) ;
1000-
1001- it ( 'allows symlinks that point within the repository root' , async ( ) => {
1002- // Simulate dispatcher-style symlinks: enabled_farms/foo.farm -> ../available_farms/foo.farm
1003- readdirSyncStub . withArgs ( expectedExtractPath , { withFileTypes : true } ) . returns ( [ {
1004- name : 'dispatcher' ,
1005- isSymbolicLink : ( ) => false ,
1006- isDirectory : ( ) => true ,
1007- } ] ) ;
1008- const dispatcherPath = path . join ( expectedExtractPath , 'dispatcher' ) ;
1009- readdirSyncStub . withArgs ( dispatcherPath , { withFileTypes : true } ) . returns ( [ {
1010- name : 'enabled_farms' ,
1011- isSymbolicLink : ( ) => false ,
1012- isDirectory : ( ) => true ,
1013- } , {
1014- name : 'available_farms' ,
1015- isSymbolicLink : ( ) => false ,
1016- isDirectory : ( ) => true ,
1017- } ] ) ;
1018- const enabledPath = path . join ( dispatcherPath , 'enabled_farms' ) ;
1019- readdirSyncStub . withArgs ( enabledPath , { withFileTypes : true } ) . returns ( [ {
1020- name : 'default.farm' ,
1021- isSymbolicLink : ( ) => true ,
1022- isDirectory : ( ) => false ,
1023- } ] ) ;
1024- readlinkSyncStub . returns ( '../available_farms/default.farm' ) ;
1025- const availablePath = path . join ( dispatcherPath , 'available_farms' ) ;
1026- readdirSyncStub . withArgs ( availablePath , { withFileTypes : true } ) . returns ( [ ] ) ;
1027-
1028- const client = CloudManagerClient . createFrom ( createContext ( ) ) ;
1029- const result = await client . unzipRepository ( Buffer . from ( 'zip' ) ) ;
1030-
1031- expect ( result ) . to . equal ( expectedExtractPath ) ;
1032- } ) ;
1033951 } ) ;
1034952
1035953 describe ( 'zipRepository' , ( ) => {
@@ -1051,21 +969,10 @@ describe('CloudManagerClient', () => {
1051969 expect ( Buffer . isBuffer ( result ) ) . to . be . true ;
1052970 expect ( result . toString ( ) ) . to . equal ( 'zip-content' ) ;
1053971
1054- // Should create a temp dir for the zip file
1055- expect ( mkdtempSyncStub ) . to . have . been . calledOnce ;
1056- expect ( mkdtempSyncStub . firstCall . args [ 0 ] ) . to . match ( / c m - z i p - $ / ) ;
1057-
1058- // Should archive the folder with followSymlinks: false
972+ // Should call archiveFolder with followSymlinks: false
1059973 expect ( archiveFolderStub ) . to . have . been . calledOnce ;
1060974 expect ( archiveFolderStub . firstCall . args [ 0 ] ) . to . equal ( clonePath ) ;
1061- expect ( archiveFolderStub . firstCall . args [ 2 ] ) . to . deep . equal ( { followSymlinks : false } ) ;
1062-
1063- // Should read the zip file into a buffer
1064- expect ( readFileSyncStub ) . to . have . been . calledOnce ;
1065-
1066- // Should clean up the temp zip directory
1067- expect ( rmSyncStub ) . to . have . been . calledOnce ;
1068- expect ( rmSyncStub . firstCall . args [ 0 ] ) . to . match ( / c m - z i p - / ) ;
975+ expect ( archiveFolderStub . firstCall . args [ 1 ] ) . to . deep . equal ( { followSymlinks : false } ) ;
1069976 } ) ;
1070977
1071978 it ( 'rejects symlinks that escape the repo root before zipping' , async ( ) => {
@@ -1086,13 +993,45 @@ describe('CloudManagerClient', () => {
1086993
1087994 // archiveFolder should never be called
1088995 expect ( archiveFolderStub ) . to . not . have . been . called ;
996+ } ) ;
1089997
1090- // Should still clean up the temp zip directory
1091- expect ( rmSyncStub ) . to . have . been . calledOnce ;
1092- expect ( rmSyncStub . firstCall . args [ 0 ] ) . to . match ( / c m - z i p - / ) ;
998+ it ( 'logs a warning for broken symlinks but proceeds with zip' , async ( ) => {
999+ const clonePath = '/tmp/cm-repo-zip-test' ;
1000+ existsSyncStub . withArgs ( clonePath ) . returns ( true ) ;
1001+
1002+ const enabledFarmsPath = path . join ( clonePath , 'dispatcher' , 'enabled_farms' ) ;
1003+ const brokenLinkPath = path . join ( enabledFarmsPath , 'broken.farm' ) ;
1004+ const brokenTarget = '../available_farms/missing.farm' ;
1005+ const resolvedTarget = path . resolve ( enabledFarmsPath , brokenTarget ) ;
1006+
1007+ readdirSyncStub . withArgs ( clonePath , { withFileTypes : true } ) . returns ( [ {
1008+ name : 'dispatcher' ,
1009+ isSymbolicLink : ( ) => false ,
1010+ isDirectory : ( ) => true ,
1011+ } ] ) ;
1012+ readdirSyncStub . withArgs ( path . join ( clonePath , 'dispatcher' ) , { withFileTypes : true } ) . returns ( [ {
1013+ name : 'enabled_farms' ,
1014+ isSymbolicLink : ( ) => false ,
1015+ isDirectory : ( ) => true ,
1016+ } ] ) ;
1017+ readdirSyncStub . withArgs ( enabledFarmsPath , { withFileTypes : true } ) . returns ( [ {
1018+ name : 'broken.farm' ,
1019+ isSymbolicLink : ( ) => true ,
1020+ isDirectory : ( ) => false ,
1021+ } ] ) ;
1022+ readlinkSyncStub . withArgs ( brokenLinkPath ) . returns ( brokenTarget ) ;
1023+ existsSyncStub . withArgs ( resolvedTarget ) . returns ( false ) ;
1024+
1025+ const ctx = createContext ( ) ;
1026+ const client = CloudManagerClient . createFrom ( ctx ) ;
1027+ const result = await client . zipRepository ( clonePath ) ;
1028+
1029+ expect ( Buffer . isBuffer ( result ) ) . to . be . true ;
1030+ expect ( ctx . log . warn ) . to . have . been . calledWithMatch ( / B r o k e n s y m l i n k .* b r o k e n \. f a r m .* m i s s i n g \. f a r m / ) ;
1031+ expect ( archiveFolderStub ) . to . have . been . calledOnce ;
10931032 } ) ;
10941033
1095- it ( 'throws when archiveFolder fails and cleans up temp dir ' , async ( ) => {
1034+ it ( 'throws when archiveFolder fails' , async ( ) => {
10961035 const clonePath = '/tmp/cm-repo-zip-test' ;
10971036 existsSyncStub . withArgs ( clonePath ) . returns ( true ) ;
10981037 archiveFolderStub . rejects ( new Error ( 'failed to read directory' ) ) ;
@@ -1101,10 +1040,6 @@ describe('CloudManagerClient', () => {
11011040
11021041 await expect ( client . zipRepository ( clonePath ) )
11031042 . to . be . rejectedWith ( 'Failed to zip repository: failed to read directory' ) ;
1104-
1105- // Should still clean up the temp zip directory
1106- expect ( rmSyncStub ) . to . have . been . calledOnce ;
1107- expect ( rmSyncStub . firstCall . args [ 0 ] ) . to . match ( / c m - z i p - / ) ;
11081043 } ) ;
11091044 } ) ;
11101045
0 commit comments