Repository navigation
[Test Coverage] Add coverage for parsers, services, host-identity #5240
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
lpcox
merged 5 commits into
main
from
test-coverage-parsers-services-identity-1cebfe1f08885a2f
Jun 19, 2026
Merged
Changes from 1 commit
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
efe9650
test: add coverage for parsers, services, host-identity
github-actions[bot] 48516f2
Potential fix for pull request finding
lpcox 07289cd
Potential fix for pull request finding
lpcox 3f21780
Potential fix for pull request finding
lpcox 1db59ef
Merge remote-tracking branch 'origin/main' into test-coverage-parsers…
Copilot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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'); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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://' }); | ||
| }); | ||
| }); | ||
|
|
||
| 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(); | ||
| }); | ||
| }); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.