diff --git a/test/unit/cache-policy.test.js b/test/unit/cache-policy.test.js new file mode 100644 index 0000000..e9e9a7f --- /dev/null +++ b/test/unit/cache-policy.test.js @@ -0,0 +1,102 @@ +import { describe, expect, it } from 'vitest'; + +import { CONFIG } from '../../src/config/index.js'; +import { + isImmutableArtifactRequest, + resolveCachePolicy, + resolveResponseCachePolicy +} from '../../src/upstream/cache-policy.js'; + +const requestContext = { + isAI: false, + isDocker: false, + isGit: false, + isGitLFS: false, + isHF: false +}; + +/** + * Resolves a cache policy with common non-protocol request defaults. + * @param {Partial[0]>} overrides Policy inputs to override. + * @returns {ReturnType} Resolved cache policy. + */ +function resolvePolicy(overrides = {}) { + return resolveCachePolicy({ + canUseCache: true, + config: CONFIG, + effectivePath: '/gh/user/repo/file.txt', + hasSensitiveHeaders: false, + platform: 'gh', + request: new Request('https://example.com/gh/user/repo/file.txt'), + requestContext, + targetUrl: 'https://github.com/user/repo/file.txt', + ...overrides + }); +} + +describe('Cache policy edge coverage', () => { + it('uses metadata caching for Maven metadata files', () => { + const policy = resolvePolicy({ + effectivePath: '/maven/maven2/org/example/demo/maven-metadata.xml', + platform: 'maven', + request: new Request('https://example.com/maven/maven2/org/example/demo/maven-metadata.xml'), + targetUrl: 'https://repo1.maven.org/maven2/org/example/demo/maven-metadata.xml' + }); + + expect(policy.cacheControl).toBe('public, max-age=0, s-maxage=60, must-revalidate'); + expect(policy.edgeTtl).toBe(60); + }); + + it('does not treat incomplete or malformed artifact paths as immutable', () => { + expect( + isImmutableArtifactRequest( + 'gh', + '/gh/user/repo/releases/download/', + 'https://github.com/user/repo/releases/download/' + ) + ).toBe(false); + expect( + isImmutableArtifactRequest( + 'pypi-files', + '/pypi/files/packages/source/p/pkg/pkg-%E0%A4%A.tar.gz', + '' + ) + ).toBe(false); + }); + + it('treats Vary star as uncacheable at response time', () => { + const basePolicy = resolvePolicy(); + const policy = resolveResponseCachePolicy({ + basePolicy, + response: new Response('ok', { + headers: { + Vary: 'Accept-Encoding, *' + } + }) + }); + + expect(policy.allowCacheApi).toBe(false); + expect(policy.cacheControl).toBe('no-store'); + }); + + it('ignores non-standard header objects that throw during response cache checks', () => { + const basePolicy = resolvePolicy(); + const response = { + headers: { + get() { + throw new Error('header get unavailable'); + }, + has() { + throw new Error('header has unavailable'); + } + } + }; + + const policy = resolveResponseCachePolicy({ + basePolicy, + response: /** @type {Response} */ (/** @type {unknown} */ (response)) + }); + + expect(policy).toBe(basePolicy); + }); +}); diff --git a/test/unit/pipeline-modules.test.js b/test/unit/pipeline-modules.test.js index 821cb17..9cb7b31 100644 --- a/test/unit/pipeline-modules.test.js +++ b/test/unit/pipeline-modules.test.js @@ -282,4 +282,57 @@ describe('Pipeline modules', () => { expect(targetA.cacheTargetUrl).not.toBe(targetB.cacheTargetUrl); } }); + + it('handles Docker 401 responses without auth challenges during finalization', async () => { + const request = new Request('https://example.com/cr/ghcr/v2/user/repo/manifests/latest'); + const requestContext = { + ...createRequestContext(request, {}), + isDocker: true + }; + + const customUnauthorized = await finalizeResponse({ + cache: null, + cacheTargetUrl: 'https://ghcr.io/v2/user/repo/manifests/latest', + canUseCache: false, + config: CONFIG, + ctx: /** @type {ExecutionContext} */ ({ waitUntil() {}, passThroughOnException() {} }), + effectivePath: '/cr/ghcr/v2/user/repo/manifests/latest', + hasSensitiveHeaders: false, + monitor: new PerformanceMonitor(), + platform: 'cr-ghcr', + request, + requestContext, + response: new Response('{"errors":[{"code":"UNAUTHORIZED"}]}', { + status: 401, + headers: { 'Content-Type': 'application/json' } + }), + responseGeneratedLocally: false, + url: new URL(request.url) + }); + + const plainUnauthorized = await finalizeResponse({ + cache: null, + cacheTargetUrl: 'https://ghcr.io/v2/user/repo/manifests/latest', + canUseCache: false, + config: CONFIG, + ctx: /** @type {ExecutionContext} */ ({ waitUntil() {}, passThroughOnException() {} }), + effectivePath: '/cr/ghcr/v2/user/repo/manifests/latest', + hasSensitiveHeaders: false, + monitor: new PerformanceMonitor(), + platform: 'cr-ghcr', + request, + requestContext, + response: new Response('denied', { + status: 401, + headers: { 'Content-Type': 'text/plain' } + }), + responseGeneratedLocally: false, + url: new URL(request.url) + }); + + expect(customUnauthorized.status).toBe(401); + expect(await customUnauthorized.text()).toContain('UNAUTHORIZED'); + expect(plainUnauthorized.status).toBe(401); + expect(await plainUnauthorized.text()).toContain('Original error: denied'); + }); });