diff --git a/.changeset/swift-parks-jump.md b/.changeset/swift-parks-jump.md new file mode 100644 index 00000000..c7543707 --- /dev/null +++ b/.changeset/swift-parks-jump.md @@ -0,0 +1,7 @@ +--- +"@mimicprotocol/cli": patch +"@mimicprotocol/lib-ts": patch +"@mimicprotocol/test-ts": patch +--- + +Refactor credentials commands diff --git a/packages/cli/src/commands/authenticate.ts b/packages/cli/src/commands/authenticate.ts index 1385b687..83f88f2d 100644 --- a/packages/cli/src/commands/authenticate.ts +++ b/packages/cli/src/commands/authenticate.ts @@ -1,4 +1,5 @@ import { Command, Flags } from '@oclif/core' +import * as fs from 'fs' import { CredentialsManager, ProfileCredentials } from '../lib/CredentialsManager' import log from '../log' @@ -32,25 +33,57 @@ export default class Authenticate extends Command { } public static authenticate(cmd: Command, flags: AuthenticateFlags): ProfileCredentials { - let apiKey = flags['api-key'] - if (!apiKey) { - try { - const credentials = CredentialsManager.getDefault().getCredentials(flags.profile) - apiKey = credentials.apiKey - } catch (error) { - if (error instanceof Error) { - cmd.error(error.message, { - code: 'AuthenticationRequired', - suggestions: [ - `Run ${log.highlightText('mimic login')} to authenticate`, - `Run ${log.highlightText(`mimic login --profile ${flags.profile ?? ''}`)} to create this profile`, - `Or use ${log.highlightText('--api-key')} flag to provide API key directly`, - ].filter(Boolean) as string[], - }) - } - throw error + const apiKey = flags['api-key'] + const profileName = flags.profile || CredentialsManager.getDefaultProfileName() + + const credentialsManager = CredentialsManager.getDefault() + + if (apiKey) return { apiKey } + + try { + const credentialsDir = credentialsManager.getBaseDir() + const credentialsPath = credentialsManager.getCredentialsPath() + + if (!fs.existsSync(credentialsDir)) { + throw new Error(`No credentials directory found at ${credentialsDir}. Run 'mimic login' to authenticate.`) + } + + if (!fs.existsSync(credentialsPath)) { + throw new Error(`No credentials file found. Run 'mimic login' to authenticate.`) + } + + const profiles = credentialsManager.readCredentials() + + if (!profiles[profileName]) { + const availableProfiles = Object.keys(profiles) + const suggestion = + availableProfiles.length > 0 + ? `Available profiles: ${availableProfiles.join(', ')}` + : `No profiles found. Run 'mimic login' to create one.` + + throw new Error(`Profile '${profileName}' not found. ${suggestion}`) + } + + const credentials = profiles[profileName] + + if (!credentials.apiKey || credentials.apiKey.trim() === '') { + throw new Error( + `Profile '${profileName}' has no API key. Run 'mimic login --profile ${profileName}' to update credentials.` + ) + } + return { apiKey: credentials.apiKey } + } catch (error) { + if (error instanceof Error) { + cmd.error(`Authentication required: ${error.message}`, { + code: 'AuthenticationRequired', + suggestions: [ + `Run ${log.highlightText('mimic login')} to authenticate`, + `Run ${log.highlightText(`mimic login --profile ${flags.profile ?? ''}`)} to create this profile`, + `Or use ${log.highlightText('--api-key')} flag to provide API key directly`, + ].filter(Boolean) as string[], + }) } + throw error } - return { apiKey } } } diff --git a/packages/cli/src/commands/codegen.ts b/packages/cli/src/commands/codegen.ts index 75bf856a..7e3594cc 100644 --- a/packages/cli/src/commands/codegen.ts +++ b/packages/cli/src/commands/codegen.ts @@ -59,21 +59,21 @@ export default class Codegen extends Command { } if (!fs.existsSync(typesDir)) fs.mkdirSync(typesDir, { recursive: true }) - generateAbisCode(manifest, typesDir, manifestDir) - generateInputsCode(manifest, typesDir) + this.generateAbisCode(manifest, typesDir, manifestDir) + this.generateInputsCode(manifest, typesDir) log.stopAction() } -} -function generateAbisCode(manifest: Manifest, typesDir: string, manifestDir: string) { - for (const [contractName, path] of Object.entries(manifest.abis)) { - const abi = JSON.parse(fs.readFileSync(join(manifestDir, '../', path), 'utf-8')) - const abiInterface = AbisInterfaceGenerator.generate(abi, contractName) - if (abiInterface.length > 0) fs.writeFileSync(`${typesDir}/${contractName}.ts`, abiInterface) + private static generateAbisCode(manifest: Manifest, typesDir: string, manifestDir: string) { + for (const [contractName, path] of Object.entries(manifest.abis)) { + const abi = JSON.parse(fs.readFileSync(join(manifestDir, '../', path), 'utf-8')) + const abiInterface = AbisInterfaceGenerator.generate(abi, contractName) + if (abiInterface.length > 0) fs.writeFileSync(`${typesDir}/${contractName}.ts`, abiInterface) + } } -} -function generateInputsCode(manifest: Manifest, typesDir: string) { - const inputsInterface = InputsInterfaceGenerator.generate(manifest.inputs) - if (inputsInterface.length > 0) fs.writeFileSync(`${typesDir}/index.ts`, inputsInterface) + private static generateInputsCode(manifest: Manifest, typesDir: string) { + const inputsInterface = InputsInterfaceGenerator.generate(manifest.inputs) + if (inputsInterface.length > 0) fs.writeFileSync(`${typesDir}/index.ts`, inputsInterface) + } } diff --git a/packages/cli/src/commands/deploy.ts b/packages/cli/src/commands/deploy.ts index 2f98c97b..d25ca9b3 100644 --- a/packages/cli/src/commands/deploy.ts +++ b/packages/cli/src/commands/deploy.ts @@ -40,10 +40,10 @@ export default class Deploy extends Command { public async run(): Promise { const { flags } = await this.parse(Deploy) - await this.deploy(this, flags) + await Deploy.deploy(this, flags) } - public async deploy(cmd: Command, flags: DeployFlags): Promise { + public static async deploy(cmd: Command, flags: DeployFlags): Promise { const { 'build-directory': buildDir, 'skip-build': skipBuild, url: registryUrl } = flags const absBuildDir = resolve(buildDir) @@ -69,7 +69,7 @@ export default class Deploy extends Command { } log.startAction('Uploading to Mimic Registry') - const CID = await this.uploadToRegistry(neededFiles, credentials, registryUrl) + const CID = await this.uploadToRegistry(cmd, neededFiles, credentials, registryUrl) console.log(`IPFS CID: ${log.highlightText(CID)}`) log.stopAction() @@ -78,13 +78,14 @@ export default class Deploy extends Command { console.log(`Function deployed!`) } - private async uploadToRegistry( + private static async uploadToRegistry( + cmd: Command, files: string[], credentials: ProfileCredentials, registryUrl: string ): Promise { try { - const form = filesToForm(files) + const form = this.filesToForm(files) const { data } = await axios.post(`${registryUrl}/functions`, form, { headers: { 'x-api-key': credentials.apiKey, @@ -93,28 +94,28 @@ export default class Deploy extends Command { }) return data.CID } catch (err) { - this.handleError(err, 'Failed to upload to registry') + this.handleError(cmd, err, 'Failed to upload to registry') } } - private handleError(err: unknown, message: string): never { - if (!(err instanceof AxiosError)) this.error(err as Error) + private static handleError(cmd: Command, err: unknown, message: string): never { + if (!(err instanceof AxiosError)) cmd.error(err as Error) const statusCode = err.response?.status if (statusCode === 400) { const errMessage = err.response?.data?.content?.message || message - this.error(errMessage, { code: 'Bad Request', suggestions: ['Review the uploaded files'] }) + cmd.error(errMessage, { code: 'Bad Request', suggestions: ['Review the uploaded files'] }) } - if (statusCode === 401) this.error(message, { code: 'Unauthorized', suggestions: ['Review your key'] }) - if (statusCode === 403) this.error(message, { code: 'Invalid api key', suggestions: ['Review your key'] }) - this.error(`${message} - ${err.message}`, { code: `${statusCode} Error`, suggestions: GENERIC_SUGGESTION }) + if (statusCode === 401) cmd.error(message, { code: 'Unauthorized', suggestions: ['Review your key'] }) + if (statusCode === 403) cmd.error(message, { code: 'Invalid api key', suggestions: ['Review your key'] }) + cmd.error(`${message} - ${err.message}`, { code: `${statusCode} Error`, suggestions: GENERIC_SUGGESTION }) } -} -const filesToForm = (files: string[]): FormData => { - return files.reduce((form, file) => { - const fileStream = fs.createReadStream(file) - const filename = file.split('/').pop() - form.append('file', fileStream, { filename }) - return form - }, new FormData()) + private static filesToForm(files: string[]): FormData { + return files.reduce((form, file) => { + const fileStream = fs.createReadStream(file) + const filename = file.split('/').pop() + form.append('file', fileStream, { filename }) + return form + }, new FormData()) + } } diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 5daf54c4..c6b0f6ef 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -6,6 +6,9 @@ import simpleGit from 'simple-git' import { execBinCommand, installDependencies } from '../lib/packageManager' import log from '../log' +import { FlagsType } from '../types' + +export type InitFlags = FlagsType & { directory: string } export default class Init extends Command { static override description = 'Initializes a new Mimic-compatible project structure in the specified directory' @@ -22,8 +25,11 @@ export default class Init extends Command { public async run(): Promise { const { args, flags } = await this.parse(Init) - const { directory } = args - const { force } = flags + await Init.init(this, { ...flags, ...args }) + } + + public static async init(cmd: Command, flags: InitFlags): Promise { + const { directory, force } = flags const absDir = path.resolve(directory) if (force && fs.existsSync(absDir) && fs.readdirSync(absDir).length > 0) { @@ -37,7 +43,7 @@ export default class Init extends Command { if (!shouldDelete) { console.log('You can remove the --force flag from your command') console.log('Stopping initialization...') - this.exit(0) + cmd.exit(0) } log.startAction(`Deleting contents of ${absDir}`) // Delete files individually instead of removing the entire directory to preserve @@ -51,7 +57,7 @@ export default class Init extends Command { log.startAction('Creating files') if (fs.existsSync(absDir) && fs.readdirSync(absDir).length > 0) { - this.error(`Directory ${log.highlightText(absDir)} is not empty`, { + cmd.error(`Directory ${log.highlightText(absDir)} is not empty`, { code: 'DirectoryNotEmpty', suggestions: [ 'You can specify the directory as a positional argument', @@ -70,7 +76,7 @@ export default class Init extends Command { const gitDir = path.join(absDir, '.git') if (fs.existsSync(gitDir)) fs.rmSync(gitDir, { recursive: true, force: true }) } catch (error) { - this.error(`Failed to clone template repository. Details: ${error}`) + cmd.error(`Failed to clone template repository. Details: ${error}`) } this.installDependencies(absDir) @@ -79,12 +85,12 @@ export default class Init extends Command { console.log('New project initialized!') } - installDependencies(absDir: string) { + private static installDependencies(absDir: string) { if (process.env.NODE_ENV === 'test') return installDependencies(absDir) } - runCodegen(absDir: string) { + private static runCodegen(absDir: string) { if (process.env.NODE_ENV === 'test') return execBinCommand('mimic', ['codegen'], absDir) } diff --git a/packages/cli/src/commands/login.ts b/packages/cli/src/commands/login.ts index 0033619f..3ddd9df2 100644 --- a/packages/cli/src/commands/login.ts +++ b/packages/cli/src/commands/login.ts @@ -1,12 +1,15 @@ import { confirm, input, password } from '@inquirer/prompts' -import { Flags } from '@oclif/core' +import { Command, Flags } from '@oclif/core' import { CredentialsManager } from '../lib/CredentialsManager' import log from '../log' +import { FlagsType } from '../types' import Authenticate from './authenticate' -export default class Login extends Authenticate { +export type LoginFlags = FlagsType + +export default class Login extends Command { static override description = 'Authenticate with Mimic by storing your API key locally' static override examples = [ @@ -26,6 +29,10 @@ export default class Login extends Authenticate { public async run(): Promise { const { flags } = await this.parse(Login) + await Login.login(this, flags) + } + + public static async login(cmd: Command, flags: LoginFlags): Promise { const { profile: profileInput, 'api-key': apiKeyFlag } = flags let apiKey: string @@ -61,16 +68,21 @@ export default class Login extends Authenticate { } catch (error) { if (error instanceof Error && error.message.includes('User force closed')) { console.log('\nLogin cancelled') - this.exit(0) + cmd.exit(0) } throw error } } - this.saveAndConfirm(profileName || CredentialsManager.getDefaultProfileName(), apiKey, flags['force-login']) + this.saveAndConfirm(cmd, profileName || CredentialsManager.getDefaultProfileName(), apiKey, flags['force-login']) } - private async saveAndConfirm(profileName: string, apiKey: string, forceLogin: boolean): Promise { + private static async saveAndConfirm( + cmd: Command, + profileName: string, + apiKey: string, + forceLogin: boolean + ): Promise { try { const credentialsManager = CredentialsManager.getDefault() @@ -98,7 +110,7 @@ export default class Login extends Authenticate { console.log(`Or with your profile: ${log.highlightText(`mimic deploy --profile ${profileName}`)}`) } } catch (error) { - if (error instanceof Error) this.error(`Failed to save credentials: ${error.message}`) + if (error instanceof Error) cmd.error(`Failed to save credentials: ${error.message}`) throw error } } diff --git a/packages/cli/src/commands/logout.ts b/packages/cli/src/commands/logout.ts index 01e2a26c..6beab3b6 100644 --- a/packages/cli/src/commands/logout.ts +++ b/packages/cli/src/commands/logout.ts @@ -3,6 +3,9 @@ import { Command, Flags } from '@oclif/core' import { CredentialsManager } from '../lib/CredentialsManager' import log from '../log' +import { FlagsType } from '../types' + +export type LogoutFlags = FlagsType export default class Logout extends Command { static override description = 'Remove stored credentials for a profile' @@ -27,11 +30,15 @@ export default class Logout extends Command { public async run(): Promise { const { flags } = await this.parse(Logout) + await Logout.logout(this, flags) + } + + public static async logout(cmd: Command, flags: LogoutFlags): Promise { const { profile: profileName, force } = flags const profiles = CredentialsManager.getDefault().getProfiles() if (!profiles.includes(profileName)) { - this.error(`Profile '${profileName}' does not exist`, { + cmd.error(`Profile '${profileName}' does not exist`, { code: 'ProfileNotFound', suggestions: profiles.length > 0 @@ -48,7 +55,7 @@ export default class Logout extends Command { if (!shouldRemove) { console.log('Logout cancelled') - this.exit(0) + cmd.exit(0) } } diff --git a/packages/cli/src/commands/profiles.ts b/packages/cli/src/commands/profiles.ts index 9ff24367..cc684df0 100644 --- a/packages/cli/src/commands/profiles.ts +++ b/packages/cli/src/commands/profiles.ts @@ -2,6 +2,9 @@ import { Command } from '@oclif/core' import { CredentialsManager } from '../lib/CredentialsManager' import log from '../log' +import { FlagsType } from '../types' + +export type ProfilesFlags = FlagsType export default class Profiles extends Command { static override description = 'List all configured authentication profiles' @@ -9,6 +12,10 @@ export default class Profiles extends Command { static override examples = ['<%= config.bin %> <%= command.id %>'] public async run(): Promise { + await Profiles.profiles() + } + + public static async profiles(): Promise { const profiles = CredentialsManager.getDefault().getProfiles() if (profiles.length === 0) { diff --git a/packages/cli/src/commands/test.ts b/packages/cli/src/commands/test.ts index 8157b578..e742f8e8 100644 --- a/packages/cli/src/commands/test.ts +++ b/packages/cli/src/commands/test.ts @@ -21,15 +21,15 @@ export default class Test extends Command { public async run(): Promise { const { flags } = await this.parse(Test) - await this.test(this, flags) + await Test.test(this, flags) } - public async test(cmd: Command, flags: TestFlags): Promise { + public static async test(cmd: Command, flags: TestFlags): Promise { const { directory, 'skip-build': skipBuild } = flags const baseDir = path.resolve('./') const testPath = path.join(baseDir, directory) - if (!skipBuild) await Build.build(this, flags) + if (!skipBuild) await Build.build(cmd, flags) const result = execBinCommand('tsx', ['./node_modules/mocha/bin/mocha.js', `${testPath}/**/*.spec.ts`], baseDir) cmd.exit(result.status ?? 1) diff --git a/packages/cli/src/lib/CredentialsManager.ts b/packages/cli/src/lib/CredentialsManager.ts index 9cc8690c..43e45eb2 100644 --- a/packages/cli/src/lib/CredentialsManager.ts +++ b/packages/cli/src/lib/CredentialsManager.ts @@ -121,50 +121,6 @@ export class CredentialsManager { this.writeCredentials(profiles) } - getProfile(profileName: string = DEFAULT_PROFILE): ProfileCredentials { - const credentialsDir = this.getBaseDir() - const credentialsPath = this.getCredentialsPath() - - if (!fs.existsSync(credentialsDir)) { - throw new Error(`No credentials directory found at ${credentialsDir}. Run 'mimic login' to authenticate.`) - } - - if (!fs.existsSync(credentialsPath)) { - throw new Error(`No credentials file found. Run 'mimic login' to authenticate.`) - } - - const profiles = this.readCredentials() - - if (!profiles[profileName]) { - const availableProfiles = Object.keys(profiles) - const suggestion = - availableProfiles.length > 0 - ? `Available profiles: ${availableProfiles.join(', ')}` - : `No profiles found. Run 'mimic login' to create one.` - - throw new Error(`Profile '${profileName}' not found. ${suggestion}`) - } - - const credentials = profiles[profileName] - - if (!credentials.apiKey || credentials.apiKey.trim() === '') { - throw new Error( - `Profile '${profileName}' has no API key. Run 'mimic login --profile ${profileName}' to update credentials.` - ) - } - - return credentials - } - - getCredentials(profileName: string = DEFAULT_PROFILE): ProfileCredentials { - try { - return this.getProfile(profileName) - } catch (error) { - if (error instanceof Error) throw new Error(`Authentication required: ${error.message}`) - throw error - } - } - getProfiles(): string[] { const profiles = this.readCredentials() return Object.keys(profiles) diff --git a/packages/cli/tests/commands/authenticate.spec.ts b/packages/cli/tests/commands/authenticate.spec.ts new file mode 100644 index 00000000..2862d85d --- /dev/null +++ b/packages/cli/tests/commands/authenticate.spec.ts @@ -0,0 +1,107 @@ +import { Command } from '@oclif/core' +import { expect } from 'chai' +import * as fs from 'fs' + +import Authenticate from '../../src/commands/authenticate' +import { CredentialsManager } from '../../src/lib/CredentialsManager' +import { backupCredentials, restoreCredentials } from '../helpers' + +const DEFAULT_PROFILE = 'default' + +describe('authenticate', () => { + let credentialsManager: CredentialsManager + let backupDir: string | null = null + let mockCommand: Command + + beforeEach('backup existing credentials and setup mock command', () => { + credentialsManager = CredentialsManager.getDefault() + backupDir = backupCredentials(credentialsManager) + mockCommand = new Command([], {}) + }) + + afterEach('restore credentials', () => { + restoreCredentials(credentialsManager, backupDir) + backupDir = null + }) + + context('when api-key is not provided', () => { + context('when credentials exist', () => { + beforeEach('create credentials', () => { + credentialsManager.saveProfile(DEFAULT_PROFILE, 'test-key-123') + }) + + context('when no profile is specified', () => { + it('returns the default profile', () => { + const credentials = Authenticate.authenticate(mockCommand, {}) + + expect(credentials.apiKey).to.equal('test-key-123') + }) + }) + + context('when profile is specified', () => { + context("when profile doesn't exists", () => { + it('throws an error', () => { + expect(() => Authenticate.authenticate(mockCommand, { profile: 'nonexistent' })).to.throw( + "Profile 'nonexistent' not found" + ) + }) + }) + + context('when profile exists', () => { + it('returns the profile', () => { + const credentials = Authenticate.authenticate(mockCommand, { profile: DEFAULT_PROFILE }) + expect(credentials).to.deep.equal({ apiKey: 'test-key-123' }) + }) + }) + }) + }) + + context('when no credentials exist', () => { + context('when no folder', () => { + beforeEach('remove folder', () => { + const credDir = credentialsManager.getBaseDir() + if (fs.existsSync(credDir)) { + fs.rmSync(credDir, { recursive: true, force: true }) + } + }) + it('throws an error', () => { + expect(() => Authenticate.authenticate(mockCommand, { profile: DEFAULT_PROFILE })).to.throw( + /No credentials directory found/ + ) + }) + }) + + context('when folder exists', () => { + beforeEach('create folder', () => { + credentialsManager.createCredentialsDirIfNotExists() + }) + it('throws an error', () => { + expect(() => Authenticate.authenticate(mockCommand, { profile: DEFAULT_PROFILE })).to.throw( + /No credentials file found/ + ) + }) + }) + }) + }) + + context('when api-key flag is provided', () => { + it('returns the api key', () => { + const credentials = Authenticate.authenticate(mockCommand, { 'api-key': 'direct-key-123' }) + + expect(credentials).to.deep.equal({ apiKey: 'direct-key-123' }) + }) + + context('when profile flag is also provided', () => { + it('returns the api key', () => { + credentialsManager.saveProfile(DEFAULT_PROFILE, 'profile-key') + + const credentials = Authenticate.authenticate(mockCommand, { + profile: DEFAULT_PROFILE, + 'api-key': 'flag-key', + }) + + expect(credentials).to.deep.equal({ apiKey: 'flag-key' }) + }) + }) + }) +}) diff --git a/packages/cli/tests/credentials.spec.ts b/packages/cli/tests/credentials.spec.ts index 3af5e151..eb66400a 100644 --- a/packages/cli/tests/credentials.spec.ts +++ b/packages/cli/tests/credentials.spec.ts @@ -242,86 +242,6 @@ api_key=staging-key }) }) - describe('getProfile', () => { - it('should throw error if credentials directory does not exist', () => { - const credDir = credentialsManager.getBaseDir() - if (fs.existsSync(credDir)) { - fs.rmSync(credDir, { recursive: true, force: true }) - } - expect(() => credentialsManager.getProfile(DEFAULT_PROFILE)).to.throw(/No credentials directory found/) - }) - - it('should throw error if credentials file does not exist', () => { - credentialsManager.createCredentialsDirIfNotExists() - expect(() => credentialsManager.getProfile(DEFAULT_PROFILE)).to.throw(/No credentials file found/) - }) - - it('should throw error if profile does not exist', () => { - credentialsManager.saveProfile(DEFAULT_PROFILE, 'test-key') - expect(() => credentialsManager.getProfile('nonexistent')).to.throw(/Profile 'nonexistent' not found/) - }) - - it('should include available profiles in error message', () => { - credentialsManager.saveProfile(DEFAULT_PROFILE, 'test-key') - credentialsManager.saveProfile('staging', 'staging-key') - - try { - credentialsManager.getProfile('nonexistent') - expect.fail('Should have thrown an error') - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (error: any) { - expect(error.message).to.include(DEFAULT_PROFILE) - expect(error.message).to.include('staging') - } - }) - - it('should throw error if api_key is empty', () => { - credentialsManager.createCredentialsDirIfNotExists() - const credentialsPath = credentialsManager.getCredentialsPath() - fs.writeFileSync(credentialsPath, `[${DEFAULT_PROFILE}]\napi_key=\n`) - - expect(() => credentialsManager.getProfile(DEFAULT_PROFILE)).to.throw(/has no API key/) - }) - - it('should return profile credentials if valid', () => { - credentialsManager.saveProfile(DEFAULT_PROFILE, 'test-key-123') - - const credentials = credentialsManager.getProfile(DEFAULT_PROFILE) - - expect(credentials).to.deep.equal({ apiKey: 'test-key-123' }) - }) - - it('should default to "default" profile if no name provided', () => { - credentialsManager.saveProfile(DEFAULT_PROFILE, 'default-key') - - const credentials = credentialsManager.getProfile() - - expect(credentials.apiKey).to.equal('default-key') - }) - }) - - describe('ensureLoggedIn', () => { - it('should return credentials if profile exists', () => { - credentialsManager.saveProfile(DEFAULT_PROFILE, 'test-key') - - const credentials = credentialsManager.getCredentials(DEFAULT_PROFILE) - - expect(credentials).to.deep.equal({ apiKey: 'test-key' }) - }) - - it('should throw error with user-friendly message if not logged in', () => { - expect(() => credentialsManager.getCredentials(DEFAULT_PROFILE)).to.throw(/Authentication required/) - }) - - it('should default to "default" profile', () => { - credentialsManager.saveProfile(DEFAULT_PROFILE, 'test-key') - - const credentials = credentialsManager.getCredentials() - - expect(credentials.apiKey).to.equal('test-key') - }) - }) - describe('listProfiles', () => { it('should return empty array if no profiles exist', () => { const profiles = credentialsManager.getProfiles()