Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 193 additions & 0 deletions src/host-identity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import * as fs from 'fs';
import { getSafeHostUid, getSafeHostGid, getRealUserHome, ACT_PRESET_BASE_IMAGE } from './host-identity';

jest.mock('fs');

const mockFs = fs as jest.Mocked<typeof fs>;

describe('ACT_PRESET_BASE_IMAGE', () => {
it('is a non-empty string pointing to an Ubuntu act image', () => {
expect(typeof ACT_PRESET_BASE_IMAGE).toBe('string');
expect(ACT_PRESET_BASE_IMAGE).toContain('ubuntu');
expect(ACT_PRESET_BASE_IMAGE).toContain('act');
});
});

describe('getSafeHostUid', () => {
const originalGetuid = process.getuid;
const originalEnv = process.env;

beforeEach(() => {
jest.resetAllMocks();
process.env = { ...originalEnv };
delete process.env.SUDO_UID;
});

afterEach(() => {
Object.defineProperty(process, 'getuid', { value: originalGetuid, configurable: true });
process.env = originalEnv;
});

it('returns the current UID when it is a regular user (≥1000)', () => {
Object.defineProperty(process, 'getuid', { value: () => 1500, configurable: true });
expect(getSafeHostUid()).toBe('1500');
});

it('returns "1000" when UID is 0 (root) and no SUDO_UID', () => {
Object.defineProperty(process, 'getuid', { value: () => 0, configurable: true });
expect(getSafeHostUid()).toBe('1000');
});

it('uses SUDO_UID when running as root', () => {
Object.defineProperty(process, 'getuid', { value: () => 0, configurable: true });
process.env.SUDO_UID = '1234';
expect(getSafeHostUid()).toBe('1234');
});

it('returns "1000" when SUDO_UID is an invalid number', () => {
Object.defineProperty(process, 'getuid', { value: () => 0, configurable: true });
process.env.SUDO_UID = 'invalid';
expect(getSafeHostUid()).toBe('1000');
});

it('clamps SUDO_UID below 1000 to "1000"', () => {
Object.defineProperty(process, 'getuid', { value: () => 0, configurable: true });
process.env.SUDO_UID = '500';
expect(getSafeHostUid()).toBe('1000');
});

it('clamps system UID (e.g. 999) to "1000"', () => {
Object.defineProperty(process, 'getuid', { value: () => 999, configurable: true });
expect(getSafeHostUid()).toBe('1000');
});

it('returns "1000" when getuid is not available', () => {
Object.defineProperty(process, 'getuid', { value: undefined, configurable: true });
expect(getSafeHostUid()).toBe('1000');
});
});

describe('getSafeHostGid', () => {
const originalGetgid = process.getgid;
const originalEnv = process.env;

beforeEach(() => {
jest.resetAllMocks();
process.env = { ...originalEnv };
delete process.env.SUDO_GID;
});

afterEach(() => {
Object.defineProperty(process, 'getgid', { value: originalGetgid, configurable: true });
process.env = originalEnv;
});

it('returns the current GID when it is a regular group (≥1000)', () => {
Object.defineProperty(process, 'getgid', { value: () => 2000, configurable: true });
expect(getSafeHostGid()).toBe('2000');
});

it('returns "1000" when GID is 0 and no SUDO_GID', () => {
Object.defineProperty(process, 'getgid', { value: () => 0, configurable: true });
expect(getSafeHostGid()).toBe('1000');
});

it('uses SUDO_GID when running as root', () => {
Object.defineProperty(process, 'getgid', { value: () => 0, configurable: true });
process.env.SUDO_GID = '1234';
expect(getSafeHostGid()).toBe('1234');
});

it('returns "1000" when SUDO_GID is an invalid number', () => {
Object.defineProperty(process, 'getgid', { value: () => 0, configurable: true });
process.env.SUDO_GID = 'not-a-number';
expect(getSafeHostGid()).toBe('1000');
});

it('clamps SUDO_GID in system range to "1000"', () => {
Object.defineProperty(process, 'getgid', { value: () => 0, configurable: true });
process.env.SUDO_GID = '100';
expect(getSafeHostGid()).toBe('1000');
});

it('returns "1000" when getgid is not available', () => {
Object.defineProperty(process, 'getgid', { value: undefined, configurable: true });
expect(getSafeHostGid()).toBe('1000');
});
});

describe('getRealUserHome', () => {
const originalGetuid = process.getuid;
const originalEnv = process.env;

beforeEach(() => {
jest.resetAllMocks();
process.env = { ...originalEnv };
delete process.env.SUDO_USER;
delete process.env.HOME;
});

afterEach(() => {
Object.defineProperty(process, 'getuid', { value: originalGetuid, configurable: true });
process.env = originalEnv;
});

it('returns HOME when running as a regular user', () => {
Object.defineProperty(process, 'getuid', { value: () => 1000, configurable: true });
process.env.HOME = '/home/myuser';
expect(getRealUserHome()).toBe('/home/myuser');
});

it('falls back to /root when HOME is not set', () => {
Object.defineProperty(process, 'getuid', { value: () => 1000, configurable: true });
expect(getRealUserHome()).toBe('/root');
});

it('uses SUDO_USER to look up home from /etc/passwd', () => {
Object.defineProperty(process, 'getuid', { value: () => 0, configurable: true });
process.env.SUDO_USER = 'alice';
process.env.HOME = '/root';
mockFs.readFileSync.mockReturnValue('root:x:0:0:root:/root:/bin/bash\nalice:x:1000:1000:Alice:/home/alice:/bin/bash\n');
expect(getRealUserHome()).toBe('/home/alice');
});

it('falls back to HOME when SUDO_USER is not found in /etc/passwd', () => {
Object.defineProperty(process, 'getuid', { value: () => 0, configurable: true });
process.env.SUDO_USER = 'bob';
process.env.HOME = '/root';
mockFs.readFileSync.mockReturnValue('root:x:0:0:root:/root:/bin/bash\n');
expect(getRealUserHome()).toBe('/root');
});

it('falls back to HOME when /etc/passwd is unreadable', () => {
Object.defineProperty(process, 'getuid', { value: () => 0, configurable: true });
process.env.SUDO_USER = 'alice';
process.env.HOME = '/root';
mockFs.readFileSync.mockImplementation(() => { throw new Error('EACCES'); });
expect(getRealUserHome()).toBe('/root');
});

it('falls back to HOME when SUDO_USER is not set but running as root', () => {
Object.defineProperty(process, 'getuid', { value: () => 0, configurable: true });
process.env.HOME = '/root';
expect(getRealUserHome()).toBe('/root');
});

it('returns /root when HOME is not set and running as root without SUDO_USER', () => {
Object.defineProperty(process, 'getuid', { value: () => 0, configurable: true });
expect(getRealUserHome()).toBe('/root');
});

it('falls back to HOME when passwd line has fewer than 6 fields', () => {
Object.defineProperty(process, 'getuid', { value: () => 0, configurable: true });
process.env.SUDO_USER = 'alice';
process.env.HOME = '/home/fallback';
mockFs.readFileSync.mockReturnValue('alice:x:1000:1000\n');
expect(getRealUserHome()).toBe('/home/fallback');
});

it('returns /root as ultimate fallback when no HOME env', () => {
Object.defineProperty(process, 'getuid', { value: () => 1000, configurable: true });
expect(getRealUserHome()).toBe('/root');
});
});
129 changes: 129 additions & 0 deletions src/parsers/dns-parsers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { parseDnsServers, parseDnsOverHttps, processLocalhostKeyword } from './dns-parsers';

describe('parseDnsServers', () => {
it('parses a single valid IPv4 server', () => {
expect(parseDnsServers('8.8.8.8')).toEqual(['8.8.8.8']);
});

it('parses multiple IPv4 servers', () => {
expect(parseDnsServers('8.8.8.8,8.8.4.4')).toEqual(['8.8.8.8', '8.8.4.4']);
});

it('trims whitespace around server addresses', () => {
expect(parseDnsServers(' 8.8.8.8 , 1.1.1.1 ')).toEqual(['8.8.8.8', '1.1.1.1']);
});

it('throws when input is empty string', () => {
expect(() => parseDnsServers('')).toThrow('At least one DNS server must be specified');
});

it('throws when all entries are blank after trimming', () => {
expect(() => parseDnsServers(' , ')).toThrow('At least one DNS server must be specified');
});

it('throws on invalid IP address format', () => {
expect(() => parseDnsServers('not-an-ip')).toThrow('Invalid DNS server IP address: not-an-ip');
});

it('throws on hostname instead of IP', () => {
expect(() => parseDnsServers('dns.google')).toThrow('Invalid DNS server IP address: dns.google');
});

it('throws when second server is invalid', () => {
expect(() => parseDnsServers('8.8.8.8,invalid')).toThrow('Invalid DNS server IP address: invalid');
});

it('accepts a valid IPv6 loopback address', () => {
expect(parseDnsServers('::1')).toEqual(['::1']);
});

it('accepts a valid full IPv6 address', () => {
expect(parseDnsServers('2001:4860:4860::8888')).toEqual(['2001:4860:4860::8888']);
});

it('accepts mixed IPv4 and IPv6 servers', () => {
expect(parseDnsServers('8.8.8.8,::1')).toEqual(['8.8.8.8', '::1']);
});
});

describe('parseDnsOverHttps', () => {
it('returns undefined when value is undefined', () => {
expect(parseDnsOverHttps(undefined)).toBeUndefined();
});

it('returns the default Google DoH URL when value is true', () => {
const result = parseDnsOverHttps(true);
expect(result).toEqual({ url: 'https://dns.google/dns-query' });
});

it('returns a custom https URL unchanged', () => {
const result = parseDnsOverHttps('https://cloudflare-dns.com/dns-query');
expect(result).toEqual({ url: 'https://cloudflare-dns.com/dns-query' });
});

it('returns an error for an http:// URL', () => {
const result = parseDnsOverHttps('http://dns.example.com/dns-query');
expect(result).toEqual({ error: '--dns-over-https resolver URL must start with https://' });
});

it('returns an error for a bare hostname', () => {
const result = parseDnsOverHttps('dns.example.com');
expect(result).toEqual({ error: '--dns-over-https resolver URL must start with https://' });
});

it('returns an error when value is false (falsy but not undefined)', () => {
const result = parseDnsOverHttps(false as unknown as string);
expect(result).toEqual({ error: '--dns-over-https resolver URL must start with https://' });
});
Comment thread
Copilot marked this conversation as resolved.
Resolved
});

describe('processLocalhostKeyword', () => {
it('returns domains unchanged when localhost is not present', () => {
const result = processLocalhostKeyword(['github.com', 'api.github.com'], false, undefined);
expect(result.localhostDetected).toBe(false);
expect(result.shouldEnableHostAccess).toBe(false);
expect(result.allowedDomains).toEqual(['github.com', 'api.github.com']);
expect(result.defaultPorts).toBeUndefined();
});

it('replaces bare localhost with host.docker.internal', () => {
const result = processLocalhostKeyword(['localhost', 'github.com'], false, undefined);
expect(result.localhostDetected).toBe(true);
expect(result.allowedDomains).toContain('host.docker.internal');
expect(result.allowedDomains).not.toContain('localhost');
expect(result.allowedDomains).toContain('github.com');
});

it('preserves http:// protocol when replacing localhost', () => {
const result = processLocalhostKeyword(['http://localhost'], false, undefined);
expect(result.allowedDomains).toContain('http://host.docker.internal');
expect(result.allowedDomains).not.toContain('http://localhost');
});

it('preserves https:// protocol when replacing localhost', () => {
const result = processLocalhostKeyword(['https://localhost'], false, undefined);
expect(result.allowedDomains).toContain('https://host.docker.internal');
});

it('sets shouldEnableHostAccess to true when enableHostAccess is false', () => {
const result = processLocalhostKeyword(['localhost'], false, undefined);
expect(result.shouldEnableHostAccess).toBe(true);
});

it('sets shouldEnableHostAccess to false when enableHostAccess is already true', () => {
const result = processLocalhostKeyword(['localhost'], true, undefined);
expect(result.shouldEnableHostAccess).toBe(false);
});

it('sets defaultPorts when allowHostPorts is undefined', () => {
const result = processLocalhostKeyword(['localhost'], false, undefined);
expect(result.defaultPorts).toBeDefined();
expect(result.defaultPorts).toContain('3000');
expect(result.defaultPorts).toContain('8080');
});

it('sets defaultPorts to undefined when allowHostPorts is already provided', () => {
const result = processLocalhostKeyword(['localhost'], false, '8080,3000');
expect(result.defaultPorts).toBeUndefined();
});
});
Loading
Loading