@@ -272,6 +272,90 @@ void testShouldReadCredentialsFromPodManConfig() throws Exception {
272272 });
273273 }
274274
275+ // language=json
276+ public static final String SAMPLE_HIERARCHICAL_CONFIG =
277+ """
278+ {
279+ "auths": {
280+ "my-registry.local/namespace/user/image": {
281+ "auth": "dXNlcjE6cGFzczE="
282+ },
283+ "my-registry.local/namespace": {
284+ "auth": "dXNlcjM6cGFzczM="
285+ },
286+ "my-registry.local": {
287+ "auth": "dXNlcjI6cGFzczI="
288+ }
289+ }
290+ }
291+ """ ;
292+
293+ @ Test
294+ void testHierarchicalCredentialLookupMostSpecific () throws Exception {
295+ Path configFile = tempDir .resolve ("hierarchical-config.json" );
296+ Files .writeString (configFile , SAMPLE_HIERARCHICAL_CONFIG );
297+ AuthStore store = AuthStore .newStore (List .of (configFile ));
298+
299+ // Most specific key: my-registry.local/namespace/user/image
300+ AuthStore .Credential credential =
301+ store .get (ContainerRef .parse ("my-registry.local/namespace/user/image:latest" ));
302+ assertNotNull (credential );
303+ assertEquals ("user1" , credential .username ());
304+ assertEquals ("pass1" , credential .password ());
305+ }
306+
307+ @ Test
308+ void testHierarchicalCredentialLookupNamespaceOnly () throws Exception {
309+ Path configFile = tempDir .resolve ("hierarchical-config.json" );
310+ Files .writeString (configFile , SAMPLE_HIERARCHICAL_CONFIG );
311+ AuthStore store = AuthStore .newStore (List .of (configFile ));
312+
313+ // Credential stored at namespace level: my-registry.local/namespace
314+ // Image under that namespace but not exact-matched should fall back to namespace credential
315+ AuthStore .Credential credential =
316+ store .get (ContainerRef .parse ("my-registry.local/namespace/other-image:latest" ));
317+ assertNotNull (credential );
318+ assertEquals ("user3" , credential .username ());
319+ assertEquals ("pass3" , credential .password ());
320+ }
321+
322+ @ Test
323+ void testHierarchicalCredentialLookupFallsBackToRegistry () throws Exception {
324+ Path configFile = tempDir .resolve ("hierarchical-config.json" );
325+ Files .writeString (configFile , SAMPLE_HIERARCHICAL_CONFIG );
326+ AuthStore store = AuthStore .newStore (List .of (configFile ));
327+
328+ // Different image under the same registry falls back to registry-level credential
329+ AuthStore .Credential credential = store .get (ContainerRef .parse ("my-registry.local/other/repo:latest" ));
330+ assertNotNull (credential );
331+ assertEquals ("user2" , credential .username ());
332+ assertEquals ("pass2" , credential .password ());
333+ }
334+
335+ @ Test
336+ void testHierarchicalCredentialLookupRegistryOnly () throws Exception {
337+ Path configFile = tempDir .resolve ("hierarchical-config.json" );
338+ Files .writeString (configFile , SAMPLE_HIERARCHICAL_CONFIG );
339+ AuthStore store = AuthStore .newStore (List .of (configFile ));
340+
341+ // Image without namespace falls back to registry-level credential
342+ AuthStore .Credential credential = store .get (ContainerRef .parse ("my-registry.local/image:latest" ));
343+ assertNotNull (credential );
344+ assertEquals ("user2" , credential .username ());
345+ assertEquals ("pass2" , credential .password ());
346+ }
347+
348+ @ Test
349+ void testHierarchicalCredentialLookupNoMatch () throws Exception {
350+ Path configFile = tempDir .resolve ("hierarchical-config.json" );
351+ Files .writeString (configFile , SAMPLE_HIERARCHICAL_CONFIG );
352+ AuthStore store = AuthStore .newStore (List .of (configFile ));
353+
354+ // Unknown registry returns null
355+ AuthStore .Credential credential = store .get (ContainerRef .parse ("unknown-registry.local/foo/bar:latest" ));
356+ assertNull (credential );
357+ }
358+
275359 @ Test
276360 void testWithoutXdgRuntimeDir () throws Exception {
277361 new EnvironmentVariables ().remove ("XDG_RUNTIME_DIR" ).execute (() -> {
0 commit comments