342 lines
12 KiB
TypeScript
342 lines
12 KiB
TypeScript
import * as core from '@actions/core';
|
|
import { fs, vol } from 'memfs';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import * as ini from 'ini';
|
|
import {
|
|
ensureAwsDirectoryExists,
|
|
getProfileFilePaths,
|
|
mergeProfileSection,
|
|
validateProfileName,
|
|
writeProfileFiles,
|
|
} from '../src/profileManager';
|
|
|
|
describe('Profile Manager', {}, () => {
|
|
beforeEach(() => {
|
|
vi.restoreAllMocks();
|
|
vi.mock('node:fs');
|
|
vol.reset();
|
|
vi.spyOn(core, 'debug').mockImplementation(() => {});
|
|
vi.spyOn(core, 'info').mockImplementation(() => {});
|
|
});
|
|
|
|
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',
|
|
});
|
|
|
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
const parsed = ini.parse(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',
|
|
});
|
|
|
|
// Add second profile
|
|
mergeProfileSection(filePath, 'prod', {
|
|
aws_access_key_id: 'AKIAPRODEXAMPLE',
|
|
aws_secret_access_key: 'prodSecretKey',
|
|
});
|
|
|
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
const parsed = ini.parse(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',
|
|
});
|
|
|
|
// Overwrite with new credentials
|
|
mergeProfileSection(filePath, 'dev', {
|
|
aws_access_key_id: 'NEW_KEY',
|
|
aws_secret_access_key: 'newSecretKey',
|
|
aws_session_token: 'sessionToken',
|
|
});
|
|
|
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
const parsed = ini.parse(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('sessionToken');
|
|
});
|
|
});
|
|
|
|
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',
|
|
);
|
|
|
|
// Check credentials file
|
|
const credsPath = getProfileFilePaths().credentials;
|
|
const credContent = fs.readFileSync(credsPath, 'utf-8');
|
|
const credParsed = ini.parse(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 = ini.parse(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',
|
|
);
|
|
|
|
// Check credentials file uses [default]
|
|
const credsPath = getProfileFilePaths().credentials;
|
|
const credContent = fs.readFileSync(credsPath, 'utf-8');
|
|
const credParsed = ini.parse(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 = ini.parse(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',
|
|
);
|
|
|
|
// Write second profile
|
|
writeProfileFiles(
|
|
'prod',
|
|
{
|
|
AccessKeyId: 'AKIAPROD',
|
|
SecretAccessKey: 'prodSecret',
|
|
SessionToken: 'prodToken',
|
|
},
|
|
'us-west-2',
|
|
);
|
|
|
|
// Verify both profiles exist
|
|
const credsPath = getProfileFilePaths().credentials;
|
|
const credContent = fs.readFileSync(credsPath, 'utf-8');
|
|
const credParsed = ini.parse(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',
|
|
);
|
|
|
|
const credsPath = getProfileFilePaths().credentials;
|
|
const credContent = fs.readFileSync(credsPath, 'utf-8');
|
|
const credParsed = ini.parse(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',
|
|
),
|
|
).toThrow('Failed to write AWS profile');
|
|
expect(() =>
|
|
writeProfileFiles(
|
|
'invalid profile',
|
|
{
|
|
AccessKeyId: 'AKIA',
|
|
SecretAccessKey: 'secret',
|
|
},
|
|
'us-east-1',
|
|
),
|
|
).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',
|
|
);
|
|
|
|
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',
|
|
);
|
|
|
|
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');
|
|
});
|
|
});
|
|
});
|