@@ -149,3 +149,135 @@ func TestMultiplevolatileConfigWithSameValue(t *testing.T) {
149149 t .Errorf ("did not expect validation errors: %v" , errs )
150150 }
151151}
152+
153+ func TestImageUrlPattern (t * testing.T ) {
154+ tests := []struct {
155+ name string
156+ url string
157+ wantValid bool
158+ omitField bool // true if the field should be omitted entirely
159+ }{
160+ // Valid cases
161+ {"Simple registry path" , "quay.io/org/repo1" , true , false },
162+ {"Multi-level org path" , "registry.io/org1/org2/repo1" , true , false },
163+ {"Docker library path" , "docker.io/library/nginx" , true , false },
164+ {"GitHub container registry" , "ghcr.io/org/project/repo" , true , false },
165+ {"Registry with subdomain" , "my-registry.com/org/suborg/repo" , true , false },
166+ {"Registry with multiple subdomains" , "prod.registry.example.com/org/repo" , true , false },
167+ {"Registry with hyphens" , "my-registry.example.com/org-name/repo-name" , true , false },
168+ {"Registry with numbers" , "registry123.example.com/org123/repo123" , true , false },
169+ {"Extra path component" , "registry/org/repo/extra" , true , false },
170+ {"Omitted field" , "" , true , true }, // Field is omitted entirely
171+
172+ // Invalid cases
173+ {"URL with HTTPS" , "https://quay.io/org/repo" , false , false },
174+ {"Localhost with port" , "localhost:5000/org/repo" , false , false },
175+ {"Invalid character @" , "invalid@registry/org/repo" , false , false },
176+ {"Missing repo" , "registry/org" , false , false },
177+ {"Double slash" , "registry//org/repo" , false , false },
178+ {"Trailing slash" , "registry/org/repo/" , false , false },
179+ {"With tag" , "registry/org/repo:tag" , false , false },
180+ {"With digest" , "registry/org/repo@sha256:abc123" , false , false },
181+ {"Missing repo with slash" , "registry/org/" , false , false },
182+ {"Only registry with slash" , "registry/" , false , false },
183+ {"Only registry" , "registry" , false , false },
184+ {"Extra path with slash" , "registry/org/repo/extra/" , false , false },
185+ }
186+
187+ for _ , tt := range tests {
188+ t .Run (tt .name , func (t * testing.T ) {
189+ // Create a policy with the test URL
190+ policy := EnterpriseContractPolicy {
191+ Spec : EnterpriseContractPolicySpec {
192+ Sources : []Source {
193+ {
194+ VolatileConfig : & VolatileSourceConfig {
195+ Exclude : []VolatileCriteria {
196+ {
197+ Value : "test-rule" ,
198+ },
199+ },
200+ },
201+ },
202+ },
203+ },
204+ }
205+ if ! tt .omitField {
206+ policy .Spec .Sources [0 ].VolatileConfig .Exclude [0 ].ImageUrl = tt .url
207+ }
208+
209+ // Create a CRD validation schema
210+ crd := v1.CustomResourceDefinition {}
211+ bytes , err := os .ReadFile ("../../config/crd/bases/appstudio.redhat.com_enterprisecontractpolicies.yaml" )
212+ if err != nil {
213+ t .Fatalf ("unexpected error reading CRD: %s" , err )
214+ }
215+ if err := yaml .Unmarshal (bytes , & crd ); err != nil {
216+ t .Fatalf ("unexpected error when decoding schema: %s" , err )
217+ }
218+
219+ crdv := apiextensions.CustomResourceValidation {}
220+ if err := v1 .Convert_v1_CustomResourceValidation_To_apiextensions_CustomResourceValidation (crd .Spec .Versions [0 ].Schema , & crdv , nil ); err != nil {
221+ t .Fatalf ("failed in CRD validation conversion: %s" , err )
222+ }
223+
224+ s , err := schema .NewStructural (crdv .OpenAPIV3Schema )
225+ if err != nil {
226+ t .Fatalf ("unexpected error when creating structural: %s" , err )
227+ }
228+
229+ v := validation .NewSchemaValidatorFromOpenAPI (s .ToKubeOpenAPI ())
230+
231+ // Convert policy to unstructured for validation
232+ obj := unstructured.Unstructured {}
233+ obj .SetUnstructuredContent (map [string ]interface {}{
234+ "apiVersion" : "appstudio.redhat.com/v1alpha1" ,
235+ "kind" : "EnterpriseContractPolicy" ,
236+ "spec" : map [string ]interface {}{
237+ "sources" : []interface {}{
238+ map [string ]interface {}{
239+ "volatileConfig" : map [string ]interface {}{
240+ "exclude" : []interface {}{
241+ func () map [string ]interface {} {
242+ m := map [string ]interface {}{
243+ "value" : "test-rule" ,
244+ }
245+ if ! tt .omitField {
246+ m ["imageUrl" ] = tt .url
247+ }
248+ return m
249+ }(),
250+ },
251+ },
252+ },
253+ },
254+ },
255+ })
256+
257+ // Validate the object
258+ result := v .Validate (& obj )
259+ isValid := result .IsValid ()
260+
261+ if isValid != tt .wantValid {
262+ t .Errorf ("Validation for %q = %v, want %v. Errors: %v" , tt .url , isValid , tt .wantValid , result .Errors )
263+ }
264+
265+ // Also validate the actual policy object
266+ policyObj := unstructured.Unstructured {}
267+ policyBytes , err := json .Marshal (policy )
268+ if err != nil {
269+ t .Fatalf ("unexpected error marshaling policy: %s" , err )
270+ }
271+ if err := json .Unmarshal (policyBytes , & policyObj .Object ); err != nil {
272+ t .Fatalf ("unexpected error unmarshaling policy: %s" , err )
273+ }
274+
275+ policyResult := v .Validate (& policyObj )
276+ policyIsValid := policyResult .IsValid ()
277+
278+ if policyIsValid != tt .wantValid {
279+ t .Errorf ("Policy validation for %q = %v, want %v. Errors: %v" , tt .url , policyIsValid , tt .wantValid , policyResult .Errors )
280+ }
281+ })
282+ }
283+ }
0 commit comments