Skip to content

Commit 22f351a

Browse files
github-actions[bot]CopilotlpcoxCopilotCopilot
authored
[Test Coverage] Add coverage for parsers, services, host-identity (#5240)
* test: add coverage for parsers, services, host-identity Add 167 unit tests covering 9 previously untested files: parsers (dns, rate-limit, host-port, env, volume), host-identity, runner-tool-cache, services/host-path-prefix, and services/service-security. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Landon Cox <landon.cox@microsoft.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent 5d7027b commit 22f351a

9 files changed

Lines changed: 1119 additions & 0 deletions

src/host-identity.test.ts

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import * as fs from 'fs';
2+
import { getSafeHostUid, getSafeHostGid, getRealUserHome, ACT_PRESET_BASE_IMAGE } from './host-identity';
3+
4+
jest.mock('fs');
5+
6+
const mockFs = fs as jest.Mocked<typeof fs>;
7+
8+
describe('ACT_PRESET_BASE_IMAGE', () => {
9+
it('is a non-empty string pointing to an Ubuntu act image', () => {
10+
expect(typeof ACT_PRESET_BASE_IMAGE).toBe('string');
11+
expect(ACT_PRESET_BASE_IMAGE).toContain('ubuntu');
12+
expect(ACT_PRESET_BASE_IMAGE).toContain('act');
13+
});
14+
});
15+
16+
describe('getSafeHostUid', () => {
17+
const originalGetuid = process.getuid;
18+
const originalEnv = process.env;
19+
20+
beforeEach(() => {
21+
jest.resetAllMocks();
22+
process.env = { ...originalEnv };
23+
delete process.env.SUDO_UID;
24+
});
25+
26+
afterEach(() => {
27+
Object.defineProperty(process, 'getuid', { value: originalGetuid, configurable: true });
28+
process.env = originalEnv;
29+
});
30+
31+
it('returns the current UID when it is a regular user (≥1000)', () => {
32+
Object.defineProperty(process, 'getuid', { value: () => 1500, configurable: true });
33+
expect(getSafeHostUid()).toBe('1500');
34+
});
35+
36+
it('returns "1000" when UID is 0 (root) and no SUDO_UID', () => {
37+
Object.defineProperty(process, 'getuid', { value: () => 0, configurable: true });
38+
expect(getSafeHostUid()).toBe('1000');
39+
});
40+
41+
it('uses SUDO_UID when running as root', () => {
42+
Object.defineProperty(process, 'getuid', { value: () => 0, configurable: true });
43+
process.env.SUDO_UID = '1234';
44+
expect(getSafeHostUid()).toBe('1234');
45+
});
46+
47+
it('returns "1000" when SUDO_UID is an invalid number', () => {
48+
Object.defineProperty(process, 'getuid', { value: () => 0, configurable: true });
49+
process.env.SUDO_UID = 'invalid';
50+
expect(getSafeHostUid()).toBe('1000');
51+
});
52+
53+
it('clamps SUDO_UID below 1000 to "1000"', () => {
54+
Object.defineProperty(process, 'getuid', { value: () => 0, configurable: true });
55+
process.env.SUDO_UID = '500';
56+
expect(getSafeHostUid()).toBe('1000');
57+
});
58+
59+
it('clamps system UID (e.g. 999) to "1000"', () => {
60+
Object.defineProperty(process, 'getuid', { value: () => 999, configurable: true });
61+
expect(getSafeHostUid()).toBe('1000');
62+
});
63+
64+
it('returns "1000" when getuid is not available', () => {
65+
Object.defineProperty(process, 'getuid', { value: undefined, configurable: true });
66+
expect(getSafeHostUid()).toBe('1000');
67+
});
68+
});
69+
70+
describe('getSafeHostGid', () => {
71+
const originalGetgid = process.getgid;
72+
const originalEnv = process.env;
73+
74+
beforeEach(() => {
75+
jest.resetAllMocks();
76+
process.env = { ...originalEnv };
77+
delete process.env.SUDO_GID;
78+
});
79+
80+
afterEach(() => {
81+
Object.defineProperty(process, 'getgid', { value: originalGetgid, configurable: true });
82+
process.env = originalEnv;
83+
});
84+
85+
it('returns the current GID when it is a regular group (≥1000)', () => {
86+
Object.defineProperty(process, 'getgid', { value: () => 2000, configurable: true });
87+
expect(getSafeHostGid()).toBe('2000');
88+
});
89+
90+
it('returns "1000" when GID is 0 and no SUDO_GID', () => {
91+
Object.defineProperty(process, 'getgid', { value: () => 0, configurable: true });
92+
expect(getSafeHostGid()).toBe('1000');
93+
});
94+
95+
it('uses SUDO_GID when running as root', () => {
96+
Object.defineProperty(process, 'getgid', { value: () => 0, configurable: true });
97+
process.env.SUDO_GID = '1234';
98+
expect(getSafeHostGid()).toBe('1234');
99+
});
100+
101+
it('returns "1000" when SUDO_GID is an invalid number', () => {
102+
Object.defineProperty(process, 'getgid', { value: () => 0, configurable: true });
103+
process.env.SUDO_GID = 'not-a-number';
104+
expect(getSafeHostGid()).toBe('1000');
105+
});
106+
107+
it('clamps SUDO_GID in system range to "1000"', () => {
108+
Object.defineProperty(process, 'getgid', { value: () => 0, configurable: true });
109+
process.env.SUDO_GID = '100';
110+
expect(getSafeHostGid()).toBe('1000');
111+
});
112+
113+
it('returns "1000" when getgid is not available', () => {
114+
Object.defineProperty(process, 'getgid', { value: undefined, configurable: true });
115+
expect(getSafeHostGid()).toBe('1000');
116+
});
117+
});
118+
119+
describe('getRealUserHome', () => {
120+
const originalGetuid = process.getuid;
121+
const originalEnv = process.env;
122+
123+
beforeEach(() => {
124+
jest.resetAllMocks();
125+
process.env = { ...originalEnv };
126+
delete process.env.SUDO_USER;
127+
delete process.env.HOME;
128+
});
129+
130+
afterEach(() => {
131+
Object.defineProperty(process, 'getuid', { value: originalGetuid, configurable: true });
132+
process.env = originalEnv;
133+
});
134+
135+
it('returns HOME when running as a regular user', () => {
136+
Object.defineProperty(process, 'getuid', { value: () => 1000, configurable: true });
137+
process.env.HOME = '/home/myuser';
138+
expect(getRealUserHome()).toBe('/home/myuser');
139+
});
140+
141+
it('falls back to /root when HOME is not set', () => {
142+
Object.defineProperty(process, 'getuid', { value: () => 1000, configurable: true });
143+
expect(getRealUserHome()).toBe('/root');
144+
});
145+
146+
it('uses SUDO_USER to look up home from /etc/passwd', () => {
147+
Object.defineProperty(process, 'getuid', { value: () => 0, configurable: true });
148+
process.env.SUDO_USER = 'alice';
149+
process.env.HOME = '/root';
150+
mockFs.readFileSync.mockReturnValue('root:x:0:0:root:/root:/bin/bash\nalice:x:1000:1000:Alice:/home/alice:/bin/bash\n');
151+
expect(getRealUserHome()).toBe('/home/alice');
152+
});
153+
154+
it('falls back to HOME when SUDO_USER is not found in /etc/passwd', () => {
155+
Object.defineProperty(process, 'getuid', { value: () => 0, configurable: true });
156+
process.env.SUDO_USER = 'bob';
157+
process.env.HOME = '/root';
158+
mockFs.readFileSync.mockReturnValue('root:x:0:0:root:/root:/bin/bash\n');
159+
expect(getRealUserHome()).toBe('/root');
160+
});
161+
162+
it('falls back to HOME when /etc/passwd is unreadable', () => {
163+
Object.defineProperty(process, 'getuid', { value: () => 0, configurable: true });
164+
process.env.SUDO_USER = 'alice';
165+
process.env.HOME = '/root';
166+
mockFs.readFileSync.mockImplementation(() => { throw new Error('EACCES'); });
167+
expect(getRealUserHome()).toBe('/root');
168+
});
169+
170+
it('falls back to HOME when SUDO_USER is not set but running as root', () => {
171+
Object.defineProperty(process, 'getuid', { value: () => 0, configurable: true });
172+
process.env.HOME = '/root';
173+
expect(getRealUserHome()).toBe('/root');
174+
});
175+
176+
it('returns /root when HOME is not set and running as root without SUDO_USER', () => {
177+
Object.defineProperty(process, 'getuid', { value: () => 0, configurable: true });
178+
expect(getRealUserHome()).toBe('/root');
179+
});
180+
181+
it('falls back to HOME when passwd line has fewer than 6 fields', () => {
182+
Object.defineProperty(process, 'getuid', { value: () => 0, configurable: true });
183+
process.env.SUDO_USER = 'alice';
184+
process.env.HOME = '/home/fallback';
185+
mockFs.readFileSync.mockReturnValue('alice:x:1000:1000\n');
186+
expect(getRealUserHome()).toBe('/home/fallback');
187+
});
188+
189+
it('returns /root as ultimate fallback when no HOME env', () => {
190+
Object.defineProperty(process, 'getuid', { value: () => 1000, configurable: true });
191+
expect(getRealUserHome()).toBe('/root');
192+
});
193+
});

src/parsers/dns-parsers.test.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { parseDnsServers, parseDnsOverHttps, processLocalhostKeyword } from './dns-parsers';
2+
3+
describe('parseDnsServers', () => {
4+
it('parses a single valid IPv4 server', () => {
5+
expect(parseDnsServers('8.8.8.8')).toEqual(['8.8.8.8']);
6+
});
7+
8+
it('parses multiple IPv4 servers', () => {
9+
expect(parseDnsServers('8.8.8.8,8.8.4.4')).toEqual(['8.8.8.8', '8.8.4.4']);
10+
});
11+
12+
it('trims whitespace around server addresses', () => {
13+
expect(parseDnsServers(' 8.8.8.8 , 1.1.1.1 ')).toEqual(['8.8.8.8', '1.1.1.1']);
14+
});
15+
16+
it('throws when input is empty string', () => {
17+
expect(() => parseDnsServers('')).toThrow('At least one DNS server must be specified');
18+
});
19+
20+
it('throws when all entries are blank after trimming', () => {
21+
expect(() => parseDnsServers(' , ')).toThrow('At least one DNS server must be specified');
22+
});
23+
24+
it('throws on invalid IP address format', () => {
25+
expect(() => parseDnsServers('not-an-ip')).toThrow('Invalid DNS server IP address: not-an-ip');
26+
});
27+
28+
it('throws on hostname instead of IP', () => {
29+
expect(() => parseDnsServers('dns.google')).toThrow('Invalid DNS server IP address: dns.google');
30+
});
31+
32+
it('throws when second server is invalid', () => {
33+
expect(() => parseDnsServers('8.8.8.8,invalid')).toThrow('Invalid DNS server IP address: invalid');
34+
});
35+
36+
it('accepts a valid IPv6 loopback address', () => {
37+
expect(parseDnsServers('::1')).toEqual(['::1']);
38+
});
39+
40+
it('accepts a valid full IPv6 address', () => {
41+
expect(parseDnsServers('2001:4860:4860::8888')).toEqual(['2001:4860:4860::8888']);
42+
});
43+
44+
it('accepts mixed IPv4 and IPv6 servers', () => {
45+
expect(parseDnsServers('8.8.8.8,::1')).toEqual(['8.8.8.8', '::1']);
46+
});
47+
});
48+
49+
describe('parseDnsOverHttps', () => {
50+
it('returns undefined when value is undefined', () => {
51+
expect(parseDnsOverHttps(undefined)).toBeUndefined();
52+
});
53+
54+
it('returns the default Google DoH URL when value is true', () => {
55+
const result = parseDnsOverHttps(true);
56+
expect(result).toEqual({ url: 'https://dns.google/dns-query' });
57+
});
58+
59+
it('returns a custom https URL unchanged', () => {
60+
const result = parseDnsOverHttps('https://cloudflare-dns.com/dns-query');
61+
expect(result).toEqual({ url: 'https://cloudflare-dns.com/dns-query' });
62+
});
63+
64+
it('returns an error for an http:// URL', () => {
65+
const result = parseDnsOverHttps('http://dns.example.com/dns-query');
66+
expect(result).toEqual({ error: '--dns-over-https resolver URL must start with https://' });
67+
});
68+
69+
it('returns an error for a bare hostname', () => {
70+
const result = parseDnsOverHttps('dns.example.com');
71+
expect(result).toEqual({ error: '--dns-over-https resolver URL must start with https://' });
72+
});
73+
74+
it('returns an error when value is false (falsy but not undefined)', () => {
75+
const result = parseDnsOverHttps(false);
76+
expect(result).toEqual({ error: '--dns-over-https resolver URL must start with https://' });
77+
});
78+
});
79+
80+
describe('processLocalhostKeyword', () => {
81+
it('returns domains unchanged when localhost is not present', () => {
82+
const result = processLocalhostKeyword(['github.com', 'api.github.com'], false, undefined);
83+
expect(result.localhostDetected).toBe(false);
84+
expect(result.shouldEnableHostAccess).toBe(false);
85+
expect(result.allowedDomains).toEqual(['github.com', 'api.github.com']);
86+
expect(result.defaultPorts).toBeUndefined();
87+
});
88+
89+
it('replaces bare localhost with host.docker.internal', () => {
90+
const result = processLocalhostKeyword(['localhost', 'github.com'], false, undefined);
91+
expect(result.localhostDetected).toBe(true);
92+
expect(result.allowedDomains).toContain('host.docker.internal');
93+
expect(result.allowedDomains).not.toContain('localhost');
94+
expect(result.allowedDomains).toContain('github.com');
95+
});
96+
97+
it('preserves http:// protocol when replacing localhost', () => {
98+
const result = processLocalhostKeyword(['http://localhost'], false, undefined);
99+
expect(result.allowedDomains).toContain('http://host.docker.internal');
100+
expect(result.allowedDomains).not.toContain('http://localhost');
101+
});
102+
103+
it('preserves https:// protocol when replacing localhost', () => {
104+
const result = processLocalhostKeyword(['https://localhost'], false, undefined);
105+
expect(result.allowedDomains).toContain('https://host.docker.internal');
106+
});
107+
108+
it('sets shouldEnableHostAccess to true when enableHostAccess is false', () => {
109+
const result = processLocalhostKeyword(['localhost'], false, undefined);
110+
expect(result.shouldEnableHostAccess).toBe(true);
111+
});
112+
113+
it('sets shouldEnableHostAccess to false when enableHostAccess is already true', () => {
114+
const result = processLocalhostKeyword(['localhost'], true, undefined);
115+
expect(result.shouldEnableHostAccess).toBe(false);
116+
});
117+
118+
it('sets defaultPorts when allowHostPorts is undefined', () => {
119+
const result = processLocalhostKeyword(['localhost'], false, undefined);
120+
expect(result.defaultPorts).toBeDefined();
121+
expect(result.defaultPorts).toContain('3000');
122+
expect(result.defaultPorts).toContain('8080');
123+
});
124+
125+
it('sets defaultPorts to undefined when allowHostPorts is already provided', () => {
126+
const result = processLocalhostKeyword(['localhost'], false, '8080,3000');
127+
expect(result.defaultPorts).toBeUndefined();
128+
});
129+
});

0 commit comments

Comments
 (0)