mirror of
https://github.com/actions/setup-java.git
synced 2026-07-30 09:10:01 +08:00
Verify JDK downloads with vendor checksums (#1167)
* Verify JDK downloads with vendor checksums Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a800a031-600e-4d28-b23e-be309555d38d * Handle missing vendor checksum values Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a800a031-600e-4d28-b23e-be309555d38d * Preserve checksum error during cleanup failure Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a800a031-600e-4d28-b23e-be309555d38d * Validate checksum metadata value types Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a800a031-600e-4d28-b23e-be309555d38d * Clarify checksum documentation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a800a031-600e-4d28-b23e-be309555d38d * Expand vendor checksum verification Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a800a031-600e-4d28-b23e-be309555d38d * Accept SHA-256 or SHA-512 for JetBrains checksum sibling JetBrains publishes a single, generically-named ".checksum" sibling whose digest algorithm isn't disclosed by the filename. Older JBR 11 builds (e.g. jbrsdk_nomod-11_0_16-*-b2043.64.tar.gz) publish a SHA-256 digest there, while newer builds publish SHA-512. The JetBrains installer previously assumed SHA-512 unconditionally, so verification failed with "Malformed sha512 checksum metadata ... expected a 128-character hexadecimal digest" for those older builds, breaking the jetbrains 11 e2e job on macOS and Windows. fetchChecksum now accepts a list of candidate algorithms and infers the actual algorithm from the returned digest's length, preferring the strongest match. The JetBrains installer passes ['sha512', 'sha256']; all other callers are unaffected since they already pass a single, vendor-disclosed algorithm. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a800a031-600e-4d28-b23e-be309555d38d * Use SapMachine archive checksum files Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a800a031-600e-4d28-b23e-be309555d38d --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a800a031-600e-4d28-b23e-be309555d38d
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
import {afterEach, describe, expect, it, jest} from '@jest/globals';
|
||||
import {createHash} from 'crypto';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
import {calculateChecksum, verifyChecksum} from '../src/checksum.js';
|
||||
import type {ChecksumMetadata} from '../src/distributions/base-models.js';
|
||||
|
||||
const temporaryPaths: string[] = [];
|
||||
|
||||
async function temporaryFile(contents: string): Promise<string> {
|
||||
const directory = await fs.promises.mkdtemp(
|
||||
path.join(os.tmpdir(), 'setup-java-checksum-')
|
||||
);
|
||||
const file = path.join(directory, 'archive');
|
||||
await fs.promises.writeFile(file, contents);
|
||||
temporaryPaths.push(directory);
|
||||
return file;
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
temporaryPaths
|
||||
.splice(0)
|
||||
.map(item => fs.promises.rm(item, {recursive: true, force: true}))
|
||||
);
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('verifyChecksum', () => {
|
||||
it.each(['sha256', 'sha512'] as const)(
|
||||
'verifies a matching %s digest',
|
||||
async algorithm => {
|
||||
const contents = `jdk archive for ${algorithm}`;
|
||||
const file = await temporaryFile(contents);
|
||||
const value = createHash(algorithm).update(contents).digest('hex');
|
||||
|
||||
await expect(
|
||||
verifyChecksum(
|
||||
file,
|
||||
{algorithm, value: value.toUpperCase()},
|
||||
{distribution: 'Test', version: '21.0.1'}
|
||||
)
|
||||
).resolves.toBeUndefined();
|
||||
}
|
||||
);
|
||||
|
||||
it('reports mismatch context and both digests', async () => {
|
||||
const file = await temporaryFile('corrupt archive');
|
||||
const expected = 'a'.repeat(64);
|
||||
const actual = await calculateChecksum(file, 'sha256');
|
||||
|
||||
await expect(
|
||||
verifyChecksum(
|
||||
file,
|
||||
{algorithm: 'sha256', value: expected},
|
||||
{distribution: 'Corretto', version: '21.0.8'}
|
||||
)
|
||||
).rejects.toThrow(
|
||||
`Checksum verification failed for Corretto version 21.0.8: sha256 expected ${expected}, actual ${actual}.`
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects malformed digest metadata before reading the file', async () => {
|
||||
await expect(
|
||||
verifyChecksum(
|
||||
'/missing/archive',
|
||||
{algorithm: 'sha512', value: 'not-a-digest'},
|
||||
{distribution: 'Test', version: '17'}
|
||||
)
|
||||
).rejects.toThrow(
|
||||
'Malformed sha512 checksum metadata: expected a 128-character hexadecimal digest.'
|
||||
);
|
||||
});
|
||||
|
||||
it.each([undefined, null, 123])(
|
||||
'reports a malformed digest when the value is %p',
|
||||
async value => {
|
||||
const checksum = {
|
||||
algorithm: 'sha256',
|
||||
value
|
||||
} as unknown as ChecksumMetadata;
|
||||
|
||||
await expect(
|
||||
verifyChecksum('/missing/archive', checksum, {
|
||||
distribution: 'Test',
|
||||
version: '17'
|
||||
})
|
||||
).rejects.toThrow(
|
||||
'Malformed sha256 checksum metadata: expected a 64-character hexadecimal digest.'
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
it('rejects unsupported algorithms without leaking source query parameters', async () => {
|
||||
const checksum = {
|
||||
algorithm: 'md5',
|
||||
value: 'a'.repeat(32),
|
||||
source: 'https://vendor.example/checksum.txt?token=secret-value#private'
|
||||
} as unknown as ChecksumMetadata;
|
||||
|
||||
let message = '';
|
||||
try {
|
||||
await verifyChecksum('/missing/archive', checksum, {
|
||||
distribution: 'Test',
|
||||
version: '17'
|
||||
});
|
||||
} catch (error) {
|
||||
message = (error as Error).message;
|
||||
}
|
||||
|
||||
expect(message).toContain(
|
||||
"Unsupported checksum algorithm 'md5' from https://vendor.example/checksum.txt"
|
||||
);
|
||||
expect(message).not.toContain('secret-value');
|
||||
expect(message).not.toContain('token=');
|
||||
expect(message).not.toContain('#private');
|
||||
});
|
||||
|
||||
it('surfaces file read errors', async () => {
|
||||
await expect(
|
||||
verifyChecksum(
|
||||
'/missing/archive',
|
||||
{algorithm: 'sha256', value: 'a'.repeat(64)},
|
||||
{distribution: 'Test', version: '17'}
|
||||
)
|
||||
).rejects.toMatchObject({code: 'ENOENT'});
|
||||
});
|
||||
});
|
||||
@@ -357,6 +357,14 @@ describe('findPackageForDownload', () => {
|
||||
distribution['getAvailableVersions'] = async () => manifestData as any;
|
||||
const resolvedVersion = await distribution['findPackageForDownload'](input);
|
||||
expect(resolvedVersion.version).toBe(expected);
|
||||
const vendorPackage = (manifestData as any[]).find(
|
||||
item => item.version_data.semver === expected
|
||||
).binaries[0].package;
|
||||
expect(resolvedVersion.checksum).toEqual({
|
||||
algorithm: 'sha256',
|
||||
value: vendorPackage.checksum,
|
||||
source: vendorPackage.checksum_link
|
||||
});
|
||||
});
|
||||
|
||||
it('version is found but binaries list is empty', async () => {
|
||||
|
||||
@@ -16,6 +16,9 @@ import type {
|
||||
|
||||
import path from 'path';
|
||||
import * as semver from 'semver';
|
||||
import fs from 'fs';
|
||||
import {createHash} from 'crypto';
|
||||
import {HttpClient} from '@actions/http-client';
|
||||
|
||||
import os from 'os';
|
||||
|
||||
@@ -117,6 +120,17 @@ class EmptyJavaBase extends JavaBase {
|
||||
url: `some/random_url/java/${availableVersion}`
|
||||
};
|
||||
}
|
||||
|
||||
public downloadRelease(javaRelease: JavaDownloadRelease): Promise<string> {
|
||||
return this.downloadAndVerify(javaRelease);
|
||||
}
|
||||
|
||||
public fetchChecksumForTest(
|
||||
checksumUrl: string,
|
||||
algorithm: 'sha256' | 'sha512' | ('sha256' | 'sha512')[]
|
||||
) {
|
||||
return this.fetchChecksum(checksumUrl, algorithm);
|
||||
}
|
||||
}
|
||||
|
||||
describe('findInToolcache', () => {
|
||||
@@ -817,6 +831,306 @@ describe('setupJava', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('downloadAndVerify', () => {
|
||||
const options: JavaInstallerOptions = {
|
||||
version: '21',
|
||||
architecture: 'x64',
|
||||
packageType: 'jdk',
|
||||
checkLatest: false
|
||||
};
|
||||
let temporaryDirectory: string;
|
||||
let archivePath: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
temporaryDirectory = await fs.promises.mkdtemp(
|
||||
path.join(os.tmpdir(), 'setup-java-base-')
|
||||
);
|
||||
archivePath = path.join(temporaryDirectory, 'archive');
|
||||
await fs.promises.writeFile(archivePath, 'downloaded archive');
|
||||
(tc.downloadTool as jest.Mock<any>).mockResolvedValue(archivePath);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await fs.promises.rm(temporaryDirectory, {recursive: true, force: true});
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('returns a download after successful verification', async () => {
|
||||
const distribution = new EmptyJavaBase(options);
|
||||
const result = await distribution.downloadRelease({
|
||||
version: '21.0.8',
|
||||
url: 'https://vendor.example/jdk.tar.gz',
|
||||
checksum: {
|
||||
algorithm: 'sha256',
|
||||
value: createHash('sha256').update('downloaded archive').digest('hex')
|
||||
}
|
||||
});
|
||||
|
||||
expect(result).toBe(archivePath);
|
||||
expect(fs.existsSync(archivePath)).toBe(true);
|
||||
expect(core.debug).toHaveBeenCalledWith(
|
||||
'Verified sha256 checksum for Empty version 21.0.8.'
|
||||
);
|
||||
});
|
||||
|
||||
it('removes the download after verification failure', async () => {
|
||||
const distribution = new EmptyJavaBase(options);
|
||||
|
||||
await expect(
|
||||
distribution.downloadRelease({
|
||||
version: '21.0.8',
|
||||
url: 'https://vendor.example/jdk.tar.gz?token=secret',
|
||||
checksum: {algorithm: 'sha256', value: 'a'.repeat(64)}
|
||||
})
|
||||
).rejects.toThrow('Checksum verification failed for Empty version 21.0.8');
|
||||
|
||||
expect(fs.existsSync(archivePath)).toBe(false);
|
||||
});
|
||||
|
||||
it('preserves the verification error when removing the download fails', async () => {
|
||||
const distribution = new EmptyJavaBase(options);
|
||||
const cleanupError = new Error('cleanup failed');
|
||||
jest.spyOn(fs.promises, 'rm').mockRejectedValueOnce(cleanupError);
|
||||
|
||||
const result = distribution.downloadRelease({
|
||||
version: '21.0.8',
|
||||
url: 'https://vendor.example/jdk.tar.gz',
|
||||
checksum: {algorithm: 'sha256', value: 'a'.repeat(64)}
|
||||
});
|
||||
|
||||
await expect(result).rejects.toMatchObject({
|
||||
message: expect.stringContaining(
|
||||
'Failed to remove the downloaded archive after verification failure: cleanup failed'
|
||||
),
|
||||
cause: expect.objectContaining({
|
||||
message: expect.stringContaining(
|
||||
'Checksum verification failed for Empty version 21.0.8'
|
||||
)
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
it('logs when authoritative checksum metadata is unavailable', async () => {
|
||||
const distribution = new EmptyJavaBase(options);
|
||||
|
||||
await expect(
|
||||
distribution.downloadRelease({
|
||||
version: '21.0.8',
|
||||
url: 'https://vendor.example/jdk.tar.gz'
|
||||
})
|
||||
).resolves.toBe(archivePath);
|
||||
|
||||
expect(core.debug).toHaveBeenCalledWith(
|
||||
'No authoritative checksum is available for Empty version 21.0.8; skipping checksum verification.'
|
||||
);
|
||||
});
|
||||
|
||||
it.each([undefined, '', ' '])(
|
||||
'skips verification when the vendor digest is %p',
|
||||
async value => {
|
||||
const distribution = new EmptyJavaBase(options);
|
||||
|
||||
await expect(
|
||||
distribution.downloadRelease({
|
||||
version: '21.0.8',
|
||||
url: 'https://vendor.example/jdk.tar.gz',
|
||||
checksum: {
|
||||
algorithm: 'sha256',
|
||||
value
|
||||
} as JavaDownloadRelease['checksum']
|
||||
})
|
||||
).resolves.toBe(archivePath);
|
||||
|
||||
expect(core.debug).toHaveBeenCalledWith(
|
||||
'No authoritative checksum is available for Empty version 21.0.8; skipping checksum verification.'
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('fetchChecksum', () => {
|
||||
const options: JavaInstallerOptions = {
|
||||
version: '21',
|
||||
architecture: 'x64',
|
||||
packageType: 'jdk',
|
||||
checkLatest: false
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
function mockGet(statusCode: number, body: string) {
|
||||
return jest.spyOn(HttpClient.prototype, 'get').mockResolvedValue({
|
||||
message: {statusCode},
|
||||
readBody: async () => body
|
||||
} as any);
|
||||
}
|
||||
|
||||
it('parses a bare hex digest', async () => {
|
||||
const digest = 'a'.repeat(64);
|
||||
const spy = mockGet(200, digest);
|
||||
const distribution = new EmptyJavaBase(options);
|
||||
|
||||
const checksum = await distribution.fetchChecksumForTest(
|
||||
'https://vendor.example/jdk.tar.gz.sha256',
|
||||
'sha256'
|
||||
);
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
'https://vendor.example/jdk.tar.gz.sha256'
|
||||
);
|
||||
expect(checksum).toEqual({
|
||||
algorithm: 'sha256',
|
||||
value: digest,
|
||||
source: 'https://vendor.example/jdk.tar.gz.sha256'
|
||||
});
|
||||
});
|
||||
|
||||
it('parses only the first token of a GNU-style checksum file', async () => {
|
||||
const digest = 'b'.repeat(128);
|
||||
mockGet(200, `${digest} jbrsdk-21.0.3-linux-x64-b465.3.tar.gz\n`);
|
||||
const distribution = new EmptyJavaBase(options);
|
||||
|
||||
const checksum = await distribution.fetchChecksumForTest(
|
||||
'https://vendor.example/jdk.tar.gz.checksum',
|
||||
'sha512'
|
||||
);
|
||||
|
||||
expect(checksum).toEqual({
|
||||
algorithm: 'sha512',
|
||||
value: digest,
|
||||
source: 'https://vendor.example/jdk.tar.gz.checksum'
|
||||
});
|
||||
});
|
||||
|
||||
it('trims surrounding whitespace and newlines', async () => {
|
||||
const digest = 'c'.repeat(64);
|
||||
mockGet(200, `\n ${digest} \n`);
|
||||
const distribution = new EmptyJavaBase(options);
|
||||
|
||||
const checksum = await distribution.fetchChecksumForTest(
|
||||
'https://vendor.example/jdk.tar.gz.sha256',
|
||||
'sha256'
|
||||
);
|
||||
|
||||
expect(checksum.value).toBe(digest);
|
||||
});
|
||||
|
||||
it('skips verification when the sibling checksum is not published', async () => {
|
||||
mockGet(404, 'Not Found');
|
||||
const distribution = new EmptyJavaBase(options);
|
||||
|
||||
await expect(
|
||||
distribution.fetchChecksumForTest(
|
||||
'https://vendor.example/jdk.tar.gz.sha256',
|
||||
'sha256'
|
||||
)
|
||||
).resolves.toBeUndefined();
|
||||
expect(core.debug).toHaveBeenCalledWith(
|
||||
'No authoritative sha256 checksum is available for Empty from https://vendor.example/jdk.tar.gz.sha256; skipping checksum verification.'
|
||||
);
|
||||
});
|
||||
|
||||
it('surfaces unexpected HTTP failures without query parameters', async () => {
|
||||
mockGet(500, 'Server Error');
|
||||
const distribution = new EmptyJavaBase(options);
|
||||
|
||||
await expect(
|
||||
distribution.fetchChecksumForTest(
|
||||
'https://vendor.example/jdk.tar.gz.sha256?token=secret',
|
||||
'sha256'
|
||||
)
|
||||
).rejects.toThrow(
|
||||
'Failed to fetch the authoritative sha256 checksum for Empty from https://vendor.example/jdk.tar.gz.sha256 (HTTP 500).'
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects an empty successful checksum response', async () => {
|
||||
mockGet(200, ' \n');
|
||||
const distribution = new EmptyJavaBase(options);
|
||||
|
||||
await expect(
|
||||
distribution.fetchChecksumForTest(
|
||||
'https://vendor.example/jdk.tar.gz.sha256',
|
||||
'sha256'
|
||||
)
|
||||
).rejects.toThrow(
|
||||
'Received an empty authoritative sha256 checksum for Empty from https://vendor.example/jdk.tar.gz.sha256.'
|
||||
);
|
||||
});
|
||||
|
||||
describe('with a list of candidate algorithms', () => {
|
||||
it('infers sha512 when the digest is 128 hex characters', async () => {
|
||||
const digest = 'd'.repeat(128);
|
||||
mockGet(200, `${digest} jbrsdk.tar.gz\n`);
|
||||
const distribution = new EmptyJavaBase(options);
|
||||
|
||||
const checksum = await distribution.fetchChecksumForTest(
|
||||
'https://vendor.example/jbrsdk.tar.gz.checksum',
|
||||
['sha512', 'sha256']
|
||||
);
|
||||
|
||||
expect(checksum).toEqual({
|
||||
algorithm: 'sha512',
|
||||
value: digest,
|
||||
source: 'https://vendor.example/jbrsdk.tar.gz.checksum'
|
||||
});
|
||||
});
|
||||
|
||||
it('infers sha256 when the digest is 64 hex characters, even though sha512 was preferred', async () => {
|
||||
// Reproduces older JetBrains JBR builds (e.g. JBR 11), which publish a
|
||||
// SHA-256 digest at the generic `.checksum` sibling instead of SHA-512.
|
||||
const digest = 'e'.repeat(64);
|
||||
mockGet(200, `${digest} jbrsdk_nomod-11_0_16-osx-x64-b2043.64.tar.gz\n`);
|
||||
const distribution = new EmptyJavaBase(options);
|
||||
|
||||
const checksum = await distribution.fetchChecksumForTest(
|
||||
'https://vendor.example/jbrsdk_nomod-11_0_16-osx-x64-b2043.64.tar.gz.checksum',
|
||||
['sha512', 'sha256']
|
||||
);
|
||||
|
||||
expect(checksum).toEqual({
|
||||
algorithm: 'sha256',
|
||||
value: digest,
|
||||
source:
|
||||
'https://vendor.example/jbrsdk_nomod-11_0_16-osx-x64-b2043.64.tar.gz.checksum'
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to the first candidate algorithm when the digest length matches none of them', async () => {
|
||||
const digest = 'f'.repeat(40); // e.g. sha1, not supported
|
||||
mockGet(200, `${digest} jbrsdk.tar.gz\n`);
|
||||
const distribution = new EmptyJavaBase(options);
|
||||
|
||||
const checksum = await distribution.fetchChecksumForTest(
|
||||
'https://vendor.example/jbrsdk.tar.gz.checksum',
|
||||
['sha512', 'sha256']
|
||||
);
|
||||
|
||||
// No candidate algorithm matches, so the first-listed one is kept;
|
||||
// downstream verification will reject it as malformed.
|
||||
expect(checksum.algorithm).toBe('sha512');
|
||||
expect(checksum.value).toBe(digest);
|
||||
});
|
||||
|
||||
it('reports the checksum as unavailable using a combined algorithm label on 404', async () => {
|
||||
mockGet(404, 'Not Found');
|
||||
const distribution = new EmptyJavaBase(options);
|
||||
|
||||
await expect(
|
||||
distribution.fetchChecksumForTest(
|
||||
'https://vendor.example/jbrsdk.tar.gz.checksum',
|
||||
['sha512', 'sha256']
|
||||
)
|
||||
).resolves.toBeUndefined();
|
||||
expect(core.debug).toHaveBeenCalledWith(
|
||||
'No authoritative sha512 or sha256 checksum is available for Empty from https://vendor.example/jbrsdk.tar.gz.checksum; skipping checksum verification.'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeVersion', () => {
|
||||
const DummyJavaBase = JavaBase as any;
|
||||
|
||||
|
||||
@@ -202,6 +202,12 @@ describe('getAvailableVersions', () => {
|
||||
await distribution['findPackageForDownload'](version);
|
||||
expect(availableVersion).not.toBeNull();
|
||||
expect(availableVersion.url).toBe(expectedLink);
|
||||
expect(availableVersion.checksum).toEqual({
|
||||
algorithm: 'sha256',
|
||||
value: expect.stringMatching(/^[a-f0-9]{64}$/),
|
||||
source:
|
||||
'https://corretto.github.io/corretto-downloads/latest_links/indexmap_with_checksum.json'
|
||||
});
|
||||
});
|
||||
|
||||
it('with latest resolves to the newest available major version', async () => {
|
||||
|
||||
@@ -259,6 +259,10 @@ describe('getAvailableVersions', () => {
|
||||
await distribution['findPackageForDownload'](jdkVersion);
|
||||
expect(availableVersion).not.toBeNull();
|
||||
expect(availableVersion.url).toBe(expectedLink);
|
||||
expect(availableVersion.checksum).toEqual({
|
||||
algorithm: 'sha256',
|
||||
value: expect.stringMatching(/^[a-f0-9]{64}$/)
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -129,6 +129,14 @@ describe('GraalVMDistribution', () => {
|
||||
(distribution as any).http = mockHttpClient;
|
||||
(communityDistribution as any).http = mockHttpClient;
|
||||
|
||||
// Default checksum sibling response for `${url}.sha256` requests made by
|
||||
// GraalVM (Oracle) and GraalVM EA. Individual tests override this when
|
||||
// they need to assert the exact URL/digest contract.
|
||||
mockHttpClient.get.mockResolvedValue({
|
||||
message: {statusCode: 200},
|
||||
readBody: jest.fn().mockResolvedValue('a'.repeat(64))
|
||||
});
|
||||
|
||||
(util.getDownloadArchiveExtension as jest.Mock<any>).mockReturnValue(
|
||||
'tar.gz'
|
||||
);
|
||||
@@ -407,9 +415,16 @@ describe('GraalVMDistribution', () => {
|
||||
|
||||
expect(result).toEqual({
|
||||
url: 'https://download.oracle.com/graalvm/17/archive/graalvm-jdk-17.0.5_linux-x64_bin.tar.gz',
|
||||
version: '17.0.5'
|
||||
version: '17.0.5',
|
||||
checksum: {
|
||||
algorithm: 'sha256',
|
||||
value: 'a'.repeat(64),
|
||||
source:
|
||||
'https://download.oracle.com/graalvm/17/archive/graalvm-jdk-17.0.5_linux-x64_bin.tar.gz.sha256'
|
||||
}
|
||||
});
|
||||
expect(mockHttpClient.head).toHaveBeenCalledWith(result.url);
|
||||
expect(mockHttpClient.get).toHaveBeenCalledWith(`${result.url}.sha256`);
|
||||
});
|
||||
|
||||
it('should construct correct URL for major version (latest)', async () => {
|
||||
@@ -422,7 +437,13 @@ describe('GraalVMDistribution', () => {
|
||||
|
||||
expect(result).toEqual({
|
||||
url: 'https://download.oracle.com/graalvm/21/latest/graalvm-jdk-21_linux-x64_bin.tar.gz',
|
||||
version: '21'
|
||||
version: '21',
|
||||
checksum: {
|
||||
algorithm: 'sha256',
|
||||
value: 'a'.repeat(64),
|
||||
source:
|
||||
'https://download.oracle.com/graalvm/21/latest/graalvm-jdk-21_linux-x64_bin.tar.gz.sha256'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -465,7 +486,13 @@ describe('GraalVMDistribution', () => {
|
||||
|
||||
expect(result).toEqual({
|
||||
url: 'https://download.oracle.com/graalvm/25/latest/graalvm-jdk-25_linux-x64_bin.tar.gz',
|
||||
version: '25'
|
||||
version: '25',
|
||||
checksum: {
|
||||
algorithm: 'sha256',
|
||||
value: 'a'.repeat(64),
|
||||
source:
|
||||
'https://download.oracle.com/graalvm/25/latest/graalvm-jdk-25_linux-x64_bin.tar.gz.sha256'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -637,13 +664,20 @@ describe('GraalVMDistribution', () => {
|
||||
|
||||
expect(result).toEqual({
|
||||
url: 'https://example.com/download/graalvm-jdk-23_linux-x64_bin.tar.gz',
|
||||
version: '23-ea-20240716'
|
||||
version: '23-ea-20240716',
|
||||
checksum: {
|
||||
algorithm: 'sha256',
|
||||
value: 'a'.repeat(64),
|
||||
source:
|
||||
'https://example.com/download/graalvm-jdk-23_linux-x64_bin.tar.gz.sha256'
|
||||
}
|
||||
});
|
||||
|
||||
expect(mockHttpClient.getJson).toHaveBeenCalledWith(
|
||||
'https://api.github.com/repos/graalvm/oracle-graalvm-ea-builds/contents/versions/23-ea.json?ref=main',
|
||||
{Accept: 'application/json'}
|
||||
);
|
||||
expect(mockHttpClient.get).toHaveBeenCalledWith(`${result.url}.sha256`);
|
||||
});
|
||||
|
||||
it('should throw error when no latest EA version found', async () => {
|
||||
@@ -876,8 +910,15 @@ describe('GraalVMDistribution', () => {
|
||||
expect(fetchEASpy).toHaveBeenCalledWith('23-ea');
|
||||
expect(result).toEqual({
|
||||
url: 'https://example.com/download/graalvm-jdk-23_linux-x64_bin.tar.gz',
|
||||
version: '23-ea-20240716'
|
||||
version: '23-ea-20240716',
|
||||
checksum: {
|
||||
algorithm: 'sha256',
|
||||
value: 'a'.repeat(64),
|
||||
source:
|
||||
'https://example.com/download/graalvm-jdk-23_linux-x64_bin.tar.gz.sha256'
|
||||
}
|
||||
});
|
||||
expect(mockHttpClient.get).toHaveBeenCalledWith(`${result.url}.sha256`);
|
||||
|
||||
// Verify debug logging
|
||||
expect(core.debug).toHaveBeenCalledWith('Searching for EA build: 23-ea');
|
||||
@@ -976,7 +1017,13 @@ describe('GraalVMDistribution', () => {
|
||||
|
||||
expect(result).toEqual({
|
||||
url: 'https://example.com/download/graalvm-jdk-23_linux-aarch64_bin.tar.gz',
|
||||
version: '23-ea-20240716'
|
||||
version: '23-ea-20240716',
|
||||
checksum: {
|
||||
algorithm: 'sha256',
|
||||
value: 'a'.repeat(64),
|
||||
source:
|
||||
'https://example.com/download/graalvm-jdk-23_linux-aarch64_bin.tar.gz.sha256'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1151,6 +1198,80 @@ describe('GraalVMDistribution', () => {
|
||||
url: 'https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-21.0.2/graalvm-community-jdk-21.0.2_linux-x64_bin.tar.gz',
|
||||
version: '21.0.2'
|
||||
});
|
||||
// The asset had no `digest` field, so no checksum should be attached,
|
||||
// and the checksum sibling-URL fetch path (used by Oracle GraalVM)
|
||||
// must not be consulted for GraalVM Community.
|
||||
expect(result.checksum).toBeUndefined();
|
||||
expect(mockHttpClient.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('strips the `sha256:` prefix from a GitHub release asset digest', async () => {
|
||||
const digest = 'd'.repeat(64);
|
||||
mockHttpClient.getJson.mockResolvedValue({
|
||||
result: [
|
||||
{
|
||||
draft: false,
|
||||
prerelease: false,
|
||||
assets: [
|
||||
{
|
||||
name: 'graalvm-community-jdk-21.0.2_linux-x64_bin.tar.gz',
|
||||
browser_download_url:
|
||||
'https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-21.0.2/graalvm-community-jdk-21.0.2_linux-x64_bin.tar.gz',
|
||||
digest: `sha256:${digest}`
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
statusCode: 200,
|
||||
headers: {}
|
||||
});
|
||||
|
||||
const result = await (
|
||||
communityDistribution as any
|
||||
).findPackageForDownload('21.0.2');
|
||||
|
||||
expect(result.checksum).toEqual({
|
||||
algorithm: 'sha256',
|
||||
value: digest,
|
||||
source:
|
||||
'https://api.github.com/repos/graalvm/graalvm-ce-builds/releases?per_page=100'
|
||||
});
|
||||
// The digest came from the release listing itself, so no additional
|
||||
// HTTP request should be made to resolve the checksum.
|
||||
expect(mockHttpClient.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('safely skips a missing or malformed release asset digest', async () => {
|
||||
mockHttpClient.getJson.mockResolvedValue({
|
||||
result: [
|
||||
{
|
||||
draft: false,
|
||||
prerelease: false,
|
||||
assets: [
|
||||
{
|
||||
name: 'graalvm-community-jdk-21.0.2_linux-x64_bin.tar.gz',
|
||||
browser_download_url:
|
||||
'https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-21.0.2/graalvm-community-jdk-21.0.2_linux-x64_bin.tar.gz',
|
||||
digest: 'md5:not-a-sha256-digest'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
statusCode: 200,
|
||||
headers: {}
|
||||
});
|
||||
|
||||
const result = await (
|
||||
communityDistribution as any
|
||||
).findPackageForDownload('21.0.2');
|
||||
|
||||
expect(result.checksum).toBeUndefined();
|
||||
expect(mockHttpClient.get).not.toHaveBeenCalled();
|
||||
expect(core.debug).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'No authoritative sha256 digest is available for graalvm-community-jdk-21.0.2_linux-x64_bin.tar.gz'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('should resolve the latest GraalVM Community release for a major version', async () => {
|
||||
|
||||
@@ -148,6 +148,27 @@ describe('getAvailableVersions', () => {
|
||||
});
|
||||
|
||||
describe('findPackageForDownload', () => {
|
||||
let spyHttpClientGet: any;
|
||||
|
||||
const JETBRAINS_CHECKSUM = 'c'.repeat(128);
|
||||
|
||||
beforeEach(() => {
|
||||
// Every resolved release fetches `${url}.checksum` (sha512, GNU
|
||||
// `<hex> <filename>` format); stub it so tests never reach the real
|
||||
// network, except the dedicated 'version %s can be downloaded' test
|
||||
// below which intentionally exercises real HTTPS HEAD requests.
|
||||
spyHttpClientGet = jest
|
||||
.spyOn(HttpClient.prototype, 'get')
|
||||
.mockResolvedValue({
|
||||
message: {statusCode: 200},
|
||||
readBody: async () => `${JETBRAINS_CHECKSUM} jbrsdk.tar.gz\n`
|
||||
} as any);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it.each([
|
||||
['17', '17.0.11+1207.24'],
|
||||
['11.0', '11.0.16+2043.64'],
|
||||
@@ -231,4 +252,72 @@ describe('findPackageForDownload', () => {
|
||||
/No matching version found for SemVer */
|
||||
);
|
||||
});
|
||||
|
||||
it('fetches the authoritative sha512 checksum only for the resolved version', async () => {
|
||||
const distribution = new JetBrainsDistribution({
|
||||
version: '21',
|
||||
architecture: 'x64',
|
||||
packageType: 'jdk',
|
||||
checkLatest: false
|
||||
});
|
||||
distribution['getAvailableVersions'] = async () => manifestData as any;
|
||||
|
||||
const result = await distribution['findPackageForDownload']('21');
|
||||
|
||||
expect(result.checksum).toEqual({
|
||||
algorithm: 'sha512',
|
||||
value: JETBRAINS_CHECKSUM,
|
||||
source: `${result.url}.checksum`
|
||||
});
|
||||
// Only the single resolved/winning version's checksum is requested,
|
||||
// not one per candidate considered during version resolution.
|
||||
expect(spyHttpClientGet).toHaveBeenCalledWith(`${result.url}.checksum`);
|
||||
expect(spyHttpClientGet).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('parses only the first whitespace-delimited token from the GNU checksum payload', async () => {
|
||||
spyHttpClientGet.mockResolvedValue({
|
||||
message: {statusCode: 200},
|
||||
readBody: async () => `${JETBRAINS_CHECKSUM} jbrsdk-21.tar.gz\n`
|
||||
} as any);
|
||||
|
||||
const distribution = new JetBrainsDistribution({
|
||||
version: '21',
|
||||
architecture: 'x64',
|
||||
packageType: 'jdk',
|
||||
checkLatest: false
|
||||
});
|
||||
distribution['getAvailableVersions'] = async () => manifestData as any;
|
||||
|
||||
const result = await distribution['findPackageForDownload']('21');
|
||||
|
||||
expect(result.checksum?.value).toBe(JETBRAINS_CHECKSUM);
|
||||
});
|
||||
|
||||
it('falls back to a sha256 checksum for older JBR builds that only publish one', async () => {
|
||||
// Older JBR 11 builds (e.g. jbrsdk_nomod-11_0_16-*-b2043.64.tar.gz) publish
|
||||
// a SHA-256 digest at the generic `.checksum` sibling instead of SHA-512.
|
||||
const sha256Checksum = 'a'.repeat(64);
|
||||
spyHttpClientGet.mockResolvedValue({
|
||||
message: {statusCode: 200},
|
||||
readBody: async () =>
|
||||
`${sha256Checksum} jbrsdk_nomod-11_0_16-osx-x64-b2043.64.tar.gz\n`
|
||||
} as any);
|
||||
|
||||
const distribution = new JetBrainsDistribution({
|
||||
version: '21',
|
||||
architecture: 'x64',
|
||||
packageType: 'jdk',
|
||||
checkLatest: false
|
||||
});
|
||||
distribution['getAvailableVersions'] = async () => manifestData as any;
|
||||
|
||||
const result = await distribution['findPackageForDownload']('21');
|
||||
|
||||
expect(result.checksum).toEqual({
|
||||
algorithm: 'sha256',
|
||||
value: sha256Checksum,
|
||||
source: `${result.url}.checksum`
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -216,6 +216,15 @@ describe('Check findPackageForDownload', () => {
|
||||
await distribution['findPackageForDownload'](version);
|
||||
expect(availableRelease).not.toBeNull();
|
||||
expect(availableRelease.url).toBe(expectedUrl);
|
||||
if (availableRelease.checksum) {
|
||||
expect(availableRelease.checksum).toEqual({
|
||||
algorithm: 'sha256',
|
||||
value: expect.stringMatching(/^[a-f0-9]{64}$/),
|
||||
source: 'https://tencent.github.io/konajdk/releases/kona-v1.json'
|
||||
});
|
||||
} else {
|
||||
expect(version).toBe('8.0.20');
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -103,9 +103,12 @@ const util = await import('../../src/util.js');
|
||||
describe('findPackageForDownload', () => {
|
||||
let distribution: InstanceType<typeof MicrosoftDistributions>;
|
||||
let spyGetManifestFromRepo: any;
|
||||
let spyHttpClientGet: any;
|
||||
let spyDebug: any;
|
||||
let spyCoreError: any;
|
||||
|
||||
const MICROSOFT_CHECKSUM = 'b'.repeat(64);
|
||||
|
||||
beforeEach(() => {
|
||||
mockOsArch.mockReturnValue('x64');
|
||||
mockOsPlatform.mockReturnValue(process.platform);
|
||||
@@ -124,6 +127,15 @@ describe('findPackageForDownload', () => {
|
||||
headers: {}
|
||||
});
|
||||
|
||||
// Every resolved release fetches `${download_url}.sha256sum.txt`; stub
|
||||
// it with a GNU-style `<hex> <filename>` payload so tests never reach
|
||||
// the real network.
|
||||
spyHttpClientGet = jest.spyOn(HttpClient.prototype, 'get');
|
||||
spyHttpClientGet.mockResolvedValue({
|
||||
message: {statusCode: 200},
|
||||
readBody: async () => `${MICROSOFT_CHECKSUM} microsoft-jdk.tar.gz\n`
|
||||
});
|
||||
|
||||
spyDebug = core.debug as jest.Mock;
|
||||
spyDebug.mockImplementation(() => {});
|
||||
|
||||
@@ -311,6 +323,34 @@ describe('findPackageForDownload', () => {
|
||||
'https://example.test/jdk.tar.gz.custom.sig'
|
||||
);
|
||||
});
|
||||
|
||||
it('fetches the authoritative sha256 checksum from the GNU-style sibling file', async () => {
|
||||
mockOsPlatform.mockReturnValue(process.platform);
|
||||
|
||||
const result = await distribution['findPackageForDownload']('17.0.7');
|
||||
|
||||
expect(result.checksum).toEqual({
|
||||
algorithm: 'sha256',
|
||||
value: MICROSOFT_CHECKSUM,
|
||||
source: `${result.url}.sha256sum.txt`
|
||||
});
|
||||
expect(spyHttpClientGet).toHaveBeenCalledWith(
|
||||
`${result.url}.sha256sum.txt`
|
||||
);
|
||||
expect(spyHttpClientGet).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('parses only the first whitespace-delimited token from the GNU checksum payload', async () => {
|
||||
spyHttpClientGet.mockResolvedValue({
|
||||
message: {statusCode: 200},
|
||||
readBody: async () =>
|
||||
`${MICROSOFT_CHECKSUM} microsoft-jdk-17.0.7-linux-x64.tar.gz\n`
|
||||
});
|
||||
|
||||
const result = await distribution['findPackageForDownload']('17.0.7');
|
||||
|
||||
expect(result.checksum?.value).toBe(MICROSOFT_CHECKSUM);
|
||||
});
|
||||
});
|
||||
|
||||
describe('downloadTool', () => {
|
||||
|
||||
@@ -50,6 +50,14 @@ const archivePage = `
|
||||
<th>9.0.4 (build 9.0.4+11)</th>
|
||||
<a href="https://download.java.net/java/GA/jdk9/9.0.4/binaries/openjdk-9.0.4_linux-x64_bin.tar.gz">tar.gz</a>
|
||||
`;
|
||||
const GA_CHECKSUM = 'c'.repeat(64);
|
||||
const EA_CHECKSUM = 'd'.repeat(64);
|
||||
const checksumPages: Record<string, string> = {
|
||||
'https://download.java.net/java/GA/jdk26.0.2/hash/10/GPL/openjdk-26.0.2_linux-x64_bin.tar.gz.sha256':
|
||||
GA_CHECKSUM,
|
||||
'https://download.java.net/java/early_access/jdk27/32/GPL/openjdk-27-ea+32_linux-x64_bin.tar.gz.sha256':
|
||||
EA_CHECKSUM
|
||||
};
|
||||
|
||||
function createDistribution(
|
||||
version = '26',
|
||||
@@ -82,8 +90,16 @@ describe('OpenJdkDistribution', () => {
|
||||
'https://jdk.java.net/27/': earlyAccessPage,
|
||||
'https://jdk.java.net/archive/': archivePage
|
||||
};
|
||||
if (url in pages) {
|
||||
return {
|
||||
message: {statusCode: 200},
|
||||
readBody: async () => pages[url]
|
||||
} as Awaited<ReturnType<HttpClient['get']>>;
|
||||
}
|
||||
// Any other GET is a `${archiveUrl}.sha256` checksum sibling request.
|
||||
return {
|
||||
readBody: async () => pages[url] ?? ''
|
||||
message: {statusCode: 200},
|
||||
readBody: async () => checksumPages[url] ?? 'e'.repeat(64)
|
||||
} as Awaited<ReturnType<HttpClient['get']>>;
|
||||
});
|
||||
});
|
||||
@@ -97,8 +113,15 @@ describe('OpenJdkDistribution', () => {
|
||||
|
||||
expect(result).toEqual({
|
||||
version: '26.0.2+10',
|
||||
url: 'https://download.java.net/java/GA/jdk26.0.2/hash/10/GPL/openjdk-26.0.2_linux-x64_bin.tar.gz'
|
||||
url: 'https://download.java.net/java/GA/jdk26.0.2/hash/10/GPL/openjdk-26.0.2_linux-x64_bin.tar.gz',
|
||||
checksum: {
|
||||
algorithm: 'sha256',
|
||||
value: GA_CHECKSUM,
|
||||
source:
|
||||
'https://download.java.net/java/GA/jdk26.0.2/hash/10/GPL/openjdk-26.0.2_linux-x64_bin.tar.gz.sha256'
|
||||
}
|
||||
});
|
||||
expect(getSpy).toHaveBeenCalledWith(`${result.url}.sha256`);
|
||||
});
|
||||
|
||||
it('resolves an archived GA release', async () => {
|
||||
@@ -144,9 +167,16 @@ describe('OpenJdkDistribution', () => {
|
||||
|
||||
expect(result).toEqual({
|
||||
version: '27.0.0+32',
|
||||
url: 'https://download.java.net/java/early_access/jdk27/32/GPL/openjdk-27-ea+32_linux-x64_bin.tar.gz'
|
||||
url: 'https://download.java.net/java/early_access/jdk27/32/GPL/openjdk-27-ea+32_linux-x64_bin.tar.gz',
|
||||
checksum: {
|
||||
algorithm: 'sha256',
|
||||
value: EA_CHECKSUM,
|
||||
source:
|
||||
'https://download.java.net/java/early_access/jdk27/32/GPL/openjdk-27-ea+32_linux-x64_bin.tar.gz.sha256'
|
||||
}
|
||||
});
|
||||
expect(getSpy).not.toHaveBeenCalledWith('https://jdk.java.net/archive/');
|
||||
expect(getSpy).toHaveBeenCalledWith(`${result.url}.sha256`);
|
||||
});
|
||||
|
||||
it('reports available versions when no release matches', async () => {
|
||||
|
||||
@@ -47,8 +47,11 @@ describe('findPackageForDownload', () => {
|
||||
let distribution: InstanceType<typeof OracleDistribution>;
|
||||
let spyDebug: any;
|
||||
let spyHttpClient: any;
|
||||
let spyHttpClientGet: any;
|
||||
let spyCoreError: any;
|
||||
|
||||
const ORACLE_CHECKSUM = 'f'.repeat(64);
|
||||
|
||||
beforeEach(() => {
|
||||
distribution = new OracleDistribution({
|
||||
version: '',
|
||||
@@ -63,6 +66,14 @@ describe('findPackageForDownload', () => {
|
||||
// Mock core.error to suppress error logs
|
||||
spyCoreError = core.error as jest.Mock;
|
||||
spyCoreError.mockImplementation(() => {});
|
||||
|
||||
// Every resolved release fetches its `${url}.sha256` sibling checksum;
|
||||
// stub it so tests never reach the real network.
|
||||
spyHttpClientGet = jest.spyOn(HttpClient.prototype, 'get');
|
||||
spyHttpClientGet.mockResolvedValue({
|
||||
message: {statusCode: 200},
|
||||
readBody: async () => ORACLE_CHECKSUM
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -133,6 +144,23 @@ describe('findPackageForDownload', () => {
|
||||
expect(result.url).toBe(url);
|
||||
});
|
||||
|
||||
it('fetches the authoritative sha256 checksum for the resolved archive', async () => {
|
||||
spyHttpClient = jest.spyOn(HttpClient.prototype, 'head');
|
||||
spyHttpClient.mockResolvedValue({message: {statusCode: 200}});
|
||||
|
||||
const result = await distribution['findPackageForDownload']('21');
|
||||
|
||||
jest.restoreAllMocks();
|
||||
|
||||
expect(result.checksum).toEqual({
|
||||
algorithm: 'sha256',
|
||||
value: ORACLE_CHECKSUM,
|
||||
source: `${result.url}.sha256`
|
||||
});
|
||||
expect(spyHttpClientGet).toHaveBeenCalledWith(`${result.url}.sha256`);
|
||||
expect(spyHttpClientGet).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['amd64', 'x64'],
|
||||
['arm64', 'aarch64']
|
||||
@@ -196,6 +224,10 @@ describe('findPackageForDownload with latest', () => {
|
||||
it('resolves the newest major version from the Adoptium API', async () => {
|
||||
spyHttpClientHead = jest.spyOn(HttpClient.prototype, 'head');
|
||||
spyHttpClientHead.mockResolvedValue({message: {statusCode: 200}});
|
||||
jest.spyOn(HttpClient.prototype, 'get').mockResolvedValue({
|
||||
message: {statusCode: 200},
|
||||
readBody: async () => 'f'.repeat(64)
|
||||
} as any);
|
||||
|
||||
const distribution = new OracleDistribution({
|
||||
version: 'latest',
|
||||
|
||||
@@ -52,8 +52,10 @@ const utils = await import('../../src/util.js');
|
||||
|
||||
describe('getAvailableVersions', () => {
|
||||
let spyHttpClient: any;
|
||||
let spyHttpGet: any;
|
||||
let spyUtilGetDownloadArchiveExtension: any;
|
||||
let spyCoreError: any;
|
||||
const archiveChecksum = 'f'.repeat(64);
|
||||
|
||||
beforeEach(() => {
|
||||
spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson');
|
||||
@@ -62,6 +64,11 @@ describe('getAvailableVersions', () => {
|
||||
headers: {},
|
||||
result: manifestData
|
||||
});
|
||||
spyHttpGet = jest.spyOn(HttpClient.prototype, 'get');
|
||||
spyHttpGet.mockResolvedValue({
|
||||
message: {statusCode: 200},
|
||||
readBody: async () => `${archiveChecksum} archive`
|
||||
});
|
||||
|
||||
spyUtilGetDownloadArchiveExtension =
|
||||
utils.getDownloadArchiveExtension as jest.Mock<any>;
|
||||
@@ -282,9 +289,31 @@ describe('getAvailableVersions', () => {
|
||||
await distribution['findPackageForDownload'](normalizedVersion);
|
||||
expect(availableVersion).not.toBeNull();
|
||||
expect(availableVersion.url).toBe(expectedLink);
|
||||
expect(availableVersion.checksum).toEqual({
|
||||
algorithm: 'sha256',
|
||||
value: archiveChecksum,
|
||||
source: expectedLink.replace(/\.(?:tar\.gz|zip)$/, '.sha256.txt')
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
it('uses the checksum published beside the selected EA archive', async () => {
|
||||
const distribution = new SapMachineDistribution({
|
||||
version: '21-ea',
|
||||
architecture: 'x64',
|
||||
packageType: 'jdk',
|
||||
checkLatest: false
|
||||
});
|
||||
mockPlatform(distribution, 'linux');
|
||||
|
||||
const release = await distribution['findPackageForDownload']('21');
|
||||
|
||||
expect(spyHttpGet).toHaveBeenCalledWith(
|
||||
release.url.replace(/\.(?:tar\.gz|zip)$/, '.sha256.txt')
|
||||
);
|
||||
expect(release.checksum?.value).toBe(archiveChecksum);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['8', 'linux', 'x64'],
|
||||
['8', 'macos', 'aarch64'],
|
||||
|
||||
@@ -208,6 +208,14 @@ describe('findPackageForDownload', () => {
|
||||
distribution['getAvailableVersions'] = async () => manifestData as any;
|
||||
const resolvedVersion = await distribution['findPackageForDownload'](input);
|
||||
expect(resolvedVersion.version).toBe(expected);
|
||||
const vendorPackage = (manifestData as any[]).find(
|
||||
item => item.version_data.semver === expected
|
||||
).binaries[0].package;
|
||||
expect(resolvedVersion.checksum).toEqual({
|
||||
algorithm: 'sha256',
|
||||
value: vendorPackage.checksum,
|
||||
source: vendorPackage.checksum_link
|
||||
});
|
||||
});
|
||||
|
||||
it('version is found but binaries list is empty', async () => {
|
||||
|
||||
@@ -347,6 +347,14 @@ describe('findPackageForDownload', () => {
|
||||
const resolvedVersion = await distribution['findPackageForDownload'](input);
|
||||
expect(resolvedVersion.version).toBe(expected);
|
||||
expect(resolvedVersion.signatureUrl).toBeDefined();
|
||||
const vendorPackage = (manifestData as any[]).find(
|
||||
item => item.version_data.semver === expected
|
||||
).binaries[0].package;
|
||||
expect(resolvedVersion.checksum).toEqual({
|
||||
algorithm: 'sha256',
|
||||
value: vendorPackage.checksum,
|
||||
source: vendorPackage.checksum_link
|
||||
});
|
||||
});
|
||||
|
||||
it('version "latest" is normalized to the newest available version', async () => {
|
||||
|
||||
@@ -241,6 +241,26 @@ describe('getArchitectureOptions', () => {
|
||||
});
|
||||
|
||||
describe('findPackageForDownload', () => {
|
||||
let spyPackageDetails: any;
|
||||
|
||||
const ZULU_CHECKSUM = 'a'.repeat(64);
|
||||
|
||||
beforeEach(() => {
|
||||
// The resolved winning package fetches sha256_hash from the Azul
|
||||
// package-details endpoint; stub it so tests never reach the real
|
||||
// network.
|
||||
spyPackageDetails = jest.spyOn(HttpClient.prototype, 'getJson');
|
||||
spyPackageDetails.mockResolvedValue({
|
||||
statusCode: 200,
|
||||
headers: {},
|
||||
result: {sha256_hash: ZULU_CHECKSUM}
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it.each([
|
||||
['8', '8.0.282+8'],
|
||||
['11.x', '11.0.10+9'],
|
||||
@@ -279,6 +299,38 @@ describe('findPackageForDownload', () => {
|
||||
expect(result.url).toBe(
|
||||
'https://cdn.azul.com/zulu/bin/zulu11.35.15-ca-jdk11.0.5-macosx_x64.tar.gz'
|
||||
);
|
||||
expect(result.checksum).toEqual({
|
||||
algorithm: 'sha256',
|
||||
value: ZULU_CHECKSUM,
|
||||
source: 'https://api.azul.com/metadata/v1/zulu/packages/test-uuid-10933'
|
||||
});
|
||||
// Only the winning package's UUID triggers a details request.
|
||||
expect(spyPackageDetails).toHaveBeenCalledWith(
|
||||
'https://api.azul.com/metadata/v1/zulu/packages/test-uuid-10933'
|
||||
);
|
||||
expect(spyPackageDetails).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('skips checksum verification when sha256_hash is missing or malformed', async () => {
|
||||
spyPackageDetails.mockResolvedValue({
|
||||
statusCode: 200,
|
||||
headers: {},
|
||||
result: {sha256_hash: 'not-a-valid-digest'}
|
||||
});
|
||||
|
||||
const distribution = new ZuluDistribution({
|
||||
version: '',
|
||||
architecture: 'x86',
|
||||
packageType: 'jdk',
|
||||
checkLatest: false
|
||||
});
|
||||
distribution['getAvailableVersions'] = async () => manifestData;
|
||||
const result = await distribution['findPackageForDownload']('11.0.5');
|
||||
|
||||
expect(result.checksum).toBeUndefined();
|
||||
expect(core.debug).toHaveBeenCalledWith(
|
||||
expect.stringContaining('No authoritative sha256 checksum')
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error', async () => {
|
||||
|
||||
@@ -245,6 +245,26 @@ describe('getArchitectureOptions', () => {
|
||||
});
|
||||
|
||||
describe('findPackageForDownload', () => {
|
||||
let spyPackageDetails: any;
|
||||
|
||||
const ZULU_CHECKSUM = 'a'.repeat(64);
|
||||
|
||||
beforeEach(() => {
|
||||
// The resolved winning package fetches sha256_hash from the Azul
|
||||
// package-details endpoint; stub it so tests never reach the real
|
||||
// network.
|
||||
spyPackageDetails = jest.spyOn(HttpClient.prototype, 'getJson');
|
||||
spyPackageDetails.mockResolvedValue({
|
||||
statusCode: 200,
|
||||
headers: {},
|
||||
result: {sha256_hash: ZULU_CHECKSUM}
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it.each([
|
||||
['8', '8.0.282+8'],
|
||||
['11.x', '11.0.10+9'],
|
||||
@@ -283,6 +303,38 @@ describe('findPackageForDownload', () => {
|
||||
expect(result.url).toBe(
|
||||
'https://cdn.azul.com/zulu/bin/zulu21.32.17-ca-jdk21.0.2-linux_aarch64.tar.gz'
|
||||
);
|
||||
expect(result.checksum).toEqual({
|
||||
algorithm: 'sha256',
|
||||
value: ZULU_CHECKSUM,
|
||||
source: 'https://api.azul.com/metadata/v1/zulu/packages/test-uuid-12447'
|
||||
});
|
||||
// Only the winning package's UUID triggers a details request.
|
||||
expect(spyPackageDetails).toHaveBeenCalledWith(
|
||||
'https://api.azul.com/metadata/v1/zulu/packages/test-uuid-12447'
|
||||
);
|
||||
expect(spyPackageDetails).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('skips checksum verification when sha256_hash is missing or malformed', async () => {
|
||||
spyPackageDetails.mockResolvedValue({
|
||||
statusCode: 200,
|
||||
headers: {},
|
||||
result: {}
|
||||
});
|
||||
|
||||
const distribution = new ZuluDistribution({
|
||||
version: '',
|
||||
architecture: 'arm64',
|
||||
packageType: 'jdk',
|
||||
checkLatest: false
|
||||
});
|
||||
distribution['getAvailableVersions'] = async () => manifestData;
|
||||
const result = await distribution['findPackageForDownload']('21.0.2');
|
||||
|
||||
expect(result.checksum).toBeUndefined();
|
||||
expect(core.debug).toHaveBeenCalledWith(
|
||||
expect.stringContaining('No authoritative sha256 checksum')
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error', async () => {
|
||||
|
||||
@@ -242,6 +242,26 @@ describe('getArchitectureOptions', () => {
|
||||
});
|
||||
|
||||
describe('findPackageForDownload', () => {
|
||||
let spyPackageDetails: any;
|
||||
|
||||
const ZULU_CHECKSUM = 'a'.repeat(64);
|
||||
|
||||
beforeEach(() => {
|
||||
// The resolved winning package fetches sha256_hash from the Azul
|
||||
// package-details endpoint; stub it so tests never reach the real
|
||||
// network.
|
||||
spyPackageDetails = jest.spyOn(HttpClient.prototype, 'getJson');
|
||||
spyPackageDetails.mockResolvedValue({
|
||||
statusCode: 200,
|
||||
headers: {},
|
||||
result: {sha256_hash: ZULU_CHECKSUM}
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it.each([
|
||||
['8', '8.0.282+8'],
|
||||
['11.x', '11.0.10+9'],
|
||||
@@ -280,6 +300,38 @@ describe('findPackageForDownload', () => {
|
||||
expect(result.url).toBe(
|
||||
'https://cdn.azul.com/zulu/bin/zulu17.48.15-ca-jdk17.0.10-windows_aarch64.zip'
|
||||
);
|
||||
expect(result.checksum).toEqual({
|
||||
algorithm: 'sha256',
|
||||
value: ZULU_CHECKSUM,
|
||||
source: 'https://api.azul.com/metadata/v1/zulu/packages/test-uuid-12446'
|
||||
});
|
||||
// Only the winning package's UUID triggers a details request.
|
||||
expect(spyPackageDetails).toHaveBeenCalledWith(
|
||||
'https://api.azul.com/metadata/v1/zulu/packages/test-uuid-12446'
|
||||
);
|
||||
expect(spyPackageDetails).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('skips checksum verification when sha256_hash is missing or malformed', async () => {
|
||||
spyPackageDetails.mockResolvedValue({
|
||||
statusCode: 200,
|
||||
headers: {},
|
||||
result: {sha256_hash: '123'}
|
||||
});
|
||||
|
||||
const distribution = new ZuluDistribution({
|
||||
version: '',
|
||||
architecture: 'arm64',
|
||||
packageType: 'jdk',
|
||||
checkLatest: false
|
||||
});
|
||||
distribution['getAvailableVersions'] = async () => manifestData;
|
||||
const result = await distribution['findPackageForDownload']('17.0.10');
|
||||
|
||||
expect(result.checksum).toBeUndefined();
|
||||
expect(core.debug).toHaveBeenCalledWith(
|
||||
expect.stringContaining('No authoritative sha256 checksum')
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error', async () => {
|
||||
|
||||
Reference in New Issue
Block a user