import * as core from '@actions/core'; import { fs, vol } from 'memfs'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { ensureAwsDirectoryExists, getProfileFilePaths, mergeProfileSection, parseIni, stringifyIni, validateProfileName, writeProfileFiles, } from '../src/profileManager'; vi.mock('node:fs'); vi.mock('@actions/core'); describe('Profile Manager', {}, () => { beforeEach(() => { vi.restoreAllMocks(); vi.clearAllMocks(); vol.reset(); }); describe('parseIni', {}, () => { it('parses a single section', {}, () => { const result = parseIni('[dev]\naws_access_key_id=AKIA\naws_secret_access_key=secret\n'); expect(result.dev).toEqual({ aws_access_key_id: 'AKIA', aws_secret_access_key: 'secret' }); }); it('parses multiple sections', {}, () => { const result = parseIni('[dev]\nkey=dev_val\n\n[prod]\nkey=prod_val\n'); expect(result.dev.key).toBe('dev_val'); expect(result.prod.key).toBe('prod_val'); }); it('skips comments and empty lines', {}, () => { const result = parseIni('# comment\n; another comment\n\n[dev]\nkey=val\n'); expect(result.dev).toEqual({ key: 'val' }); }); it('trims whitespace around keys and values', {}, () => { const result = parseIni('[dev]\n key = val \n'); expect(result.dev.key).toBe('val'); }); it('preserves section names with spaces (e.g. profile prefix)', {}, () => { const result = parseIni('[profile dev]\nregion=us-east-1\n'); expect(result['profile dev']).toEqual({ region: 'us-east-1' }); }); it('guards against __proto__ section pollution', {}, () => { const result = parseIni('[__proto__]\npolluted=true\n[safe]\nkey=val\n'); expect(Object.getPrototypeOf(result)).not.toHaveProperty('polluted'); expect(result.safe).toEqual({ key: 'val' }); }); it('guards against __proto__ key pollution', {}, () => { const result = parseIni('[dev]\n__proto__=evil\naws_access_key_id=AKIA\n'); expect(result.dev).toEqual({ aws_access_key_id: 'AKIA' }); expect(result.dev).not.toHaveProperty('__proto__', 'evil'); }); it('handles values containing equals signs', {}, () => { const result = parseIni('[dev]\naws_session_token=FwoGZXIvYXdzEBYa/base64==\n'); expect(result.dev.aws_session_token).toBe('FwoGZXIvYXdzEBYa/base64=='); }); it('handles empty values', {}, () => { const result = parseIni('[dev]\ncli_pager=\n'); expect(result.dev.cli_pager).toBe(''); }); it('returns empty object for empty input', {}, () => { expect(parseIni('')).toEqual({}); }); it('returns empty object for whitespace-only input', {}, () => { expect(parseIni(' \n\n \n')).toEqual({}); }); it('handles Windows line endings (CRLF)', {}, () => { const result = parseIni('[dev]\r\naws_access_key_id=AKIA\r\naws_secret_access_key=secret\r\n'); expect(result.dev).toEqual({ aws_access_key_id: 'AKIA', aws_secret_access_key: 'secret' }); }); }); describe('stringifyIni', {}, () => { it('serializes a single section', {}, () => { const result = stringifyIni({ dev: { key: 'val' } }); expect(result).toBe('[dev]\nkey = val\n'); }); it('serializes multiple sections with blank line separator', {}, () => { const result = stringifyIni({ dev: { a: '1' }, prod: { b: '2' } }); expect(result).toBe('[dev]\na = 1\n\n[prod]\nb = 2\n'); }); it('round-trips through parseIni', {}, () => { const data = { dev: { aws_access_key_id: 'AKIA', aws_secret_access_key: 'secret' }, 'profile prod': { region: 'us-west-2' }, }; const roundTripped = parseIni(stringifyIni(data)); expect(roundTripped).toEqual(data); }); it('handles empty data object', {}, () => { const result = stringifyIni({}); expect(result).toBe('\n'); expect(parseIni(result)).toEqual({}); }); it('handles section with no keys', {}, () => { const result = stringifyIni({ dev: {} }); expect(result).toBe('[dev]\n'); }); }); describe('validateProfileName', {}, () => { it('accepts valid profile names', {}, () => { expect(() => validateProfileName('dev')).not.toThrow(); expect(() => validateProfileName('production')).not.toThrow(); expect(() => validateProfileName('my-profile-123')).not.toThrow(); expect(() => validateProfileName('default')).not.toThrow(); }); it('rejects empty profile names', {}, () => { expect(() => validateProfileName('')).toThrow('aws-profile must not be empty'); expect(() => validateProfileName(' ')).toThrow('aws-profile must not be empty'); }); it('rejects profile names with whitespace', {}, () => { expect(() => validateProfileName('my profile')).toThrow('aws-profile must not contain whitespace'); expect(() => validateProfileName('dev\ntest')).toThrow('aws-profile must not contain whitespace'); expect(() => validateProfileName('prod\tenv')).toThrow('aws-profile must not contain whitespace'); }); it('rejects profile names with brackets', {}, () => { expect(() => validateProfileName('dev[test]')).toThrow('aws-profile must not contain brackets'); expect(() => validateProfileName('[profile]')).toThrow('aws-profile must not contain brackets'); }); it('rejects profile names with path separators', {}, () => { expect(() => validateProfileName('dev/test')).toThrow('aws-profile must not contain path separators'); expect(() => validateProfileName('dev\\test')).toThrow('aws-profile must not contain path separators'); expect(() => validateProfileName('../etc/passwd')).toThrow('aws-profile must not contain path separators'); }); }); describe('getProfileFilePaths', {}, () => { it('returns default paths when env vars not set', {}, () => { delete process.env.AWS_SHARED_CREDENTIALS_FILE; delete process.env.AWS_CONFIG_FILE; const paths = getProfileFilePaths(); expect(paths.credentials).toMatch(/\.aws[/\\]credentials$/); expect(paths.config).toMatch(/\.aws[/\\]config$/); }); it('respects AWS_SHARED_CREDENTIALS_FILE env var', {}, () => { process.env.AWS_SHARED_CREDENTIALS_FILE = '/custom/path/credentials'; process.env.AWS_CONFIG_FILE = '/custom/path/config'; const paths = getProfileFilePaths(); expect(paths.credentials).toBe('/custom/path/credentials'); expect(paths.config).toBe('/custom/path/config'); }); }); describe('ensureAwsDirectoryExists', {}, () => { it('creates directory if it does not exist', {}, () => { const filePath = '/home/runner/.aws/credentials'; ensureAwsDirectoryExists(filePath); expect(fs.existsSync('/home/runner/.aws')).toBe(true); }); it('does not error if directory already exists', {}, () => { const filePath = '/home/runner/.aws/credentials'; fs.mkdirSync('/home/runner/.aws', { recursive: true }); expect(() => ensureAwsDirectoryExists(filePath)).not.toThrow(); }); it('creates nested directories', {}, () => { const filePath = '/home/runner/custom/path/.aws/credentials'; ensureAwsDirectoryExists(filePath); expect(fs.existsSync('/home/runner/custom/path/.aws')).toBe(true); }); }); describe('mergeProfileSection', {}, () => { it('creates new file with profile section', {}, () => { const filePath = '/home/runner/.aws/credentials'; fs.mkdirSync('/home/runner/.aws', { recursive: true }); mergeProfileSection( filePath, 'dev', { aws_access_key_id: 'AKIAIOSFODNN7EXAMPLE', aws_secret_access_key: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', }, false, ); const content = fs.readFileSync(filePath, 'utf-8'); const parsed = parseIni(content); expect(parsed.dev).toBeDefined(); expect(parsed.dev.aws_access_key_id).toBe('AKIAIOSFODNN7EXAMPLE'); expect(parsed.dev.aws_secret_access_key).toBe('wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'); }); it('merges with existing profiles', {}, () => { const filePath = '/home/runner/.aws/credentials'; fs.mkdirSync('/home/runner/.aws', { recursive: true }); // Create initial profile mergeProfileSection( filePath, 'dev', { aws_access_key_id: 'AKIAIOSFODNN7EXAMPLE', aws_secret_access_key: 'devSecretKey', }, false, ); // Add second profile mergeProfileSection( filePath, 'prod', { aws_access_key_id: 'AKIAPRODEXAMPLE', aws_secret_access_key: 'prodSecretKey', }, false, ); const content = fs.readFileSync(filePath, 'utf-8'); const parsed = parseIni(content); expect(parsed.dev).toBeDefined(); expect(parsed.dev.aws_access_key_id).toBe('AKIAIOSFODNN7EXAMPLE'); expect(parsed.prod).toBeDefined(); expect(parsed.prod.aws_access_key_id).toBe('AKIAPRODEXAMPLE'); }); it('overwrites existing profile with same name', {}, () => { const filePath = '/home/runner/.aws/credentials'; fs.mkdirSync('/home/runner/.aws', { recursive: true }); // Create initial profile mergeProfileSection( filePath, 'dev', { aws_access_key_id: 'OLD_KEY', aws_secret_access_key: 'oldSecretKey', aws_session_token: 'oldSessionToken', }, false, ); // Overwrite with new credentials mergeProfileSection( filePath, 'dev', { aws_access_key_id: 'NEW_KEY', aws_secret_access_key: 'newSecretKey', aws_session_token: 'newSessionToken', }, true, ); const content = fs.readFileSync(filePath, 'utf-8'); const parsed = parseIni(content); expect(parsed.dev.aws_access_key_id).toBe('NEW_KEY'); expect(parsed.dev.aws_secret_access_key).toBe('newSecretKey'); expect(parsed.dev.aws_session_token).toBe('newSessionToken'); }); it('overwriting a profile removes stale keys', {}, () => { const filePath = '/home/runner/.aws/credentials'; fs.mkdirSync('/home/runner/.aws', { recursive: true }); // Create profile with session token mergeProfileSection( filePath, 'dev', { aws_access_key_id: 'AKIA', aws_secret_access_key: 'secret', aws_session_token: 'old-token', }, false, ); // Overwrite without session token mergeProfileSection( filePath, 'dev', { aws_access_key_id: 'AKIA2', aws_secret_access_key: 'secret2', }, true, ); const content = fs.readFileSync(filePath, 'utf-8'); const parsed = parseIni(content); expect(parsed.dev.aws_access_key_id).toBe('AKIA2'); expect(parsed.dev.aws_secret_access_key).toBe('secret2'); expect(parsed.dev.aws_session_token).toBeUndefined(); }); it('handles empty existing file', {}, () => { const filePath = '/home/runner/.aws/credentials'; fs.mkdirSync('/home/runner/.aws', { recursive: true }); fs.writeFileSync(filePath, '', { mode: 0o600 }); mergeProfileSection( filePath, 'dev', { aws_access_key_id: 'AKIA', aws_secret_access_key: 'secret', }, false, ); const content = fs.readFileSync(filePath, 'utf-8'); const parsed = parseIni(content); expect(parsed.dev.aws_access_key_id).toBe('AKIA'); expect(parsed.dev.aws_secret_access_key).toBe('secret'); }); it('errors if profile name already exists but overwrite flag is false', {}, () => { const filePath = '/home/runner/.aws/credentials'; fs.mkdirSync('/home/runner/.aws', { recursive: true }); // Create initial profile mergeProfileSection( filePath, 'dev', { aws_access_key_id: 'OLD_KEY', aws_secret_access_key: 'oldSecretKey', }, false, ); // Overwrite with new credentials expect(() => mergeProfileSection( filePath, 'dev', { aws_access_key_id: 'NEW_KEY', aws_secret_access_key: 'newSecretKey', aws_session_token: 'sessionToken', }, false, ), ).toThrow( `Profile with name "dev" already exists. Please use the overwrite-aws-profile input if you want to overwrite existing profiles.`, ); const content = fs.readFileSync(filePath, 'utf-8'); const parsed = parseIni(content); expect(parsed.dev.aws_access_key_id).toBe('OLD_KEY'); expect(parsed.dev.aws_secret_access_key).toBe('oldSecretKey'); expect(parsed.dev.aws_session_token).toBeUndefined(); }); }); describe('writeProfileFiles', {}, () => { beforeEach(() => { delete process.env.AWS_SHARED_CREDENTIALS_FILE; delete process.env.AWS_CONFIG_FILE; }); it('writes credentials and config for new profile', {}, () => { writeProfileFiles( 'dev', { AccessKeyId: 'AKIAIOSFODNN7EXAMPLE', SecretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', SessionToken: 'FwoGZXIvYXdzEBYaDEXAMPLE', }, 'us-east-1', false, ); // Check credentials file const credsPath = getProfileFilePaths().credentials; const credContent = fs.readFileSync(credsPath, 'utf-8'); const credParsed = parseIni(credContent); expect(credParsed.dev).toBeDefined(); expect(credParsed.dev.aws_access_key_id).toBe('AKIAIOSFODNN7EXAMPLE'); expect(credParsed.dev.aws_secret_access_key).toBe('wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'); expect(credParsed.dev.aws_session_token).toBe('FwoGZXIvYXdzEBYaDEXAMPLE'); // Check config file const configPath = getProfileFilePaths().config; const configContent = fs.readFileSync(configPath, 'utf-8'); const configParsed = parseIni(configContent); expect(configParsed['profile dev']).toBeDefined(); expect(configParsed['profile dev'].region).toBe('us-east-1'); }); it('uses correct section naming for default profile', {}, () => { writeProfileFiles( 'default', { AccessKeyId: 'AKIAIOSFODNN7EXAMPLE', SecretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', }, 'us-west-2', false, ); // Check credentials file uses [default] const credsPath = getProfileFilePaths().credentials; const credContent = fs.readFileSync(credsPath, 'utf-8'); const credParsed = parseIni(credContent); expect(credParsed.default).toBeDefined(); expect(credParsed['profile default']).toBeUndefined(); // Check config file uses [default] (not [profile default]) const configPath = getProfileFilePaths().config; const configContent = fs.readFileSync(configPath, 'utf-8'); const configParsed = parseIni(configContent); expect(configParsed.default).toBeDefined(); expect(configParsed['profile default']).toBeUndefined(); }); it('supports multiple profiles', {}, () => { // Write first profile writeProfileFiles( 'dev', { AccessKeyId: 'AKIADEV', SecretAccessKey: 'devSecret', }, 'us-east-1', false, ); // Write second profile writeProfileFiles( 'prod', { AccessKeyId: 'AKIAPROD', SecretAccessKey: 'prodSecret', SessionToken: 'prodToken', }, 'us-west-2', false, ); // Verify both profiles exist const credsPath = getProfileFilePaths().credentials; const credContent = fs.readFileSync(credsPath, 'utf-8'); const credParsed = parseIni(credContent); expect(credParsed.dev).toBeDefined(); expect(credParsed.prod).toBeDefined(); expect(credParsed.dev.aws_access_key_id).toBe('AKIADEV'); expect(credParsed.prod.aws_access_key_id).toBe('AKIAPROD'); }); it('handles credentials without session token', {}, () => { writeProfileFiles( 'dev', { AccessKeyId: 'AKIAIOSFODNN7EXAMPLE', SecretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', }, 'us-east-1', false, ); const credsPath = getProfileFilePaths().credentials; const credContent = fs.readFileSync(credsPath, 'utf-8'); const credParsed = parseIni(credContent); expect(credParsed.dev.aws_access_key_id).toBe('AKIAIOSFODNN7EXAMPLE'); expect(credParsed.dev.aws_secret_access_key).toBe('wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'); expect(credParsed.dev.aws_session_token).toBeUndefined(); }); it('throws error for invalid profile name', {}, () => { expect(() => writeProfileFiles( 'invalid profile', { AccessKeyId: 'AKIA', SecretAccessKey: 'secret', }, 'us-east-1', false, ), ).toThrow('Failed to write AWS profile'); expect(() => writeProfileFiles( 'invalid profile', { AccessKeyId: 'AKIA', SecretAccessKey: 'secret', }, 'us-east-1', false, ), ).toThrow('whitespace'); }); it('respects custom file paths from env vars', {}, () => { process.env.AWS_SHARED_CREDENTIALS_FILE = '/custom/credentials'; process.env.AWS_CONFIG_FILE = '/custom/config'; fs.mkdirSync('/custom', { recursive: true }); writeProfileFiles( 'dev', { AccessKeyId: 'AKIA', SecretAccessKey: 'secret', }, 'us-east-1', false, ); expect(fs.existsSync('/custom/credentials')).toBe(true); expect(fs.existsSync('/custom/config')).toBe(true); }); it('logs info messages', {}, () => { writeProfileFiles( 'dev', { AccessKeyId: 'AKIA', SecretAccessKey: 'secret', }, 'us-east-1', false, ); expect(core.info).toHaveBeenCalledWith('Writing credentials to profile: dev'); expect(core.info).toHaveBeenCalledWith('Writing config to profile: dev'); expect(core.info).toHaveBeenCalledWith('✓ Successfully configured AWS profile: dev'); }); it('preserves pre-existing unrelated profiles in credentials file', {}, () => { const credsPath = getProfileFilePaths().credentials; fs.mkdirSync(require('node:path').dirname(credsPath), { recursive: true }); fs.writeFileSync( credsPath, '[personal]\naws_access_key_id=AKIAPERSONAL\naws_secret_access_key=personalSecret\naws_session_token=personalToken\n', ); writeProfileFiles('dev', { AccessKeyId: 'AKIADEV', SecretAccessKey: 'devSecret' }, 'us-east-1', false); const content = fs.readFileSync(credsPath, 'utf-8'); const parsed = parseIni(content); // Pre-existing profile must be fully intact expect(parsed.personal).toEqual({ aws_access_key_id: 'AKIAPERSONAL', aws_secret_access_key: 'personalSecret', aws_session_token: 'personalToken', }); // New profile also present expect(parsed.dev.aws_access_key_id).toBe('AKIADEV'); }); it('preserves pre-existing config with extra keys', {}, () => { const configPath = getProfileFilePaths().config; fs.mkdirSync(require('node:path').dirname(configPath), { recursive: true }); fs.writeFileSync(configPath, '[profile personal]\nregion=eu-west-1\noutput=json\ncli_pager=\n'); writeProfileFiles('dev', { AccessKeyId: 'AKIA', SecretAccessKey: 'secret' }, 'us-east-1', false); const content = fs.readFileSync(configPath, 'utf-8'); const parsed = parseIni(content); expect(parsed['profile personal']).toEqual({ region: 'eu-west-1', output: 'json', cli_pager: '', }); expect(parsed['profile dev'].region).toBe('us-east-1'); }); it('preserves pre-existing default profile when writing a named profile', {}, () => { const credsPath = getProfileFilePaths().credentials; fs.mkdirSync(require('node:path').dirname(credsPath), { recursive: true }); fs.writeFileSync(credsPath, '[default]\naws_access_key_id=AKIADEFAULT\naws_secret_access_key=defaultSecret\n'); writeProfileFiles('dev', { AccessKeyId: 'AKIADEV', SecretAccessKey: 'devSecret' }, 'us-west-2', false); const content = fs.readFileSync(credsPath, 'utf-8'); const parsed = parseIni(content); expect(parsed.default).toEqual({ aws_access_key_id: 'AKIADEFAULT', aws_secret_access_key: 'defaultSecret', }); expect(parsed.dev.aws_access_key_id).toBe('AKIADEV'); }); it('comments in pre-existing files are stripped on round-trip', {}, () => { const credsPath = getProfileFilePaths().credentials; fs.mkdirSync(require('node:path').dirname(credsPath), { recursive: true }); fs.writeFileSync( credsPath, '# My important comment\n[personal]\naws_access_key_id=AKIA\naws_secret_access_key=secret\n', ); writeProfileFiles('dev', { AccessKeyId: 'AKIADEV', SecretAccessKey: 'devSecret' }, 'us-east-1', false); const content = fs.readFileSync(credsPath, 'utf-8') as string; // Comment is lost (known trade-off), but profile data is preserved expect(content).not.toContain('# My important comment'); const parsed = parseIni(content); expect(parsed.personal.aws_access_key_id).toBe('AKIA'); expect(parsed.dev.aws_access_key_id).toBe('AKIADEV'); }); it('writes empty section when credentials object has no keys', {}, () => { writeProfileFiles('dev', {}, 'us-east-1', false); const credsPath = getProfileFilePaths().credentials; const credContent = fs.readFileSync(credsPath, 'utf-8'); const credParsed = parseIni(credContent); // Section exists but has no credential keys expect(credParsed.dev).toEqual({}); // Config still gets region const configPath = getProfileFilePaths().config; const configContent = fs.readFileSync(configPath, 'utf-8'); const configParsed = parseIni(configContent); expect(configParsed['profile dev'].region).toBe('us-east-1'); }); it('resolves credentials and config paths independently from env vars', {}, () => { process.env.AWS_SHARED_CREDENTIALS_FILE = '/custom-creds/credentials'; // AWS_CONFIG_FILE is NOT set — should use default path fs.mkdirSync('/custom-creds', { recursive: true }); writeProfileFiles('dev', { AccessKeyId: 'AKIA', SecretAccessKey: 'secret' }, 'us-east-1', false); expect(fs.existsSync('/custom-creds/credentials')).toBe(true); // Config file should be at the default path (under homedir) const defaultConfigPath = require('node:path').join(require('node:os').homedir(), '.aws', 'config'); expect(fs.existsSync(defaultConfigPath)).toBe(true); }); it('produces AWS CLI-compatible INI output (golden file)', {}, () => { writeProfileFiles( 'dev', { AccessKeyId: 'AKIAIOSFODNN7EXAMPLE', SecretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', SessionToken: 'FwoGZXIvYXdzEBYaDEXAMPLE', }, 'us-east-1', false, ); const credsPath = getProfileFilePaths().credentials; const credContent = fs.readFileSync(credsPath, 'utf-8'); const configPath = getProfileFilePaths().config; const configContent = fs.readFileSync(configPath, 'utf-8'); // Verify exact byte-for-byte format matching AWS CLI style: // - [section] header on its own line // - key = value with spaces around = // - LF line endings, trailing newline expect(credContent).toBe( '[dev]\n' + 'aws_access_key_id = AKIAIOSFODNN7EXAMPLE\n' + 'aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\n' + 'aws_session_token = FwoGZXIvYXdzEBYaDEXAMPLE\n', ); expect(configContent).toBe('[profile dev]\n' + 'region = us-east-1\n'); }); it('golden file for multi-profile output', {}, () => { writeProfileFiles('dev', { AccessKeyId: 'AKIADEV', SecretAccessKey: 'devSecret' }, 'us-east-1', false); writeProfileFiles( 'prod', { AccessKeyId: 'AKIAPROD', SecretAccessKey: 'prodSecret', SessionToken: 'prodToken' }, 'us-west-2', false, ); const credsPath = getProfileFilePaths().credentials; const credContent = fs.readFileSync(credsPath, 'utf-8'); const configPath = getProfileFilePaths().config; const configContent = fs.readFileSync(configPath, 'utf-8'); expect(credContent).toBe( '[dev]\n' + 'aws_access_key_id = AKIADEV\n' + 'aws_secret_access_key = devSecret\n' + '\n' + '[prod]\n' + 'aws_access_key_id = AKIAPROD\n' + 'aws_secret_access_key = prodSecret\n' + 'aws_session_token = prodToken\n', ); expect(configContent).toBe( '[profile dev]\n' + 'region = us-east-1\n' + '\n' + '[profile prod]\n' + 'region = us-west-2\n', ); }); }); describe('symlink hardening', {}, () => { const credsPath = '/home/user/.aws/credentials'; const configPath = '/home/user/.aws/config'; beforeEach(() => { process.env.AWS_SHARED_CREDENTIALS_FILE = credsPath; process.env.AWS_CONFIG_FILE = configPath; }); it('mergeProfileSection refuses when the credentials path is a symlink and leaves the target unchanged', {}, () => { fs.mkdirSync('/home/user/.aws', { recursive: true }); fs.mkdirSync('/etc', { recursive: true }); fs.writeFileSync('/etc/passwd', 'root:x:0:0::/root:/bin/sh'); fs.symlinkSync('/etc/passwd', credsPath); expect(() => mergeProfileSection(credsPath, 'dev', { aws_access_key_id: 'AKIA' }, true)).toThrow( /Refusing .* \(.* symbolic link\)/, ); expect(fs.readFileSync('/etc/passwd', 'utf-8')).toBe('root:x:0:0::/root:/bin/sh'); }); it('mergeProfileSection refuses when the config path is a symlink', {}, () => { fs.mkdirSync('/home/user/.aws', { recursive: true }); fs.mkdirSync('/etc', { recursive: true }); fs.writeFileSync('/etc/sensitive', 'do not overwrite'); fs.symlinkSync('/etc/sensitive', configPath); expect(() => mergeProfileSection(configPath, 'profile dev', { region: 'us-east-1' }, true)).toThrow( /Refusing .* \(.* symbolic link\)/, ); expect(fs.readFileSync('/etc/sensitive', 'utf-8')).toBe('do not overwrite'); }); it('ensureAwsDirectoryExists refuses when ~/.aws is a symlink', {}, () => { fs.mkdirSync('/real-target', { recursive: true }); fs.mkdirSync('/home/user', { recursive: true }); fs.symlinkSync('/real-target', '/home/user/.aws'); expect(() => ensureAwsDirectoryExists(credsPath)).toThrow(/Refusing .* \(.* symbolic link\)/); }); it('writeProfileFiles refuses to overwrite a pre-existing symlink at the credentials path', {}, () => { fs.mkdirSync('/home/user/.aws', { recursive: true }); fs.mkdirSync('/etc', { recursive: true }); fs.writeFileSync('/etc/passwd', 'root:x:0:0::/root:/bin/sh'); fs.symlinkSync('/etc/passwd', credsPath); expect(() => writeProfileFiles('dev', { AccessKeyId: 'AKIA', SecretAccessKey: 'secret' }, 'us-east-1', true), ).toThrow(/Refusing .* \(.* symbolic link\)/); expect(fs.lstatSync(credsPath).isSymbolicLink()).toBe(true); expect(fs.readFileSync('/etc/passwd', 'utf-8')).toBe('root:x:0:0::/root:/bin/sh'); }); it('happy path still writes both files with mode 0o600 when no symlinks are present', {}, () => { writeProfileFiles('dev', { AccessKeyId: 'AKIA', SecretAccessKey: 'secret' }, 'us-east-1', false); expect(fs.statSync(credsPath).mode & 0o777).toBe(0o600); expect(fs.statSync(configPath).mode & 0o777).toBe(0o600); expect(fs.lstatSync(credsPath).isSymbolicLink()).toBe(false); expect(fs.lstatSync(configPath).isSymbolicLink()).toBe(false); }); }); });