1+ import assert from "assert" ;
2+ import fs from "fs" ;
3+ import os from "os" ;
4+ import path from "path" ;
5+ import yaml from "js-yaml" ;
6+ import {
7+ extractNameVersion ,
8+ buildPackageUrl ,
9+ patchCliConstructor ,
10+ parseConstructorFile ,
11+ } from "./component-constructor.js" ;
12+
13+ // ----------------------------------------------------------
14+ // extractNameVersion
15+ // ----------------------------------------------------------
16+ console . log ( "Testing extractNameVersion..." ) ;
17+
18+ assert . deepStrictEqual (
19+ extractNameVersion ( { name : "ocm.software/cli" , version : "1.2.3" } ) ,
20+ { name : "ocm.software/cli" , version : "1.2.3" } ,
21+ "Should extract name and version from valid constructor"
22+ ) ;
23+
24+ assert . deepStrictEqual (
25+ extractNameVersion ( { name : "ocm.software/plugins/helm" , version : "0.0.0-main" } ) ,
26+ { name : "ocm.software/plugins/helm" , version : "0.0.0-main" } ,
27+ "Should handle nested component names"
28+ ) ;
29+
30+ assert . throws (
31+ ( ) => extractNameVersion ( { version : "1.0.0" } ) ,
32+ / m i s s i n g r e q u i r e d f i e l d ' n a m e ' / ,
33+ "Should throw when name is missing"
34+ ) ;
35+
36+ assert . throws (
37+ ( ) => extractNameVersion ( { name : "foo" } ) ,
38+ / m i s s i n g r e q u i r e d f i e l d ' v e r s i o n ' / ,
39+ "Should throw when version is missing"
40+ ) ;
41+
42+ assert . throws (
43+ ( ) => extractNameVersion ( { name : "" , version : "1.0.0" } ) ,
44+ / m i s s i n g r e q u i r e d f i e l d ' n a m e ' / ,
45+ "Should throw when name is empty"
46+ ) ;
47+
48+ assert . throws (
49+ ( ) => extractNameVersion ( { name : 42 , version : "1.0.0" } ) ,
50+ / m i s s i n g r e q u i r e d f i e l d ' n a m e ' / ,
51+ "Should throw when name is not a string"
52+ ) ;
53+
54+ // ----------------------------------------------------------
55+ // buildPackageUrl
56+ // ----------------------------------------------------------
57+ console . log ( "Testing buildPackageUrl..." ) ;
58+
59+ assert . strictEqual (
60+ buildPackageUrl ( "open-component-model/open-component-model" , "ocm.software/cli" ) ,
61+ "https://github.com/open-component-model/open-component-model/pkgs/container/component-descriptors%2Focm.software%2Fcli" ,
62+ "Should build correct package URL with encoded slashes"
63+ ) ;
64+
65+ assert . strictEqual (
66+ buildPackageUrl ( "my-org/my-repo" , "simple" ) ,
67+ "https://github.com/my-org/my-repo/pkgs/container/component-descriptors%2Fsimple" ,
68+ "Should handle component name without slashes"
69+ ) ;
70+
71+ assert . strictEqual (
72+ buildPackageUrl ( "org/repo" , "a/b/c/d" ) ,
73+ "https://github.com/org/repo/pkgs/container/component-descriptors%2Fa%2Fb%2Fc%2Fd" ,
74+ "Should encode all slashes in deeply nested component names"
75+ ) ;
76+
77+ // ----------------------------------------------------------
78+ // patchCliConstructor
79+ // ----------------------------------------------------------
80+ console . log ( "Testing patchCliConstructor..." ) ;
81+
82+ {
83+ const constructor = {
84+ name : "ocm.software/cli" ,
85+ version : "1.0.0" ,
86+ resources : [
87+ {
88+ name : "cli" ,
89+ type : "executable" ,
90+ input : { type : "file" , path : "/full/absolute/path/to/bin/ocm-linux-amd64" } ,
91+ extraIdentity : { os : "linux" , architecture : "amd64" } ,
92+ relation : "local" ,
93+ } ,
94+ {
95+ name : "cli" ,
96+ type : "executable" ,
97+ input : { type : "file" , path : "/another/path/bin/ocm-darwin-arm64" } ,
98+ extraIdentity : { os : "darwin" , architecture : "arm64" } ,
99+ relation : "local" ,
100+ } ,
101+ {
102+ name : "image" ,
103+ type : "ociImage" ,
104+ version : "old" ,
105+ relation : "local" ,
106+ input : { type : "file" , mediaType : "application/vnd.ocm.software.oci.layout.v1+tar" , path : "/path/to/cli.tar" } ,
107+ } ,
108+ ] ,
109+ } ;
110+
111+ const result = patchCliConstructor ( constructor , "ghcr.io/ocm/cli:v1.0.0" , "v1.0.0" ) ;
112+
113+ // CLI binary paths should be rewritten
114+ assert . strictEqual (
115+ result . resources [ 0 ] . input . path ,
116+ "resources/bin/ocm-linux-amd64" ,
117+ "Should rewrite first CLI binary path"
118+ ) ;
119+ assert . strictEqual (
120+ result . resources [ 1 ] . input . path ,
121+ "resources/bin/ocm-darwin-arm64" ,
122+ "Should rewrite second CLI binary path"
123+ ) ;
124+
125+ // Image resource should be converted to ociArtifact
126+ const image = result . resources [ 2 ] ;
127+ assert . strictEqual ( image . type , "ociImage" , "Image type should be ociImage" ) ;
128+ assert . strictEqual ( image . version , "v1.0.0" , "Image version should be updated" ) ;
129+ assert . deepStrictEqual ( image . access , {
130+ type : "ociArtifact" ,
131+ imageReference : "ghcr.io/ocm/cli:v1.0.0" ,
132+ } , "Image access should have ociArtifact reference" ) ;
133+ assert . strictEqual ( image . relation , undefined , "relation should be deleted" ) ;
134+ assert . strictEqual ( image . input , undefined , "input should be deleted" ) ;
135+ }
136+
137+ // patchCliConstructor: missing image resource
138+ assert . throws (
139+ ( ) => patchCliConstructor ( { resources : [ { name : "cli" , input : { type : "file" , path : "x" } } ] } , "ref" , "tag" ) ,
140+ / n o r e s o u r c e n a m e d ' i m a g e ' / ,
141+ "Should throw when image resource is missing"
142+ ) ;
143+
144+ // patchCliConstructor: missing resources array
145+ assert . throws (
146+ ( ) => patchCliConstructor ( { name : "test" } , "ref" , "tag" ) ,
147+ / n o ' r e s o u r c e s ' a r r a y / ,
148+ "Should throw when resources array is missing"
149+ ) ;
150+
151+ // patchCliConstructor: missing input.path on CLI file resource
152+ assert . throws (
153+ ( ) => patchCliConstructor ( {
154+ resources : [
155+ { name : "cli" , input : { type : "file" } } ,
156+ { name : "image" , type : "ociImage" , relation : "local" , input : { type : "file" , path : "x" } } ,
157+ ] ,
158+ } , "ref" , "tag" ) ,
159+ / m i s s i n g r e q u i r e d f i e l d ' i n p u t \. p a t h ' / ,
160+ "Should throw when CLI file resource has no input.path"
161+ ) ;
162+
163+ // patchCliConstructor: empty input.path on CLI file resource
164+ assert . throws (
165+ ( ) => patchCliConstructor ( {
166+ resources : [
167+ { name : "cli" , input : { type : "file" , path : "" } } ,
168+ { name : "image" , type : "ociImage" , relation : "local" , input : { type : "file" , path : "x" } } ,
169+ ] ,
170+ } , "ref" , "tag" ) ,
171+ / m i s s i n g r e q u i r e d f i e l d ' i n p u t \. p a t h ' / ,
172+ "Should throw when CLI file resource has empty input.path"
173+ ) ;
174+
175+ // patchCliConstructor: non-file CLI resources are left untouched
176+ {
177+ const constructor = {
178+ resources : [
179+ { name : "cli" , type : "executable" , input : { type : "dir" , path : "/some/dir" } } ,
180+ { name : "image" , type : "ociImage" , relation : "local" , input : { type : "file" , path : "x" } } ,
181+ ] ,
182+ } ;
183+ patchCliConstructor ( constructor , "ref" , "tag" ) ;
184+ assert . strictEqual (
185+ constructor . resources [ 0 ] . input . path ,
186+ "/some/dir" ,
187+ "Non-file CLI resources should not be modified"
188+ ) ;
189+ }
190+
191+ // ----------------------------------------------------------
192+ // parseConstructorFile (round-trip via temp file)
193+ // ----------------------------------------------------------
194+ console . log ( "Testing parseConstructorFile..." ) ;
195+
196+ {
197+ const tmpDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "ocm-test-" ) ) ;
198+ const tmpFile = path . join ( tmpDir , "component-constructor.yaml" ) ;
199+
200+ const testDoc = { name : "ocm.software/test" , version : "0.1.0" , provider : { name : "test" } } ;
201+ fs . writeFileSync ( tmpFile , yaml . dump ( testDoc ) , "utf8" ) ;
202+
203+ const parsed = parseConstructorFile ( tmpFile ) ;
204+ assert . strictEqual ( parsed . name , "ocm.software/test" , "Should parse name from YAML file" ) ;
205+ assert . strictEqual ( parsed . version , "0.1.0" , "Should parse version from YAML file" ) ;
206+
207+ // Cleanup
208+ fs . unlinkSync ( tmpFile ) ;
209+ fs . rmdirSync ( tmpDir ) ;
210+ }
211+
212+ // parseConstructorFile: non-existent file
213+ assert . throws (
214+ ( ) => parseConstructorFile ( "/nonexistent/file.yaml" ) ,
215+ / E N O E N T / ,
216+ "Should throw for non-existent file"
217+ ) ;
218+
219+ // parseConstructorFile: invalid YAML (empty file)
220+ {
221+ const tmpDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "ocm-test-" ) ) ;
222+ const tmpFile = path . join ( tmpDir , "empty.yaml" ) ;
223+ fs . writeFileSync ( tmpFile , "" , "utf8" ) ;
224+
225+ assert . throws (
226+ ( ) => parseConstructorFile ( tmpFile ) ,
227+ / I n v a l i d c o n s t r u c t o r f i l e / ,
228+ "Should throw for empty YAML file"
229+ ) ;
230+
231+ fs . unlinkSync ( tmpFile ) ;
232+ fs . rmdirSync ( tmpDir ) ;
233+ }
234+
235+ console . log ( "✅ All component-constructor tests passed." ) ;
0 commit comments