diff --git a/package-lock.json b/package-lock.json index 860cc66b..d8b871e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -100,9 +100,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "SEE LICENSE IN LICENSE.md", "optional": true, "os": [ @@ -116,9 +113,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "SEE LICENSE IN LICENSE.md", "optional": true, "os": [ @@ -1275,9 +1269,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1295,9 +1286,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1315,9 +1303,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1335,9 +1320,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2116,9 +2098,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2133,9 +2112,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2150,9 +2126,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2167,9 +2140,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2184,9 +2154,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2201,9 +2168,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2218,9 +2182,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2235,9 +2196,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6940,9 +6898,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -6964,9 +6919,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ diff --git a/src/bin/cli.ts b/src/bin/cli.ts index f19dd77c..c3154be3 100644 --- a/src/bin/cli.ts +++ b/src/bin/cli.ts @@ -21,7 +21,7 @@ import { findLocalCommand, findLocalCommandTakingPositional, } from 'lib/commands/registry.js' -import { getConfigStore } from 'lib/config/index.js' +import { getConfig } from 'lib/config/index.js' import { type CliContext, resolveAuth } from 'lib/context.js' import { tokenEnvVar } from 'lib/env.js' import { reportErrorAndExit } from 'lib/errors.js' @@ -41,7 +41,7 @@ import { renderHelp } from 'lib/render/help.js' import seamapiCliVersion from 'lib/version.js' async function cli(args: ParsedArgs, argv: string[]) { - const config = getConfigStore() + const config = getConfig() const output = getOutput() // Scoped to this one command, and read wherever auth resolves, so they are @@ -158,7 +158,7 @@ async function cli(args: ParsedArgs, argv: string[]) { } const useRemoteApiDefs = - args['remote_api_defs'] ?? config.get('use_remote_api_defs') + args['remote_api_defs'] ?? config.getUseRemoteApiDefs() const blueprint = await getApiBlueprint({ useRemoteDefinitions: useRemoteApiDefs ?? false, diff --git a/src/lib/auth/operations.ts b/src/lib/auth/operations.ts index 85e28dfd..d3b06f00 100644 --- a/src/lib/auth/operations.ts +++ b/src/lib/auth/operations.ts @@ -1,4 +1,4 @@ -import { type ConfigStore, getConfigStore } from 'lib/config/index.js' +import { type CliConfig, getConfig } from 'lib/config/index.js' import { type AuthContext, resolveAuth } from 'lib/context.js' import { assertEnvVarUnset, @@ -57,7 +57,7 @@ export const assertMutable = ( */ export const login = async ( token: string, - config: ConfigStore = getConfigStore(), + config: CliConfig = getConfig(), validate: typeof validateToken = validateToken, ): Promise => { const auth = resolveAuth(config) @@ -68,30 +68,27 @@ export const login = async ( await validate(token, auth.workspaceId ?? undefined) - config.set(`${auth.endpoint}.pat`, token) + config.setToken(auth.endpoint, token) // The selection belongs to whoever was logged in before. - config.delete('current_workspace_id') + config.unsetWorkspace() } /** Store the token for the current endpoint, e.g., one just prompted for. */ export const storeToken = ( token: string, - config: ConfigStore = getConfigStore(), + config: CliConfig = getConfig(), ): void => { const auth = resolveAuth(config) assertMutable(auth, 'token', 'log in') - config.set(`${auth.endpoint}.pat`, token) + config.setToken(auth.endpoint, token) } /** Remove the stored token and workspace selection. */ -export const logout = (config: ConfigStore = getConfigStore()): void => { +export const logout = (config: CliConfig = getConfig()): void => { const auth = resolveAuth(config) assertMutable(auth, 'token', 'log out') - config.delete(`${auth.endpoint}.pat`) - // Configs written before tokens were stored per endpoint may still hold an - // un-namespaced token, so drop that too. - config.delete('pat') - config.delete('current_workspace_id') + config.unsetToken(auth.endpoint) + config.unsetWorkspace() } /** @@ -101,36 +98,25 @@ export const logout = (config: ConfigStore = getConfigStore()): void => { */ export const selectEndpoint = ( endpoint: string, - config: ConfigStore = getConfigStore(), + config: CliConfig = getConfig(), ): void => { assertMutable(resolveAuth(config), 'endpoint', 'select an endpoint') - storeEndpoint(endpoint, config) + config.setEndpoint(endpoint) } /** Store the workspace requests are made against. */ export const selectWorkspace = ( workspaceId: string, - config: ConfigStore = getConfigStore(), + config: CliConfig = getConfig(), ): void => { assertMutable(resolveAuth(config), 'workspaceId', 'select a workspace') - config.set('current_workspace_id', workspaceId) + config.setWorkspace(workspaceId) } /** Store whether API definitions come from the endpoint instead of npm. */ export const setUseRemoteApiDefs = ( useRemoteApiDefs: boolean, - config: ConfigStore = getConfigStore(), + config: CliConfig = getConfig(), ): void => { - config.set('use_remote_api_defs', useRemoteApiDefs) -} - -/** - * Write the endpoint, dropping what belonged to the previous one: the - * workspace selection, and any value left under the legacy `server` key that - * {@link resolveAuth} would otherwise still fall back to. - */ -const storeEndpoint = (endpoint: string, config: ConfigStore): void => { - config.set('endpoint', endpoint) - config.delete('server') - config.delete('current_workspace_id') + config.setUseRemoteApiDefs(useRemoteApiDefs) } diff --git a/src/lib/blueprint/source-npm.ts b/src/lib/blueprint/source-npm.ts index 9ca623d3..f3096fef 100644 --- a/src/lib/blueprint/source-npm.ts +++ b/src/lib/blueprint/source-npm.ts @@ -3,9 +3,9 @@ import { join } from 'node:path' import { pathToFileURL } from 'node:url' import type { Blueprint, TypesModuleInput } from '@seamapi/blueprint' -import envPaths from 'env-paths' import { extract } from 'tar' +import { rootPaths } from 'lib/config/index.js' import { withLoading } from 'lib/output/with-loading.js' import { @@ -38,8 +38,7 @@ export const getBlueprint = async ( options: GetBlueprintOptions = {}, ): Promise => { const update = options.update ?? false - const cacheDirectory = - options.cacheDirectory ?? envPaths('seam', { suffix: '' }).cache + const cacheDirectory = options.cacheDirectory ?? rootPaths.cache const cacheFile = getCacheFile(cacheDirectory) const blueprintVersion = await getBlueprintVersion() diff --git a/src/lib/config/cli-config.ts b/src/lib/config/cli-config.ts new file mode 100644 index 00000000..88acc271 --- /dev/null +++ b/src/lib/config/cli-config.ts @@ -0,0 +1,83 @@ +import type { ConfigStore } from './config-store.js' + +/** Tokens are stored per endpoint, e.g. `https://connect.getseam.com.pat`. */ +const tokenKey = (endpoint: string): `${string}.pat` => `${endpoint}.pat` + +/** + * The CLI's configuration, in the terms the CLI thinks in. Keys are named + * here alone: a caller says what it wants stored, not where it goes. + */ +export interface CliConfig { + readonly path: string + getEndpoint: () => string | null + setEndpoint: (endpoint: string) => void + getToken: (endpoint: string) => string | null + setToken: (endpoint: string, token: string) => void + unsetToken: (endpoint: string) => void + getWorkspace: () => string | null + setWorkspace: (workspaceId: string) => void + unsetWorkspace: () => void + getUseRemoteApiDefs: () => boolean | null + setUseRemoteApiDefs: (useRemoteApiDefs: boolean) => void +} + +export const createCliConfig = (store: ConfigStore): CliConfig => ({ + get path() { + return store.path + }, + + getEndpoint: () => + // `server` is what an older CLI called the endpoint. + readString(store.get('endpoint')) ?? readString(store.get('server')), + + /** + * Store the endpoint, dropping what belonged to the previous one: the + * workspace selection, and any value left under the legacy key that + * {@link CliConfig.getEndpoint} would otherwise still fall back to. + */ + setEndpoint: (endpoint) => { + store.set('endpoint', endpoint) + store.delete('server') + store.delete('current_workspace_id') + }, + + getToken: (endpoint) => readString(store.get(tokenKey(endpoint))), + + setToken: (endpoint, token) => { + store.set(tokenKey(endpoint), token) + }, + + unsetToken: (endpoint) => { + store.delete(tokenKey(endpoint)) + // Configs written before tokens were stored per endpoint may still hold + // an un-namespaced one. + store.delete('pat') + }, + + getWorkspace: () => readString(store.get('current_workspace_id')), + + setWorkspace: (workspaceId) => { + store.set('current_workspace_id', workspaceId) + }, + + unsetWorkspace: () => { + store.delete('current_workspace_id') + }, + + getUseRemoteApiDefs: () => { + const useRemoteApiDefs = store.get('use_remote_api_defs') + return typeof useRemoteApiDefs === 'boolean' ? useRemoteApiDefs : null + }, + + setUseRemoteApiDefs: (useRemoteApiDefs) => { + store.set('use_remote_api_defs', useRemoteApiDefs) + }, +}) + +const readString = (value: unknown): string | null => { + if (typeof value !== 'string') return null + + const trimmedValue = value.trim() + + return trimmedValue === '' ? null : trimmedValue +} diff --git a/src/lib/config/config-store.ts b/src/lib/config/config-store.ts index 07c6fd7a..9fef04df 100644 --- a/src/lib/config/config-store.ts +++ b/src/lib/config/config-store.ts @@ -3,12 +3,15 @@ import { join } from 'node:path' import Configstore from 'configstore' import envPaths from 'env-paths' +import { type CliConfig, createCliConfig } from './cli-config.js' import { migrateConfigStore } from './migrate.js' import { isStateKey, mergeConfig, splitConfig } from './values.js' const configFileName = 'cli.json' const legacyConfigStoreId = 'seam-cli' -const paths = envPaths('seam', { suffix: '' }) + +/** Every directory the CLI keeps files in, and what it mounts keeps its own. */ +export const rootPaths = envPaths('seam', { suffix: '' }) /** * What a config store can do, regardless of where it keeps the values. @@ -28,21 +31,21 @@ export interface ConfigStore { clear: () => void } -let configStore: ConfigStore | null = null +let config: CliConfig | null = null -export const getConfigStore = (): ConfigStore => { - configStore ??= createConfigStore() - return configStore +export const getConfig = (): CliConfig => { + config ??= createCliConfig(createConfigStore()) + return config } -/** Replace the store, e.g., with an in-memory one for a test. */ -export const setConfigStore = (store: ConfigStore): void => { - configStore = store +/** Replace the config, e.g., with an in-memory one for a test. */ +export const setConfig = (nextConfig: CliConfig): void => { + config = nextConfig } -/** Drop the current store so the next read builds the real one. */ -export const resetConfigStore = (): void => { - configStore = null +/** Drop the current config so the next read builds the real one. */ +export const resetConfig = (): void => { + config = null } const createConfigStore = (): PersistentConfigStore => { @@ -121,11 +124,11 @@ export class PersistentConfigStore implements ConfigStore { } const getConfigPath = (): string => { - return join(paths.config, configFileName) + return join(rootPaths.config, configFileName) } const getStateConfigPath = (): string => { - return join(paths.log, configFileName) + return join(rootPaths.log, configFileName) } const isRecord = (value: unknown): value is Record => { diff --git a/src/lib/config/index.ts b/src/lib/config/index.ts index 380110c5..89ea419e 100644 --- a/src/lib/config/index.ts +++ b/src/lib/config/index.ts @@ -1,8 +1,13 @@ +export { type CliConfig, createCliConfig } from './cli-config.js' export { type ConfigStore, - getConfigStore, + getConfig, type PersistentConfigStore, - resetConfigStore, - setConfigStore, + resetConfig, + rootPaths, + setConfig, } from './config-store.js' -export { createMemoryConfigStore } from './memory-config-store.js' +export { + createMemoryConfig, + createMemoryConfigStore, +} from './memory-config-store.js' diff --git a/src/lib/config/memory-config-store.ts b/src/lib/config/memory-config-store.ts index 372d08df..9eaa8f9e 100644 --- a/src/lib/config/memory-config-store.ts +++ b/src/lib/config/memory-config-store.ts @@ -1,3 +1,4 @@ +import { type CliConfig, createCliConfig } from './cli-config.js' import type { ConfigStore } from './config-store.js' /** @@ -60,3 +61,11 @@ export class MemoryConfigStore implements ConfigStore { export const createMemoryConfigStore = ( initialValues: Record = {}, ): ConfigStore => new MemoryConfigStore(initialValues) + +/** + * A config held in memory, for tests. Values may be seeded by key, e.g., to + * stand for a config an older CLI wrote. + */ +export const createMemoryConfig = ( + initialValues: Record = {}, +): CliConfig => createCliConfig(createMemoryConfigStore(initialValues)) diff --git a/src/lib/context.ts b/src/lib/context.ts index 4c959291..d7f422eb 100644 --- a/src/lib/context.ts +++ b/src/lib/context.ts @@ -1,6 +1,6 @@ import type { Interactivity } from './args/parse.js' import type { ApiBlueprint } from './blueprint/index.js' -import { type ConfigStore, getConfigStore } from './config/index.js' +import { type CliConfig, getConfig } from './config/index.js' import { getEndpointFromEnv, getTokenFromEnv, @@ -32,30 +32,21 @@ export interface AuthContext { workspaceIdSource: Exclude | null } -export const resolveAuth = ( - config: ConfigStore = getConfigStore(), -): AuthContext => { +export const resolveAuth = (config: CliConfig = getConfig()): AuthContext => { const { endpoint: flagEndpoint, workspaceId: flagWorkspaceId } = getAuthOverrides() const envEndpoint = getEndpointFromEnv() - // Configs written before the endpoint was called one still hold it under - // `server`, so fall back to that key rather than silently resetting them. - const storedEndpoint = config.get('endpoint') ?? config.get('server') - const endpoint = - flagEndpoint ?? - envEndpoint ?? - (typeof storedEndpoint === 'string' ? storedEndpoint : null) + const storedEndpoint = config.getEndpoint() + const endpoint = flagEndpoint ?? envEndpoint ?? storedEndpoint const envToken = getTokenFromEnv() // The token is stored per endpoint, so an overridden endpoint is read with // the token belonging to it rather than the one it replaced. - const storedToken = readString( - config.get(`${endpoint ?? defaultEndpoint}.pat`), - ) + const storedToken = config.getToken(endpoint ?? defaultEndpoint) const envWorkspaceId = getWorkspaceIdFromEnv() - const storedWorkspaceId = readString(config.get('current_workspace_id')) + const storedWorkspaceId = config.getWorkspace() const workspaceId = flagWorkspaceId ?? envWorkspaceId ?? storedWorkspaceId return { @@ -88,7 +79,7 @@ export const resolveAuth = ( * shape it acts on, and how it may interact with the user. */ export interface CliContext { - config: ConfigStore + config: CliConfig auth: AuthContext output: Output blueprint: ApiBlueprint @@ -96,11 +87,3 @@ export interface CliContext { /** The Seam API, constructed on first use and shared for the run. */ api: () => Promise } - -const readString = (value: unknown): string | null => { - if (typeof value !== 'string') return null - - const trimmedValue = value.trim() - - return trimmedValue === '' ? null : trimmedValue -} diff --git a/src/lib/interactions/endpoint-selection.ts b/src/lib/interactions/endpoint-selection.ts index 3c77ba31..2a9e05a6 100644 --- a/src/lib/interactions/endpoint-selection.ts +++ b/src/lib/interactions/endpoint-selection.ts @@ -1,11 +1,11 @@ import { assertMutable, selectEndpoint } from 'lib/auth/operations.js' -import { getConfigStore } from 'lib/config/index.js' +import { getConfig } from 'lib/config/index.js' import { resolveAuth } from 'lib/context.js' import { getOutput } from 'lib/output/get-output.js' import { promptAutocomplete } from 'lib/prompt.js' export async function interactForEndpointSelection() { - const config = getConfigStore() + const config = getConfig() assertMutable(resolveAuth(config), 'endpoint', 'select an endpoint') const endpoints = ['http://localhost:3020', 'https://connect.getseam.com'] diff --git a/src/lib/interactions/login.ts b/src/lib/interactions/login.ts index 066836b7..a3102ad7 100644 --- a/src/lib/interactions/login.ts +++ b/src/lib/interactions/login.ts @@ -3,7 +3,7 @@ import chalk from 'chalk' import { assertMutable, storeToken } from 'lib/auth/operations.js' import { validateToken } from 'lib/auth/validate-token.js' -import { getConfigStore } from 'lib/config/index.js' +import { getConfig } from 'lib/config/index.js' import { resolveAuth } from 'lib/context.js' import { getOutput } from 'lib/output/get-output.js' import { withLoading } from 'lib/output/with-loading.js' @@ -12,7 +12,7 @@ import { promptText } from 'lib/prompt.js' import { interactForWorkspaceId } from './workspace-id.js' export const interactForLogin = async () => { - const config = getConfigStore() + const config = getConfig() const output = getOutput() const auth = resolveAuth(config) diff --git a/src/lib/interactions/workspace-id.ts b/src/lib/interactions/workspace-id.ts index 45c886c7..8669a7d2 100644 --- a/src/lib/interactions/workspace-id.ts +++ b/src/lib/interactions/workspace-id.ts @@ -1,14 +1,14 @@ import { SeamHttpWithoutWorkspace } from '@seamapi/http/connect' import { assertMutable, selectWorkspace } from 'lib/auth/operations.js' -import { getConfigStore } from 'lib/config/index.js' +import { getConfig } from 'lib/config/index.js' import { resolveAuth } from 'lib/context.js' import { getSeamMultiWorkspace } from 'lib/http/client.js' import { withLoading } from 'lib/output/with-loading.js' import { promptAutocomplete } from 'lib/prompt.js' export const interactForWorkspaceId = async (personalAccessToken?: string) => { - const config = getConfigStore() + const config = getConfig() // Refuse before prompting: nothing selected here could be stored. assertMutable(resolveAuth(config), 'workspaceId', 'select a workspace') diff --git a/test/auth/operations.test.ts b/test/auth/operations.test.ts index f04b2de9..04165115 100644 --- a/test/auth/operations.test.ts +++ b/test/auth/operations.test.ts @@ -7,7 +7,11 @@ import { selectWorkspace, storeToken, } from 'lib/auth/operations.js' -import { createMemoryConfigStore } from 'lib/config/memory-config-store.js' +import { createCliConfig } from 'lib/config/cli-config.js' +import { + createMemoryConfig, + createMemoryConfigStore, +} from 'lib/config/memory-config-store.js' import { endpointEnvVar, tokenEnvVar, workspaceIdEnvVar } from 'lib/env.js' import { resetAuthOverrides, setAuthOverrides } from 'lib/overrides.js' @@ -42,84 +46,88 @@ beforeEach(clearEnv) afterEach(clearEnv) test('login: stores a validated token under the current endpoint', async () => { - const store = createMemoryConfigStore({ endpoint }) + const config = createMemoryConfig({ endpoint }) const { validate, validated } = createValidate() - await login('seam_apikey1_stored', store, validate) + await login('seam_apikey1_stored', config, validate) expect(validated).toEqual([ { token: 'seam_apikey1_stored', workspaceId: undefined }, ]) - expect(store.get(`${endpoint}.pat`)).toBe('seam_apikey1_stored') + expect(config.getToken(endpoint)).toBe('seam_apikey1_stored') }) test('login: stores the token under an overridden endpoint without selecting it', async () => { setAuthOverrides({ endpoint: 'https://other.example.com', workspaceId: null }) - const store = createMemoryConfigStore({ endpoint }) + const config = createMemoryConfig({ endpoint }) const { validate } = createValidate() - await login('seam_apikey1_stored', store, validate) + await login('seam_apikey1_stored', config, validate) - expect(store.get('https://other.example.com.pat')).toBe('seam_apikey1_stored') + expect(config.getToken('https://other.example.com')).toBe( + 'seam_apikey1_stored', + ) // The override scopes the command: the selection is left as it was. - expect(store.get('endpoint')).toBe(endpoint) - expect(store.has(`${endpoint}.pat`)).toBe(false) + expect(config.getEndpoint()).toBe(endpoint) + expect(config.getToken(endpoint)).toBeNull() }) test('login: a new login clears the previous workspace selection', async () => { - const store = createMemoryConfigStore({ + const config = createMemoryConfig({ endpoint, current_workspace_id: 'workspace1', }) const { validate } = createValidate() - await login('seam_apikey1_stored', store, validate) + await login('seam_apikey1_stored', config, validate) - expect(store.has('current_workspace_id')).toBe(false) + expect(config.getWorkspace()).toBeNull() }) test('login: validates against the workspace in effect without storing it', async () => { setAuthOverrides({ endpoint: null, workspaceId: 'workspace1' }) - const store = createMemoryConfigStore({ endpoint }) + const config = createMemoryConfig({ endpoint }) const { validate, validated } = createValidate() - await login('seam_at1_stored', store, validate) + await login('seam_at1_stored', config, validate) expect(validated).toEqual([ { token: 'seam_at1_stored', workspaceId: 'workspace1' }, ]) - expect(store.has('current_workspace_id')).toBe(false) + expect(config.getWorkspace()).toBeNull() }) test(`login: refuses while ${tokenEnvVar} is set, before storing anything`, async () => { process.env[tokenEnvVar] = 'seam_apikey1_env' - const store = createMemoryConfigStore({ endpoint }) + const config = createMemoryConfig({ endpoint }) const { validate, validated } = createValidate() - await expect(login('seam_apikey1_stored', store, validate)).rejects.toThrow( + await expect(login('seam_apikey1_stored', config, validate)).rejects.toThrow( `Cannot log in while ${tokenEnvVar} is set`, ) - expect(store.has(`${endpoint}.pat`)).toBe(false) + expect(config.getToken(endpoint)).toBeNull() expect(validated).toEqual([]) }) test(`login: stores under the endpoint ${endpointEnvVar} names`, async () => { process.env[endpointEnvVar] = 'https://other.example.com' - const store = createMemoryConfigStore({ endpoint }) + const config = createMemoryConfig({ endpoint }) const { validate } = createValidate() - await login('seam_apikey1_stored', store, validate) + await login('seam_apikey1_stored', config, validate) - expect(store.get('https://other.example.com.pat')).toBe('seam_apikey1_stored') - expect(store.get('endpoint')).toBe(endpoint) + expect(config.getToken('https://other.example.com')).toBe( + 'seam_apikey1_stored', + ) + expect(config.getEndpoint()).toBe(endpoint) }) test('storeToken: stores under the current endpoint without validating', () => { - const store = createMemoryConfigStore({ endpoint }) + const config = createMemoryConfig({ endpoint }) - storeToken('seam_apikey1_stored', store) + storeToken('seam_apikey1_stored', config) - expect(store.get(`${endpoint}.pat`)).toBe('seam_apikey1_stored') + expect(config.getToken(endpoint)).toBe('seam_apikey1_stored') }) test('logout: removes the stored token, legacy token, and workspace', () => { @@ -129,67 +137,70 @@ test('logout: removes the stored token, legacy token, and workspace', () => { pat: 'seam_apikey1_legacy', current_workspace_id: 'workspace1', }) + const config = createCliConfig(store) - logout(store) + logout(config) - expect(store.has(`${endpoint}.pat`)).toBe(false) + expect(config.getToken(endpoint)).toBeNull() + expect(config.getWorkspace()).toBeNull() + // Nothing reads the un-namespaced token, so it is asserted where it lives. expect(store.has('pat')).toBe(false) - expect(store.has('current_workspace_id')).toBe(false) }) test(`logout: refuses while ${tokenEnvVar} is set`, () => { process.env[tokenEnvVar] = 'seam_apikey1_env' - const store = createMemoryConfigStore({ + const config = createMemoryConfig({ endpoint, [`${endpoint}.pat`]: 'seam_apikey1_stored', }) expect(() => { - logout(store) + logout(config) }).toThrow(`Cannot log out while ${tokenEnvVar} is set`) - expect(store.get(`${endpoint}.pat`)).toBe('seam_apikey1_stored') + expect(config.getToken(endpoint)).toBe('seam_apikey1_stored') }) test('selectEndpoint: stores the endpoint and clears the workspace', () => { - const store = createMemoryConfigStore({ current_workspace_id: 'workspace1' }) + const config = createMemoryConfig({ current_workspace_id: 'workspace1' }) - selectEndpoint(endpoint, store) + selectEndpoint(endpoint, config) - expect(store.get('endpoint')).toBe(endpoint) - expect(store.has('current_workspace_id')).toBe(false) + expect(config.getEndpoint()).toBe(endpoint) + expect(config.getWorkspace()).toBeNull() }) test('selectEndpoint: drops an endpoint left under the legacy key', () => { const store = createMemoryConfigStore({ server: 'https://old.example.com' }) + const config = createCliConfig(store) - selectEndpoint(endpoint, store) + selectEndpoint(endpoint, config) - expect(store.get('endpoint')).toBe(endpoint) + expect(config.getEndpoint()).toBe(endpoint) expect(store.has('server')).toBe(false) }) test(`selectEndpoint: refuses while ${endpointEnvVar} is set`, () => { process.env[endpointEnvVar] = 'http://localhost:3020' - const store = createMemoryConfigStore() + const config = createMemoryConfig() expect(() => { - selectEndpoint(endpoint, store) + selectEndpoint(endpoint, config) }).toThrow(`Cannot select an endpoint while ${endpointEnvVar} is set`) }) test('selectWorkspace: stores the workspace selection', () => { - const store = createMemoryConfigStore() + const config = createMemoryConfig() - selectWorkspace('workspace1', store) + selectWorkspace('workspace1', config) - expect(store.get('current_workspace_id')).toBe('workspace1') + expect(config.getWorkspace()).toBe('workspace1') }) test(`selectWorkspace: refuses while ${workspaceIdEnvVar} is set`, () => { process.env[workspaceIdEnvVar] = 'workspace_env' - const store = createMemoryConfigStore() + const config = createMemoryConfig() expect(() => { - selectWorkspace('workspace1', store) + selectWorkspace('workspace1', config) }).toThrow(`Cannot select a workspace while ${workspaceIdEnvVar} is set`) }) diff --git a/test/context.test.ts b/test/context.test.ts index ff9acccf..4a1313fd 100644 --- a/test/context.test.ts +++ b/test/context.test.ts @@ -1,13 +1,13 @@ import { afterEach, beforeEach, expect, test } from 'vitest' -import { createMemoryConfigStore } from 'lib/config/memory-config-store.js' +import { createMemoryConfig } from 'lib/config/memory-config-store.js' import { resolveAuth } from 'lib/context.js' import { endpointEnvVar, tokenEnvVar, workspaceIdEnvVar } from 'lib/env.js' import { resetAuthOverrides, setAuthOverrides } from 'lib/overrides.js' const endpoint = 'https://connect.example.com' -const store = createMemoryConfigStore +const config = createMemoryConfig /** The flags for one command, as `bin/cli.ts` sets them from the arguments. */ const overrideWith = (overrides: { @@ -31,21 +31,21 @@ beforeEach(clearEnv) afterEach(clearEnv) test('resolveAuth: reads the stored endpoint', () => { - const auth = resolveAuth(store({ endpoint })) + const auth = resolveAuth(config({ endpoint })) expect(auth.endpoint).toBe(endpoint) expect(auth.endpointSource).toBe('config') }) test('resolveAuth: defaults the endpoint to Seam', () => { - const auth = resolveAuth(store()) + const auth = resolveAuth(config()) expect(auth.endpoint).toBe('https://connect.getseam.com') expect(auth.endpointSource).toBe('default') }) test('resolveAuth: reads an endpoint stored under the legacy key', () => { - const auth = resolveAuth(store({ server: endpoint })) + const auth = resolveAuth(config({ server: endpoint })) expect(auth.endpoint).toBe(endpoint) expect(auth.endpointSource).toBe('config') @@ -53,7 +53,7 @@ test('resolveAuth: reads an endpoint stored under the legacy key', () => { test('resolveAuth: the stored endpoint wins over the legacy key', () => { const auth = resolveAuth( - store({ endpoint, server: 'https://old.example.com' }), + config({ endpoint, server: 'https://old.example.com' }), ) expect(auth.endpoint).toBe(endpoint) @@ -62,7 +62,7 @@ test('resolveAuth: the stored endpoint wins over the legacy key', () => { test('resolveAuth: --endpoint wins over the stored endpoint', () => { overrideWith({ endpoint: 'http://localhost:3020' }) - const auth = resolveAuth(store({ endpoint })) + const auth = resolveAuth(config({ endpoint })) expect(auth.endpoint).toBe('http://localhost:3020') expect(auth.endpointSource).toBe('flag') @@ -72,7 +72,7 @@ test(`resolveAuth: --endpoint wins over ${endpointEnvVar}`, () => { process.env[endpointEnvVar] = 'http://localhost:3020' overrideWith({ endpoint: 'http://localhost:9999' }) - const auth = resolveAuth(store({ endpoint })) + const auth = resolveAuth(config({ endpoint })) expect(auth.endpoint).toBe('http://localhost:9999') expect(auth.endpointSource).toBe('flag') @@ -82,7 +82,7 @@ test('resolveAuth: reads the token stored for an overridden endpoint', () => { overrideWith({ endpoint: 'http://localhost:3020' }) const auth = resolveAuth( - store({ + config({ endpoint, [`${endpoint}.pat`]: 'seam_apikey1_stored', 'http://localhost:3020.pat': 'seam_apikey1_local', @@ -95,7 +95,7 @@ test('resolveAuth: reads the token stored for an overridden endpoint', () => { test('resolveAuth: --workspace-id wins over the stored selection', () => { overrideWith({ workspaceId: 'workspace2' }) - const auth = resolveAuth(store({ current_workspace_id: 'workspace1' })) + const auth = resolveAuth(config({ current_workspace_id: 'workspace1' })) expect(auth.workspaceId).toBe('workspace2') expect(auth.workspaceIdSource).toBe('flag') @@ -105,7 +105,7 @@ test(`resolveAuth: --workspace-id wins over ${workspaceIdEnvVar}`, () => { process.env[workspaceIdEnvVar] = 'workspace2' overrideWith({ workspaceId: 'workspace3' }) - const auth = resolveAuth(store({ current_workspace_id: 'workspace1' })) + const auth = resolveAuth(config({ current_workspace_id: 'workspace1' })) expect(auth.workspaceId).toBe('workspace3') expect(auth.workspaceIdSource).toBe('flag') @@ -114,7 +114,7 @@ test(`resolveAuth: --workspace-id wins over ${workspaceIdEnvVar}`, () => { test(`resolveAuth: ${endpointEnvVar} wins over the stored endpoint`, () => { process.env[endpointEnvVar] = 'http://localhost:3020' - const auth = resolveAuth(store({ endpoint })) + const auth = resolveAuth(config({ endpoint })) expect(auth.endpoint).toBe('http://localhost:3020') expect(auth.endpointSource).toBe('env') @@ -123,13 +123,13 @@ test(`resolveAuth: ${endpointEnvVar} wins over the stored endpoint`, () => { test(`resolveAuth: ${endpointEnvVar} is used without a stored endpoint`, () => { process.env[endpointEnvVar] = 'http://localhost:3020' - expect(resolveAuth(store()).endpoint).toBe('http://localhost:3020') + expect(resolveAuth(config()).endpoint).toBe('http://localhost:3020') }) test(`resolveAuth: ignores an empty ${endpointEnvVar}`, () => { process.env[endpointEnvVar] = '' - const auth = resolveAuth(store({ endpoint })) + const auth = resolveAuth(config({ endpoint })) expect(auth.endpoint).toBe(endpoint) expect(auth.endpointSource).toBe('config') @@ -137,7 +137,7 @@ test(`resolveAuth: ignores an empty ${endpointEnvVar}`, () => { test('resolveAuth: reads the token stored for the current endpoint', () => { const auth = resolveAuth( - store({ + config({ endpoint, [`${endpoint}.pat`]: 'seam_apikey1_stored', }), @@ -151,7 +151,7 @@ test(`resolveAuth: the token stored for ${endpointEnvVar} wins over the stored e process.env[endpointEnvVar] = 'http://localhost:3020' const auth = resolveAuth( - store({ + config({ endpoint, [`${endpoint}.pat`]: 'seam_apikey1_stored', 'http://localhost:3020.pat': 'seam_apikey1_local', @@ -165,7 +165,7 @@ test(`resolveAuth: ${tokenEnvVar} wins over the stored token`, () => { process.env[tokenEnvVar] = 'seam_apikey1_env' const auth = resolveAuth( - store({ + config({ endpoint, [`${endpoint}.pat`]: 'seam_apikey1_stored', }), @@ -178,14 +178,14 @@ test(`resolveAuth: ${tokenEnvVar} wins over the stored token`, () => { test(`resolveAuth: ${tokenEnvVar} is used without a stored token`, () => { process.env[tokenEnvVar] = 'seam_apikey1_env' - expect(resolveAuth(store()).token).toBe('seam_apikey1_env') + expect(resolveAuth(config()).token).toBe('seam_apikey1_env') }) test(`resolveAuth: ignores an empty ${tokenEnvVar}`, () => { process.env[tokenEnvVar] = ' ' const auth = resolveAuth( - store({ + config({ endpoint, [`${endpoint}.pat`]: 'seam_apikey1_stored', }), @@ -195,14 +195,14 @@ test(`resolveAuth: ignores an empty ${tokenEnvVar}`, () => { }) test('resolveAuth: token is null when nothing is set', () => { - const auth = resolveAuth(store()) + const auth = resolveAuth(config()) expect(auth.token).toBe(null) expect(auth.tokenSource).toBe(null) }) test('resolveAuth: reads the stored workspace selection', () => { - const auth = resolveAuth(store({ current_workspace_id: 'workspace1' })) + const auth = resolveAuth(config({ current_workspace_id: 'workspace1' })) expect(auth.workspaceId).toBe('workspace1') expect(auth.workspaceIdSource).toBe('config') @@ -211,7 +211,7 @@ test('resolveAuth: reads the stored workspace selection', () => { test(`resolveAuth: ${workspaceIdEnvVar} wins over the stored selection`, () => { process.env[workspaceIdEnvVar] = 'workspace2' - const auth = resolveAuth(store({ current_workspace_id: 'workspace1' })) + const auth = resolveAuth(config({ current_workspace_id: 'workspace1' })) expect(auth.workspaceId).toBe('workspace2') expect(auth.workspaceIdSource).toBe('env') @@ -220,19 +220,19 @@ test(`resolveAuth: ${workspaceIdEnvVar} wins over the stored selection`, () => { test(`resolveAuth: ${workspaceIdEnvVar} is used without a stored selection`, () => { process.env[workspaceIdEnvVar] = 'workspace2' - expect(resolveAuth(store()).workspaceId).toBe('workspace2') + expect(resolveAuth(config()).workspaceId).toBe('workspace2') }) test(`resolveAuth: ignores an empty ${workspaceIdEnvVar}`, () => { process.env[workspaceIdEnvVar] = '' expect( - resolveAuth(store({ current_workspace_id: 'workspace1' })).workspaceId, + resolveAuth(config({ current_workspace_id: 'workspace1' })).workspaceId, ).toBe('workspace1') }) test('resolveAuth: workspace is null when nothing is set', () => { - const auth = resolveAuth(store()) + const auth = resolveAuth(config()) expect(auth.workspaceId).toBe(null) expect(auth.workspaceIdSource).toBe(null) @@ -242,7 +242,7 @@ test('resolveAuth: each value resolves on its own', () => { process.env[workspaceIdEnvVar] = 'workspace2' const auth = resolveAuth( - store({ + config({ endpoint, [`${endpoint}.pat`]: 'seam_apikey1_stored', current_workspace_id: 'workspace1',