@@ -47,18 +47,24 @@ function mockS3ListVersions(prefix: "app" | "system", versions: string[]) {
4747}
4848
4949// Mock S3 hash file response for legacy versions (no SKU support)
50- function mockS3HashFile ( prefix : "app" | "system" , version : string , hash : string ) {
50+ function mockS3HashFile ( prefix : "app" | "system" , version : string , hash : string , opts ?: { hasSig ?: boolean } ) {
5151 const fileName = prefix === "app" ? "jetkvm_app" : "system.tar" ;
52+ const artifactPath = `${ prefix } /${ version } /${ fileName } ` ;
5253
5354 // Mock versionHasSkuSupport to return false (no SKU folders)
5455 s3Mock . on ( ListObjectsV2Command , { Prefix : `${ prefix } /${ version } /skus/` } ) . resolves ( {
5556 Contents : [ ] ,
5657 } ) ;
5758
5859 // Mock legacy hash path
59- s3Mock . on ( GetObjectCommand , { Key : `${ prefix } / ${ version } / ${ fileName } .sha256` } ) . resolves ( {
60+ s3Mock . on ( GetObjectCommand , { Key : `${ artifactPath } .sha256` } ) . resolves ( {
6061 Body : createAsyncIterable ( hash ) as any ,
6162 } ) ;
63+
64+ // Mock .sig existence check (absence handled by default HeadObject reject in beforeEach)
65+ if ( opts ?. hasSig ) {
66+ s3Mock . on ( HeadObjectCommand , { Key : `${ artifactPath } .sig` } ) . resolves ( { } ) ;
67+ }
6268}
6369
6470// Mock S3 for versions with SKU support
@@ -67,6 +73,7 @@ function mockS3SkuVersion(
6773 version : string ,
6874 sku : string ,
6975 hash : string ,
76+ opts ?: { hasSig ?: boolean } ,
7077) {
7178 const fileName = prefix === "app" ? "jetkvm_app" : "system.tar" ;
7279 const skuPath = `${ prefix } /${ version } /skus/${ sku } /${ fileName } ` ;
@@ -83,6 +90,11 @@ function mockS3SkuVersion(
8390 s3Mock . on ( GetObjectCommand , { Key : `${ skuPath } .sha256` } ) . resolves ( {
8491 Body : createAsyncIterable ( hash ) as any ,
8592 } ) ;
93+
94+ // Mock .sig existence check (absence handled by default HeadObject reject in beforeEach)
95+ if ( opts ?. hasSig ) {
96+ s3Mock . on ( HeadObjectCommand , { Key : `${ skuPath } .sig` } ) . resolves ( { } ) ;
97+ }
8698}
8799
88100
@@ -161,6 +173,9 @@ function findDeviceIdInsideRollout(threshold: number) {
161173describe ( "Retrieve handler" , ( ) => {
162174 beforeEach ( ( ) => {
163175 s3Mock . reset ( ) ;
176+ // Default: .sig files don't exist unless explicitly mocked per-key.
177+ // More specific .on(HeadObjectCommand, { Key }) mocks take precedence.
178+ s3Mock . on ( HeadObjectCommand ) . rejects ( { name : "NotFound" , $metadata : { httpStatusCode : 404 } } ) ;
164179 clearCaches ( ) ;
165180 } ) ;
166181
@@ -451,6 +466,68 @@ describe("Retrieve handler", () => {
451466 } ) ;
452467 } ) ;
453468
469+ describe ( "signature URL handling" , ( ) => {
470+ it ( "should include sigUrl when .sig file exists" , async ( ) => {
471+ const req = createMockRequest ( {
472+ deviceId : "device-sig" ,
473+ prerelease : "true" ,
474+ appVersion : "^6.0.0" ,
475+ systemVersion : "^6.0.0" ,
476+ } ) ;
477+ const res = createMockResponse ( ) ;
478+
479+ mockS3ListVersions ( "app" , [ "6.0.0" ] ) ;
480+ mockS3ListVersions ( "system" , [ "6.0.0" ] ) ;
481+ mockS3HashFile ( "app" , "6.0.0" , "sig-app-hash" , { hasSig : true } ) ;
482+ mockS3HashFile ( "system" , "6.0.0" , "sig-system-hash" , { hasSig : true } ) ;
483+
484+ await Retrieve ( req , res ) ;
485+
486+ expect ( res . _json . appSigUrl ) . toBe ( "https://cdn.test.com/app/6.0.0/jetkvm_app.sig" ) ;
487+ expect ( res . _json . systemSigUrl ) . toBe ( "https://cdn.test.com/system/6.0.0/system.tar.sig" ) ;
488+ } ) ;
489+
490+ it ( "should omit sigUrl when .sig file does not exist" , async ( ) => {
491+ const req = createMockRequest ( {
492+ deviceId : "device-nosig" ,
493+ prerelease : "true" ,
494+ appVersion : "^7.0.0" ,
495+ systemVersion : "^7.0.0" ,
496+ } ) ;
497+ const res = createMockResponse ( ) ;
498+
499+ mockS3ListVersions ( "app" , [ "7.0.0" ] ) ;
500+ mockS3ListVersions ( "system" , [ "7.0.0" ] ) ;
501+ mockS3HashFile ( "app" , "7.0.0" , "nosig-app-hash" ) ;
502+ mockS3HashFile ( "system" , "7.0.0" , "nosig-system-hash" ) ;
503+
504+ await Retrieve ( req , res ) ;
505+
506+ expect ( res . _json . appSigUrl ) . toBeUndefined ( ) ;
507+ expect ( res . _json . systemSigUrl ) . toBeUndefined ( ) ;
508+ } ) ;
509+
510+ it ( "should include sigUrl with SKU path when .sig file exists" , async ( ) => {
511+ const req = createMockRequest ( {
512+ deviceId : "device-sku-sig" ,
513+ sku : "jetkvm-2" ,
514+ appVersion : "^8.0.0" ,
515+ systemVersion : "^8.0.0" ,
516+ } ) ;
517+ const res = createMockResponse ( ) ;
518+
519+ mockS3ListVersions ( "app" , [ "8.0.0" ] ) ;
520+ mockS3ListVersions ( "system" , [ "8.0.0" ] ) ;
521+ mockS3SkuVersion ( "app" , "8.0.0" , "jetkvm-2" , "sku-sig-app-hash" , { hasSig : true } ) ;
522+ mockS3SkuVersion ( "system" , "8.0.0" , "jetkvm-2" , "sku-sig-system-hash" , { hasSig : true } ) ;
523+
524+ await Retrieve ( req , res ) ;
525+
526+ expect ( res . _json . appSigUrl ) . toBe ( "https://cdn.test.com/app/8.0.0/skus/jetkvm-2/jetkvm_app.sig" ) ;
527+ expect ( res . _json . systemSigUrl ) . toBe ( "https://cdn.test.com/system/8.0.0/skus/jetkvm-2/system.tar.sig" ) ;
528+ } ) ;
529+ } ) ;
530+
454531 describe ( "forceUpdate mode" , ( ) => {
455532 it ( "should return latest release when forceUpdate=true" , async ( ) => {
456533 // Use unique version constraints to get unique cache keys
@@ -473,6 +550,25 @@ describe("Retrieve handler", () => {
473550 expect ( res . _json . appVersion ) . toBe ( "1.5.5" ) ;
474551 expect ( res . _json . systemVersion ) . toBe ( "1.5.5" ) ;
475552 } ) ;
553+
554+ it ( "should include sigUrl when forceUpdate=true and .sig file exists" , async ( ) => {
555+ const req = createMockRequest ( {
556+ deviceId : "device-force-sig" ,
557+ forceUpdate : "true" ,
558+ } ) ;
559+ const res = createMockResponse ( ) ;
560+
561+ mockS3ListVersions ( "app" , [ "10.0.0" ] ) ;
562+ mockS3ListVersions ( "system" , [ "10.0.0" ] ) ;
563+ mockS3HashFile ( "app" , "10.0.0" , "force-sig-app-hash" , { hasSig : true } ) ;
564+ mockS3HashFile ( "system" , "10.0.0" , "force-sig-system-hash" , { hasSig : true } ) ;
565+
566+ await Retrieve ( req , res ) ;
567+
568+ expect ( res . _json . appVersion ) . toBe ( "10.0.0" ) ;
569+ expect ( res . _json . appSigUrl ) . toBe ( "https://cdn.test.com/app/10.0.0/jetkvm_app.sig" ) ;
570+ expect ( res . _json . systemSigUrl ) . toBe ( "https://cdn.test.com/system/10.0.0/system.tar.sig" ) ;
571+ } ) ;
476572 } ) ;
477573
478574 describe ( "rollout logic" , ( ) => {
@@ -571,6 +667,28 @@ describe("Retrieve handler", () => {
571667 expect ( res . _json . appVersion ) . toBe ( "1.2.0" ) ;
572668 expect ( res . _json . systemVersion ) . toBe ( "1.1.0" ) ;
573669 } ) ;
670+
671+ it ( "should include sigUrl for rollout-eligible device when .sig file exists" , async ( ) => {
672+ await setRollout ( "1.1.0" , "app" , 100 ) ;
673+ await setRollout ( "1.1.0" , "system" , 100 ) ;
674+ await setRollout ( "1.2.0" , "app" , 100 ) ;
675+ await setRollout ( "1.2.0" , "system" , 100 ) ;
676+
677+ const deviceId = findDeviceIdInsideRollout ( 100 ) ;
678+ const req = createMockRequest ( { deviceId } ) ;
679+ const res = createMockResponse ( ) ;
680+
681+ mockS3ListVersions ( "app" , [ "1.0.0" , "1.1.0" , "1.2.0" ] ) ;
682+ mockS3ListVersions ( "system" , [ "1.0.0" , "1.1.0" , "1.2.0" ] ) ;
683+ mockS3HashFile ( "app" , "1.2.0" , "rollout-sig-app-hash" , { hasSig : true } ) ;
684+ mockS3HashFile ( "system" , "1.2.0" , "rollout-sig-system-hash" , { hasSig : true } ) ;
685+
686+ await Retrieve ( req , res ) ;
687+
688+ expect ( res . _json . appVersion ) . toBe ( "1.2.0" ) ;
689+ expect ( res . _json . appSigUrl ) . toBe ( "https://cdn.test.com/app/1.2.0/jetkvm_app.sig" ) ;
690+ expect ( res . _json . systemSigUrl ) . toBe ( "https://cdn.test.com/system/1.2.0/system.tar.sig" ) ;
691+ } ) ;
574692 } ) ;
575693
576694 describe ( "default release handling" , ( ) => {
0 commit comments