2026-07-08 14:45:00 +05:30
|
|
|
import {
|
|
|
|
|
jest,
|
|
|
|
|
describe,
|
|
|
|
|
it,
|
|
|
|
|
expect,
|
|
|
|
|
beforeEach,
|
|
|
|
|
afterAll,
|
|
|
|
|
afterEach
|
|
|
|
|
} from '@jest/globals';
|
|
|
|
|
import {fileURLToPath} from 'url';
|
2026-08-05 18:05:36 +02:00
|
|
|
import * as fs from 'fs';
|
2023-03-09 14:49:35 +02:00
|
|
|
import * as path from 'path';
|
|
|
|
|
import * as io from '@actions/io';
|
2020-05-02 04:33:15 -07:00
|
|
|
|
2026-07-08 14:45:00 +05:30
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
2020-05-02 04:33:15 -07:00
|
|
|
|
2026-07-08 14:45:00 +05:30
|
|
|
jest.unstable_mockModule('@actions/exec', () => ({
|
|
|
|
|
exec: jest.fn()
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
jest.unstable_mockModule('@actions/tool-cache', () => ({
|
|
|
|
|
downloadTool: jest.fn()
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const exec = await import('@actions/exec');
|
|
|
|
|
const tc = await import('@actions/tool-cache');
|
|
|
|
|
const gpg = await import('../src/gpg.js');
|
2026-06-29 13:19:49 +01:00
|
|
|
|
2020-05-02 04:33:15 -07:00
|
|
|
const tempDir = path.join(__dirname, 'runner', 'temp');
|
|
|
|
|
process.env['RUNNER_TEMP'] = tempDir;
|
|
|
|
|
|
|
|
|
|
describe('gpg tests', () => {
|
|
|
|
|
beforeEach(async () => {
|
2026-08-05 18:05:36 +02:00
|
|
|
await io.rmRF(tempDir);
|
2020-05-02 04:33:15 -07:00
|
|
|
await io.mkdirP(tempDir);
|
2026-08-05 18:05:36 +02:00
|
|
|
jest.clearAllMocks();
|
|
|
|
|
(exec.exec as jest.Mock<any>).mockResolvedValue(0);
|
2020-07-15 19:53:39 -06:00
|
|
|
});
|
2020-05-02 04:33:15 -07:00
|
|
|
|
|
|
|
|
afterAll(async () => {
|
|
|
|
|
try {
|
|
|
|
|
await io.rmRF(tempDir);
|
|
|
|
|
} catch {
|
|
|
|
|
console.log('Failed to remove test directories');
|
|
|
|
|
}
|
2020-07-16 19:12:25 -06:00
|
|
|
});
|
2020-05-02 04:33:15 -07:00
|
|
|
|
2026-06-29 13:19:49 +01:00
|
|
|
describe('toGpgPath', () => {
|
|
|
|
|
const originalPlatform = process.platform;
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
Object.defineProperty(process, 'platform', {value: originalPlatform});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('returns path unchanged on non-Windows platforms', () => {
|
|
|
|
|
Object.defineProperty(process, 'platform', {value: 'linux'});
|
|
|
|
|
expect(gpg.toGpgPath('/tmp/some/path')).toBe('/tmp/some/path');
|
|
|
|
|
expect(gpg.toGpgPath('D:\\a\\_temp\\file')).toBe('D:\\a\\_temp\\file');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('converts Windows backslashes and drive letter to POSIX path on Windows', () => {
|
|
|
|
|
Object.defineProperty(process, 'platform', {value: 'win32'});
|
|
|
|
|
expect(gpg.toGpgPath('D:\\a\\_temp\\gpg-home')).toBe(
|
|
|
|
|
'/d/a/_temp/gpg-home'
|
|
|
|
|
);
|
|
|
|
|
expect(
|
|
|
|
|
gpg.toGpgPath('C:\\Users\\runner\\AppData\\Local\\Temp\\key.asc')
|
|
|
|
|
).toBe('/c/Users/runner/AppData/Local/Temp/key.asc');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('handles uppercase and lowercase drive letters on Windows', () => {
|
|
|
|
|
Object.defineProperty(process, 'platform', {value: 'win32'});
|
|
|
|
|
expect(gpg.toGpgPath('d:\\a\\_temp\\file')).toBe('/d/a/_temp/file');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2020-05-02 04:33:15 -07:00
|
|
|
describe('importKey', () => {
|
2026-08-05 18:05:36 +02:00
|
|
|
it('imports private keys into a unique isolated GPG home', async () => {
|
2020-05-02 04:33:15 -07:00
|
|
|
const privateKey = 'KEY CONTENTS';
|
2026-08-05 18:05:36 +02:00
|
|
|
let privateKeyFile = '';
|
|
|
|
|
(exec.exec as jest.Mock<any>).mockImplementation(
|
|
|
|
|
async (_command: string, _args: string[]) => {
|
|
|
|
|
const [createdGpgHome] = fs.readdirSync(tempDir);
|
|
|
|
|
privateKeyFile = path.join(
|
|
|
|
|
tempDir,
|
|
|
|
|
createdGpgHome,
|
|
|
|
|
fs
|
|
|
|
|
.readdirSync(path.join(tempDir, createdGpgHome))
|
|
|
|
|
.find(file => file.startsWith('private-key-')) ?? ''
|
|
|
|
|
);
|
|
|
|
|
expect(fs.readFileSync(privateKeyFile, 'utf8')).toBe(privateKey);
|
|
|
|
|
if (process.platform !== 'win32') {
|
|
|
|
|
expect(fs.statSync(privateKeyFile).mode & 0o777).toBe(0o600);
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
);
|
2020-05-02 04:33:15 -07:00
|
|
|
|
2026-08-05 18:05:36 +02:00
|
|
|
const gpgHome = await gpg.importKey(privateKey);
|
2020-05-02 04:33:15 -07:00
|
|
|
|
2026-08-05 18:05:36 +02:00
|
|
|
expect(path.dirname(gpgHome)).toBe(tempDir);
|
|
|
|
|
expect(path.basename(gpgHome).startsWith(gpg.GPG_HOME_PREFIX)).toBe(true);
|
|
|
|
|
expect(fs.existsSync(gpgHome)).toBe(true);
|
|
|
|
|
expect(fs.existsSync(privateKeyFile)).toBe(false);
|
|
|
|
|
if (process.platform !== 'win32') {
|
|
|
|
|
expect(fs.statSync(gpgHome).mode & 0o777).toBe(0o700);
|
|
|
|
|
}
|
2023-03-09 14:49:35 +02:00
|
|
|
expect(exec.exec).toHaveBeenCalledWith(
|
|
|
|
|
'gpg',
|
2026-08-05 18:05:36 +02:00
|
|
|
[
|
|
|
|
|
'--homedir',
|
|
|
|
|
gpg.toGpgPath(gpgHome),
|
|
|
|
|
'--batch',
|
|
|
|
|
'--import',
|
|
|
|
|
gpg.toGpgPath(privateKeyFile)
|
|
|
|
|
],
|
|
|
|
|
{silent: true}
|
2023-03-09 14:49:35 +02:00
|
|
|
);
|
2020-05-02 04:33:15 -07:00
|
|
|
});
|
2026-08-05 18:05:36 +02:00
|
|
|
|
|
|
|
|
it('removes the private-key file and isolated home when import fails', async () => {
|
|
|
|
|
let gpgHome = '';
|
|
|
|
|
let privateKeyFile = '';
|
|
|
|
|
(exec.exec as jest.Mock<any>).mockImplementation(
|
|
|
|
|
async (_command: string, _args: string[]) => {
|
|
|
|
|
const [createdGpgHome] = fs.readdirSync(tempDir);
|
|
|
|
|
gpgHome = path.join(tempDir, createdGpgHome);
|
|
|
|
|
privateKeyFile = path.join(
|
|
|
|
|
gpgHome,
|
|
|
|
|
fs
|
|
|
|
|
.readdirSync(gpgHome)
|
|
|
|
|
.find(file => file.startsWith('private-key-')) ?? ''
|
|
|
|
|
);
|
|
|
|
|
expect(fs.existsSync(privateKeyFile)).toBe(true);
|
|
|
|
|
throw new Error('invalid key');
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await expect(gpg.importKey('INVALID KEY')).rejects.toThrow('invalid key');
|
|
|
|
|
|
|
|
|
|
expect(fs.existsSync(privateKeyFile)).toBe(false);
|
|
|
|
|
expect(fs.existsSync(gpgHome)).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('imports multi-key input without parsing or deleting fingerprints', async () => {
|
|
|
|
|
const privateKeys = 'KEY ONE\nKEY TWO';
|
|
|
|
|
(exec.exec as jest.Mock<any>).mockImplementation(
|
|
|
|
|
async (_command: string, _args: string[]) => {
|
|
|
|
|
const [createdGpgHome] = fs.readdirSync(tempDir);
|
|
|
|
|
const keyFile = fs
|
|
|
|
|
.readdirSync(path.join(tempDir, createdGpgHome))
|
|
|
|
|
.find(file => file.startsWith('private-key-'));
|
|
|
|
|
expect(
|
|
|
|
|
fs.readFileSync(
|
|
|
|
|
path.join(tempDir, createdGpgHome, keyFile ?? ''),
|
|
|
|
|
'utf8'
|
|
|
|
|
)
|
|
|
|
|
).toBe(privateKeys);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const gpgHome = await gpg.importKey(privateKeys);
|
|
|
|
|
|
|
|
|
|
expect(gpgHome).toContain(gpg.GPG_HOME_PREFIX);
|
|
|
|
|
expect(exec.exec).toHaveBeenCalledTimes(1);
|
|
|
|
|
expect((exec.exec as jest.Mock).mock.calls[0][1]).not.toContain(
|
|
|
|
|
'--delete-secret-and-public-key'
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('uses a separate GPG home for each invocation', async () => {
|
|
|
|
|
const firstGpgHome = await gpg.importKey('FIRST KEY');
|
|
|
|
|
const secondGpgHome = await gpg.importKey('SECOND KEY');
|
|
|
|
|
|
|
|
|
|
expect(firstGpgHome).not.toBe(secondGpgHome);
|
|
|
|
|
expect(fs.existsSync(firstGpgHome)).toBe(true);
|
|
|
|
|
expect(fs.existsSync(secondGpgHome)).toBe(true);
|
|
|
|
|
});
|
2020-05-02 04:33:15 -07:00
|
|
|
});
|
|
|
|
|
|
2026-08-05 18:05:36 +02:00
|
|
|
describe('removeGpgHome', () => {
|
|
|
|
|
it('removes only action-owned GPG homes and is idempotent', async () => {
|
|
|
|
|
const gpgHome = await gpg.importKey('KEY CONTENTS');
|
|
|
|
|
const unrelatedGpgHome = path.join(tempDir, 'user-gpg-home');
|
|
|
|
|
fs.mkdirSync(unrelatedGpgHome);
|
2020-05-02 04:33:15 -07:00
|
|
|
|
2026-08-05 18:05:36 +02:00
|
|
|
await gpg.removeGpgHome(gpgHome);
|
|
|
|
|
await gpg.removeGpgHome(gpgHome);
|
|
|
|
|
|
|
|
|
|
expect(exec.exec).toHaveBeenNthCalledWith(
|
|
|
|
|
2,
|
|
|
|
|
'gpgconf',
|
|
|
|
|
['--homedir', gpg.toGpgPath(gpgHome), '--kill', 'gpg-agent'],
|
|
|
|
|
{silent: true, ignoreReturnCode: true}
|
2023-03-09 14:49:35 +02:00
|
|
|
);
|
2026-08-05 18:05:36 +02:00
|
|
|
expect(exec.exec).toHaveBeenCalledTimes(2);
|
|
|
|
|
expect(fs.existsSync(gpgHome)).toBe(false);
|
|
|
|
|
expect(fs.existsSync(unrelatedGpgHome)).toBe(true);
|
2020-05-02 04:33:15 -07:00
|
|
|
});
|
2026-06-29 13:19:49 +01:00
|
|
|
|
2026-08-05 18:05:36 +02:00
|
|
|
it('removes the GPG home when gpgconf is unavailable', async () => {
|
|
|
|
|
const gpgHome = await gpg.importKey('KEY CONTENTS');
|
|
|
|
|
(exec.exec as jest.Mock<any>).mockRejectedValueOnce(
|
|
|
|
|
new Error('gpgconf not found')
|
|
|
|
|
);
|
2026-06-29 13:19:49 +01:00
|
|
|
|
2026-08-05 18:05:36 +02:00
|
|
|
await gpg.removeGpgHome(gpgHome);
|
|
|
|
|
|
|
|
|
|
expect(fs.existsSync(gpgHome)).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('refuses to remove a GPG home it does not own', async () => {
|
|
|
|
|
const unrelatedGpgHome = path.join(tempDir, 'user-gpg-home');
|
|
|
|
|
fs.mkdirSync(unrelatedGpgHome, {recursive: true});
|
|
|
|
|
|
|
|
|
|
await expect(gpg.removeGpgHome(unrelatedGpgHome)).rejects.toThrow(
|
|
|
|
|
'Refusing to remove unexpected GPG home'
|
|
|
|
|
);
|
|
|
|
|
expect(fs.existsSync(unrelatedGpgHome)).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('verifyPackageSignature', () => {
|
|
|
|
|
it('imports bundled key and verifies package', async () => {
|
|
|
|
|
const publicKeyContent =
|
|
|
|
|
'-----BEGIN PGP PUBLIC KEY BLOCK-----\ntest\n-----END PGP PUBLIC KEY BLOCK-----';
|
|
|
|
|
(tc.downloadTool as jest.Mock<any>).mockResolvedValue(
|
|
|
|
|
'/tmp/jdk.tar.gz.sig'
|
|
|
|
|
);
|
|
|
|
|
await gpg.verifyPackageSignature(
|
|
|
|
|
'/tmp/jdk.tar.gz',
|
|
|
|
|
'https://example.com/jdk.tar.gz.sig',
|
|
|
|
|
publicKeyContent
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(tc.downloadTool).toHaveBeenCalledWith(
|
|
|
|
|
'https://example.com/jdk.tar.gz.sig'
|
|
|
|
|
);
|
|
|
|
|
expect(exec.exec).toHaveBeenNthCalledWith(
|
|
|
|
|
1,
|
|
|
|
|
'gpg',
|
|
|
|
|
[
|
|
|
|
|
'--homedir',
|
|
|
|
|
expect.any(String),
|
|
|
|
|
'--batch',
|
|
|
|
|
'--import',
|
|
|
|
|
expect.stringContaining('public-key.asc')
|
|
|
|
|
],
|
|
|
|
|
expect.objectContaining({silent: true})
|
|
|
|
|
);
|
|
|
|
|
expect(exec.exec).toHaveBeenNthCalledWith(
|
|
|
|
|
2,
|
|
|
|
|
'gpg',
|
|
|
|
|
[
|
|
|
|
|
'--homedir',
|
|
|
|
|
expect.any(String),
|
|
|
|
|
'--batch',
|
|
|
|
|
'--verify',
|
|
|
|
|
'/tmp/jdk.tar.gz.sig',
|
|
|
|
|
'/tmp/jdk.tar.gz'
|
|
|
|
|
],
|
|
|
|
|
expect.objectContaining({silent: true})
|
|
|
|
|
);
|
2026-06-29 13:19:49 +01:00
|
|
|
});
|
2020-05-02 04:33:15 -07:00
|
|
|
});
|
|
|
|
|
});
|