diff --git a/packages/core/src/awsService/redshift/activation.ts b/packages/core/src/awsService/redshift/activation.ts index 588ed8ca736..58dea2e0075 100644 --- a/packages/core/src/awsService/redshift/activation.ts +++ b/packages/core/src/awsService/redshift/activation.ts @@ -10,14 +10,14 @@ import { RedshiftNotebookController } from './notebook/redshiftNotebookControlle import { CellStatusBarItemProvider } from './notebook/cellStatusBarItemProvider' import { Commands } from '../../shared/vscode/commands2' import { NotebookConnectionWizard, RedshiftNodeConnectionWizard } from './wizards/connectionWizard' -import { ConnectionParams, ConnectionType } from './models/models' +import { deleteConnection, ConnectionParams, ConnectionType } from './models/models' import { DefaultRedshiftClient } from '../../shared/clients/redshiftClient' import { localize } from '../../shared/utilities/vsCodeUtils' import { RedshiftWarehouseNode } from './explorer/redshiftWarehouseNode' import { ToolkitError } from '../../shared/errors' -import { deleteConnection, updateConnectionParamsState } from './explorer/redshiftState' import { showViewLogsMessage } from '../../shared/utilities/messages' import { showConnectionMessage } from './messageUtils' +import globals from '../../shared/extensionGlobals' export async function activate(ctx: ExtContext): Promise { if ('NotebookEdit' in vscode) { @@ -123,7 +123,10 @@ function getEditConnectionHandler() { connectionParams.secret = secretArnFetched } redshiftWarehouseNode.setConnectionParams(connectionParams) - await updateConnectionParamsState(redshiftWarehouseNode.arn, redshiftWarehouseNode.connectionParams) + await globals.globalState.saveRedshiftConnection( + redshiftWarehouseNode.arn, + redshiftWarehouseNode.connectionParams + ) await vscode.commands.executeCommand('aws.refreshAwsExplorerNode', redshiftWarehouseNode) } } catch (error) { @@ -135,7 +138,7 @@ function getEditConnectionHandler() { function getDeleteConnectionHandler() { return async (redshiftWarehouseNode: RedshiftWarehouseNode) => { redshiftWarehouseNode.connectionParams = undefined - await updateConnectionParamsState(redshiftWarehouseNode.arn, deleteConnection) + await globals.globalState.saveRedshiftConnection(redshiftWarehouseNode.arn, deleteConnection) await vscode.commands.executeCommand('aws.refreshAwsExplorerNode', redshiftWarehouseNode) } } diff --git a/packages/core/src/awsService/redshift/explorer/redshiftState.ts b/packages/core/src/awsService/redshift/explorer/redshiftState.ts deleted file mode 100644 index 7090cbce491..00000000000 --- a/packages/core/src/awsService/redshift/explorer/redshiftState.ts +++ /dev/null @@ -1,45 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { ConnectionParams } from '../models/models' -import globals from '../../../shared/extensionGlobals' - -const redshiftConnectionsGlobalStateKey = 'aws.redshift.connections' - -// Used to set global state so the connection wizard is not triggered during explorer node refresh after connection deletion -export const deleteConnection = 'DELETE_CONNECTION' - -/** - * Update the connectionParams of a specified redshiftWarehouse in the global state - * @param {string} redshiftWarehouseArn - the redshift warehouse ARN - * @param {ConnectionParams | undefined | string} connectionParams - the input connectionParams to store. The value is a string when - * the connection is deleted but the explorer node is not refreshed yet - */ -export async function updateConnectionParamsState( - redshiftWarehouseArn: string, - connectionParams: ConnectionParams | undefined | string -) { - const redshiftConnections = globals.context.globalState.get>( - redshiftConnectionsGlobalStateKey, - {} - ) - await globals.context.globalState.update(redshiftConnectionsGlobalStateKey, { - ...redshiftConnections, - [redshiftWarehouseArn]: connectionParams, - }) -} - -/** - * Get the connectionParams of a specified redshiftWarehouse from the global state - * @param {string} redshiftWarehouseArn - the redshift warehouse ARN - * @returns {ConnectionParams | undefined | string} the stored connectionParams state. The value is a string when the connection - * is deleted but the explorer node is not refreshed yet. - */ -export function getConnectionParamsState(redshiftWarehouseArn: string): ConnectionParams | undefined | string { - return globals.context.globalState.get>( - redshiftConnectionsGlobalStateKey, - {} - )[redshiftWarehouseArn] -} diff --git a/packages/core/src/awsService/redshift/explorer/redshiftWarehouseNode.ts b/packages/core/src/awsService/redshift/explorer/redshiftWarehouseNode.ts index 649a7b453b3..f61f78e1460 100644 --- a/packages/core/src/awsService/redshift/explorer/redshiftWarehouseNode.ts +++ b/packages/core/src/awsService/redshift/explorer/redshiftWarehouseNode.ts @@ -5,6 +5,7 @@ import { AWSResourceNode } from '../../../shared/treeview/nodes/awsResourceNode' import { AWSTreeNodeBase } from '../../../shared/treeview/nodes/awsTreeNodeBase' import * as vscode from 'vscode' +import globals from '../../../shared/extensionGlobals' import { RedshiftNode } from './redshiftNode' import { makeChildrenNodes } from '../../../shared/treeview/utils' import { RedshiftDatabaseNode } from './redshiftDatabaseNode' @@ -12,13 +13,12 @@ import { localize } from '../../../shared/utilities/vsCodeUtils' import { LoadMoreNode } from '../../../shared/treeview/nodes/loadMoreNode' import { ChildNodeLoader, ChildNodePage } from '../../../awsexplorer/childNodeLoader' import { DefaultRedshiftClient } from '../../../shared/clients/redshiftClient' -import { ConnectionParams, ConnectionType, RedshiftWarehouseType } from '../models/models' +import { deleteConnection, ConnectionParams, ConnectionType, RedshiftWarehouseType } from '../models/models' import { RedshiftNodeConnectionWizard } from '../wizards/connectionWizard' import { ListDatabasesResponse } from 'aws-sdk/clients/redshiftdata' import { getIcon } from '../../../shared/icons' import { AWSCommandTreeNode } from '../../../shared/treeview/nodes/awsCommandTreeNode' import { telemetry } from '../../../shared/telemetry/telemetry' -import { deleteConnection, getConnectionParamsState, updateConnectionParamsState } from './redshiftState' import { createLogsConnectionMessage, showConnectionMessage } from '../messageUtils' export class CreateNotebookNode extends AWSCommandTreeNode { @@ -50,7 +50,7 @@ export class RedshiftWarehouseNode extends AWSTreeNodeBase implements AWSResourc this.arn = redshiftWarehouse.arn this.name = redshiftWarehouse.name this.redshiftClient = parent.redshiftClient - const existingConnectionParams = getConnectionParamsState(this.arn) + const existingConnectionParams = globals.globalState.getRedshiftConnection(this.arn) if (existingConnectionParams && existingConnectionParams !== deleteConnection) { this.connectionParams = existingConnectionParams as ConnectionParams this.iconPath = getIcon('aws-redshift-cluster-connected') @@ -108,14 +108,14 @@ export class RedshiftWarehouseNode extends AWSTreeNodeBase implements AWSResourc return await makeChildrenNodes({ getChildNodes: async () => { this.childLoader.clearChildren() - const existingConnectionParams = getConnectionParamsState(this.arn) + const existingConnectionParams = globals.globalState.getRedshiftConnection(this.arn) if (existingConnectionParams && existingConnectionParams === deleteConnection) { // connection is deleted but explorer is not refreshed: return clickToEstablishConnectionNode - await updateConnectionParamsState(this.arn, undefined) + await globals.globalState.saveRedshiftConnection(this.arn, undefined) return this.getClickToEstablishConnectionNode() - } else if (existingConnectionParams && existingConnectionParams !== deleteConnection) { + } else if (existingConnectionParams) { // valid connectionParams: update the redshiftWarehouseNode - this.connectionParams = existingConnectionParams as ConnectionParams + this.connectionParams = existingConnectionParams } else { // No connectionParams: trigger connection wizard to get user input this.connectionParams = await new RedshiftNodeConnectionWizard(this).run() @@ -137,11 +137,11 @@ export class RedshiftWarehouseNode extends AWSTreeNodeBase implements AWSResourc const childNodes = await this.childLoader.getChildren() const startButtonNode = new CreateNotebookNode(this) childNodes.unshift(startButtonNode) - await updateConnectionParamsState(this.arn, this.connectionParams) + await globals.globalState.saveRedshiftConnection(this.arn, this.connectionParams) return childNodes } catch (error) { showConnectionMessage(this.redshiftWarehouse.name, error as Error) - await updateConnectionParamsState(this.arn, undefined) + await globals.globalState.saveRedshiftConnection(this.arn, undefined) return this.getRetryNode() } }, diff --git a/packages/core/src/awsService/redshift/models/models.ts b/packages/core/src/awsService/redshift/models/models.ts index 06602aca229..603cf495fb6 100644 --- a/packages/core/src/awsService/redshift/models/models.ts +++ b/packages/core/src/awsService/redshift/models/models.ts @@ -5,6 +5,10 @@ import { Region } from '../../../shared/regions/endpoints' +// Sigil treated such that the connection wizard is not triggered during explorer node refresh after +// connection deletion. +export const deleteConnection = 'DELETE_CONNECTION' as const + export class ConnectionParams { constructor( public readonly connectionType: ConnectionType, diff --git a/packages/core/src/extensionCommon.ts b/packages/core/src/extensionCommon.ts index 2f1f9c1f6c8..be5c1fcb7cc 100644 --- a/packages/core/src/extensionCommon.ts +++ b/packages/core/src/extensionCommon.ts @@ -35,7 +35,7 @@ import { LoginManager } from './auth/deprecated/loginManager' import { CredentialsStore } from './auth/credentials/store' import { initializeAwsCredentialsStatusBarItem } from './auth/ui/statusBarItem' import { RegionProvider, getEndpointsFromFetcher } from './shared/regions/regionProvider' -import { getMachineId } from './shared/vscode/env' +import { getMachineId, isAutomation } from './shared/vscode/env' import { registerCommandErrorHandler } from './shared/vscode/commands2' import { registerWebviewErrorHandler } from './webviews/server' import { showQuickStartWebview } from './shared/extensionStartup' @@ -77,7 +77,7 @@ export async function activateCommon( const homeDirLogs = await fs.init(context, homeDir => { void showViewLogsMessage(`Invalid home directory (check $HOME): "${homeDir}"`) }) - errors.init(fs.getUsername()) + errors.init(fs.getUsername(), isAutomation()) await initializeComputeRegion() globals.contextPrefix = '' //todo: disconnect supplied argument diff --git a/packages/core/src/shared/errors.ts b/packages/core/src/shared/errors.ts index e935274cc97..e3b2282b704 100644 --- a/packages/core/src/shared/errors.ts +++ b/packages/core/src/shared/errors.ts @@ -13,14 +13,15 @@ import { isNonNullable } from './utilities/tsUtils' import type * as nodefs from 'fs' import type * as os from 'os' import { CodeWhispererStreamingServiceException } from '@amzn/codewhisperer-streaming' -import { isAutomation } from './vscode/env' import { driveLetterRegex } from './utilities/pathUtils' let _username = 'unknown-user' +let _isAutomation = false -/** One-time initialization for this module. */ -export function init(username: string) { +/** Performs one-time initialization, to avoid circular dependencies. */ +export function init(username: string, isAutomation: boolean) { _username = username + _isAutomation = isAutomation } export const errorCode = { @@ -667,7 +668,7 @@ function vscodeModeToString(mode: vscode.FileStat['permissions']) { } // XXX: future-proof in case vscode.FileStat.permissions gains more granularity. - if (isAutomation()) { + if (_isAutomation) { throw new Error('vscode.FileStat.permissions gained new fields, update this logic') } } diff --git a/packages/core/src/shared/extensionGlobals.ts b/packages/core/src/shared/extensionGlobals.ts index 29cc7eee366..a1d807a5ebb 100644 --- a/packages/core/src/shared/extensionGlobals.ts +++ b/packages/core/src/shared/extensionGlobals.ts @@ -146,7 +146,7 @@ export function initialize(context: ExtensionContext, isWeb: boolean = false): T context, clock: copyClock(), didReload: checkDidReload(context), - globalState: new GlobalState(context), + globalState: new GlobalState(context.globalState), manifestPaths: {} as ToolkitGlobals['manifestPaths'], visualizationResourcePaths: {} as ToolkitGlobals['visualizationResourcePaths'], isWeb, diff --git a/packages/core/src/shared/globalState.ts b/packages/core/src/shared/globalState.ts index 8ff9ad54ea8..962ff240e28 100644 --- a/packages/core/src/shared/globalState.ts +++ b/packages/core/src/shared/globalState.ts @@ -5,11 +5,14 @@ import * as vscode from 'vscode' import { getLogger } from './logger/logger' +import * as redshift from '../awsService/redshift/models/models' +import { TypeConstructor, cast } from './utilities/typeConstructors' type globalKey = | 'aws.downloadPath' | 'aws.lastTouchedS3Folder' | 'aws.lastUploadedToS3Folder' + | 'aws.redshift.connections' | 'aws.toolkit.amazonq.dismissed' | 'aws.toolkit.amazonqInstall.dismissed' // Deprecated/legacy names. New keys should start with "aws.". @@ -19,9 +22,11 @@ type globalKey = | 'hasAlreadyOpenedAmazonQ' /** - * Extension-local, shared state which persists after IDE restart. Shared with all instances (or - * tabs, in a web browser) of this extension for a given user, but not visible to other vscode - * extensions. Global state should be avoided, except when absolutely necessary. + * Extension-local (not visible to other vscode extensions) shared state which persists after IDE + * restart. Shared with all instances (or tabs, in a web browser) of this extension for a given + * user, including "remote" instances! + * + * Note: Global state should be avoided, except when absolutely necessary. * * This wrapper adds structure and visibility to the vscode `globalState` interface. It also opens * the door for: @@ -29,19 +34,69 @@ type globalKey = * - garbage collection */ export class GlobalState implements vscode.Memento { - public constructor(private readonly extContext: vscode.ExtensionContext) {} + constructor(private readonly memento: vscode.Memento) {} - public keys(): readonly string[] { - return this.extContext.globalState.keys() + keys(): readonly string[] { + return this.memento.keys() } - public get(key: globalKey, defaultValue?: T): T | undefined { - return this.extContext.globalState.get(key) ?? defaultValue + /** + * Gets the value for `key` if it satisfies the `type` specification, or fails. + * + * @param key Key name + * @param type Type validator function, or primitive type constructor such as {@link Object}, + * {@link String}, {@link Boolean}, etc. + * @param defaultVal Value returned if `key` has no value. + */ + getStrict(key: globalKey, type: TypeConstructor, defaulVal?: T) { + try { + const val = this.memento.get(key) ?? defaulVal + return !type || val === undefined ? val : cast(val, type) + } catch (e) { + const msg = `GlobalState: invalid state (or read failed) for key: "${key}"` + // XXX: ToolkitError causes circular dependency + // throw ToolkitError.chain(e, `Failed to read globalState: "${key}"`) + const err = new Error(msg) as Error & { + code: string + cause: unknown + } + err.cause = e + err.code = 'GlobalState' + throw err + } + } + + /** + * Gets the value at `key`, without type-checking. See {@link tryGet} and {@link getStrict} for type-checking variants. + * + * @param key Key name + * @param defaultVal Value returned if `key` has no value. + */ + get(key: globalKey, defaulVal?: T): T | undefined { + const skip = (o: any) => o as T // Don't type check. + return this.getStrict(key, skip, defaulVal) + } + + /** + * Gets the value for `key` if it satisfies the `type` specification, else logs an error and returns `defaulVal`. + * + * @param key Key name + * @param type Type validator function, or primitive type constructor such as {@link Object}, + * {@link String}, {@link Boolean}, etc. + * @param defaultVal Value returned if `key` has no value. + */ + tryGet(key: globalKey, type: TypeConstructor, defaulVal?: T): T | undefined { + try { + return this.getStrict(key, type, defaulVal) + } catch (e) { + getLogger().error('%s', (e as Error).message) + return defaulVal + } } /** Asynchronously updates globalState, or logs an error on failure. */ - public tryUpdate(key: globalKey, value: any): void { - this.extContext.globalState.update(key, value).then( + tryUpdate(key: globalKey, value: any): void { + this.memento.update(key, value).then( undefined, // TODO: log.debug() ? e => { getLogger().error('GlobalState: failed to set "%s": %s', key, (e as Error).message) @@ -49,11 +104,52 @@ export class GlobalState implements vscode.Memento { ) } - public update(key: globalKey, value: any): Thenable { - return this.extContext.globalState.update(key, value) + update(key: globalKey, value: any): Thenable { + return this.memento.update(key, value) + } + + /** + * Stores Redshift connection info for the specified warehouse ARN. + * + * TODO: this never garbage-collects old connections, so the state will grow forever... + * + * @param warehouseArn redshift warehouse ARN + * @param cxnInfo Connection info. Value is 'DELETE_CONNECTION' when the connection is deleted + * but the explorer node is not refreshed yet. + */ + async saveRedshiftConnection( + warehouseArn: string, + cxnInfo: redshift.ConnectionParams | undefined | 'DELETE_CONNECTION' + ) { + const allCxns = this.tryGet('aws.redshift.connections', Object, {}) + await this.update('aws.redshift.connections', { + ...allCxns, + [warehouseArn]: cxnInfo, + }) } - public samAndCfnSchemaDestinationUri() { - return vscode.Uri.joinPath(this.extContext.globalStorageUri, 'sam.schema.json') + /** + * Get the Redshift connection info for the specified warehouse ARN. + * + * @param warehouseArn redshift warehouse ARN + * @returns Connection info. Value is 'DELETE_CONNECTION' when the connection is deleted but the + * explorer node is not refreshed yet. + */ + getRedshiftConnection(warehouseArn: string): redshift.ConnectionParams | undefined | 'DELETE_CONNECTION' { + const allCxns = this.tryGet( + 'aws.redshift.connections', + v => { + if (v !== undefined && typeof v !== 'object') { + throw new Error() + } + const cxn = (v as any)?.[warehouseArn] + if (cxn !== undefined && typeof cxn !== 'object' && cxn !== 'DELETE_CONNECTION') { + throw new Error() + } + return v + }, + undefined + ) + return (allCxns as any)?.[warehouseArn] } } diff --git a/packages/core/src/shared/settings.ts b/packages/core/src/shared/settings.ts index 81c14348f56..a8f9cb6ee0c 100644 --- a/packages/core/src/shared/settings.ts +++ b/packages/core/src/shared/settings.ts @@ -8,10 +8,16 @@ import * as codecatalyst from './clients/codecatalystClient' import * as codewhisperer from '../codewhisperer/client/codewhisperer' import packageJson from '../../package.json' import { getLogger } from './logger' -import { cast, FromDescriptor, Record, TypeConstructor, TypeDescriptor } from './utilities/typeConstructors' +import { + cast, + FromDescriptor, + isNameMangled, + Record, + TypeConstructor, + TypeDescriptor, +} from './utilities/typeConstructors' import { assertHasProps, ClassToInterfaceType, keys } from './utilities/tsUtils' import { toRecord } from './utilities/collectionUtils' -import { isNameMangled } from './vscode/env' import { once, onceChanged } from './utilities/functionUtils' import { ToolkitError } from './errors' import { telemetry } from './telemetry/telemetry' diff --git a/packages/core/src/shared/utilities/typeConstructors.ts b/packages/core/src/shared/utilities/typeConstructors.ts index e57b4410a05..763a7d32da2 100644 --- a/packages/core/src/shared/utilities/typeConstructors.ts +++ b/packages/core/src/shared/utilities/typeConstructors.ts @@ -3,9 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { isNameMangled } from '../vscode/env' import { isNonNullable } from './tsUtils' +export function isNameMangled(): boolean { + return isNameMangled.name !== 'isNameMangled' +} + /** * A 'type constructor' is any function that resolves to the given type. * @@ -73,7 +76,7 @@ export function addTypeName unknown>(name: string, } /** - * A utility function to validate or transform an unknown input into a known shape. + * Validates or transforms an unknown input into a known shape. * * This **will throw** in the following scenarios: * - `type` is a {@link primitives "primitive" constructor} and `input` is a different primitive type diff --git a/packages/core/src/shared/vscode/commands2.ts b/packages/core/src/shared/vscode/commands2.ts index 35d823e0e5e..e5adbe67c55 100644 --- a/packages/core/src/shared/vscode/commands2.ts +++ b/packages/core/src/shared/vscode/commands2.ts @@ -5,7 +5,6 @@ import * as vscode from 'vscode' import { toTitleCase } from '../utilities/textUtilities' -import { isNameMangled } from './env' import { getLogger, NullLogger } from '../logger/logger' import { FunctionKeys, Functions, getFunctions } from '../utilities/classUtils' import { TreeItemContent, TreeNode } from '../treeview/resourceTreeDataProvider' @@ -16,6 +15,7 @@ import crypto from 'crypto' import { keysAsInt } from '../utilities/tsUtils' import { partialClone } from '../utilities/collectionUtils' import { isAmazonQ } from '../extensionUtilities' +import { isNameMangled } from '../utilities/typeConstructors' type Callback = (...args: any[]) => any type CommandFactory = (...parameters: U) => T diff --git a/packages/core/src/shared/vscode/env.ts b/packages/core/src/shared/vscode/env.ts index 95016cb2de3..d8b8de70d41 100644 --- a/packages/core/src/shared/vscode/env.ts +++ b/packages/core/src/shared/vscode/env.ts @@ -50,13 +50,6 @@ export function isAutomation(): boolean { return isCI() || !!process.env['AWS_TOOLKIT_AUTOMATION'] } -/** - * Returns true if name mangling has occured to the extension source code. - */ -export function isNameMangled(): boolean { - return isNameMangled.name !== 'isNameMangled' -} - export { extensionVersion } /** diff --git a/packages/core/src/test/globalSetup.test.ts b/packages/core/src/test/globalSetup.test.ts index 861ee3de270..4799f0265a7 100644 --- a/packages/core/src/test/globalSetup.test.ts +++ b/packages/core/src/test/globalSetup.test.ts @@ -84,8 +84,9 @@ export const mochaHooks = { globals.telemetry.clearRecords() globals.telemetry.logger.clear() TelemetryDebounceInfo.instance.clear() - ;(globals.context as FakeExtensionContext).globalState = new FakeMemento() - ;(globals as any).globalState = new GlobalState(globals.context) + const fakeGlobalState = new FakeMemento() + ;(globals.context as FakeExtensionContext).globalState = fakeGlobalState + ;(globals as any).globalState = new GlobalState(fakeGlobalState) await testUtil.closeAllEditors() }, diff --git a/packages/core/src/test/shared/globalState.test.ts b/packages/core/src/test/shared/globalState.test.ts new file mode 100644 index 00000000000..b1fb409020f --- /dev/null +++ b/packages/core/src/test/shared/globalState.test.ts @@ -0,0 +1,159 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import { GlobalState } from '../../shared/globalState' +import { FakeMemento } from '../fakeExtensionContext' +import * as redshift from '../../awsService/redshift/models/models' + +describe('GlobalState', function () { + let globalState: GlobalState + const testKey = 'aws.downloadPath' + + beforeEach(async function () { + const memento = new FakeMemento() + globalState = new GlobalState(memento) + }) + + afterEach(async function () {}) + + const scenarios = [ + { testValue: 1234, desc: 'number' }, + { testValue: 0, desc: 'default number' }, + { testValue: 'hello world', desc: 'string' }, + { testValue: '', desc: 'default string' }, + { testValue: true, desc: 'true' }, + { testValue: false, desc: 'false' }, + { testValue: [], desc: 'empty array' }, + { testValue: [{ value: 'foo' }, { value: 'bar' }], desc: 'array' }, + { testValue: {}, desc: 'empty object' }, + { testValue: { value: 'foo' }, desc: 'object' }, + ] + + describe('get()', function () { + scenarios.forEach(scenario => { + it(scenario.desc, async () => { + await globalState.update(testKey, scenario.testValue) + + const actualValue = globalState.get(testKey) + assert.deepStrictEqual(actualValue, scenario.testValue) + }) + }) + }) + + describe('update()', function () { + scenarios.forEach(scenario => { + it(scenario.desc, async () => { + await globalState.update(testKey, scenario.testValue) + const savedValue = globalState.get(testKey) + assert.deepStrictEqual(savedValue, scenario.testValue) + }) + }) + }) + + it('getStrict()', async () => { + // + // Missing item: + // + const testKey = 'aws.downloadPath' + assert.strictEqual(globalState.get(testKey), undefined) + assert.strictEqual(globalState.getStrict(testKey, Boolean), undefined) + assert.strictEqual(globalState.getStrict(testKey, Boolean, true), true) + + // + // Item exists but has wrong type: + // + await globalState.update(testKey, 123) + assert.throws(() => globalState.getStrict(testKey, String)) + assert.throws(() => globalState.getStrict(testKey, Object)) + assert.throws(() => globalState.getStrict(testKey, Boolean)) + // Wrong type, but defaultValue was given: + assert.throws(() => globalState.getStrict(testKey, String, '')) + assert.throws(() => globalState.getStrict(testKey, Object, {})) + assert.throws(() => globalState.getStrict(testKey, Boolean, true)) + }) + + it('tryGet()', async () => { + // + // Missing item: + // + const testKey = 'aws.downloadPath' + assert.strictEqual(globalState.get(testKey), undefined) + assert.strictEqual(globalState.tryGet(testKey, Boolean), undefined) + assert.strictEqual(globalState.tryGet(testKey, Boolean, true), true) + + // + // Item exists but has wrong type: + // + await globalState.update(testKey, 123) + assert.strictEqual(globalState.tryGet(testKey, String), undefined) + assert.strictEqual(globalState.tryGet(testKey, Object), undefined) + assert.strictEqual(globalState.tryGet(testKey, Boolean), undefined) + // Wrong type, but defaultValue was given: + assert.deepStrictEqual(globalState.tryGet(testKey, String, ''), '') + assert.deepStrictEqual(globalState.tryGet(testKey, Object, {}), {}) + assert.deepStrictEqual(globalState.tryGet(testKey, Boolean, true), true) + }) + + describe('redshift state', function () { + const fakeCxn1: redshift.ConnectionParams = { + connectionType: redshift.ConnectionType.SecretsManager, + database: 'fake-db', + warehouseIdentifier: 'warhouse-id-1', + warehouseType: redshift.RedshiftWarehouseType.SERVERLESS, + region: { + id: 'us-east-2', + name: 'region name', + }, + password: 'password-1', + secret: 'secret 1', + } + const fakeCxn2: redshift.ConnectionParams = { + ...fakeCxn1, + password: 'pw 2', + secret: 'secret 2', + warehouseIdentifier: 'wh-id-2', + database: 'fake db 2', + } + + it('get/set connection state and special DELETE_CONNECTION value', async () => { + const testArn1 = 'arn:foo/bar/baz/1' + const testArn2 = 'arn:foo/bar/baz/2' + await globalState.saveRedshiftConnection(testArn1, 'DELETE_CONNECTION') + await globalState.saveRedshiftConnection(testArn2, undefined) + assert.deepStrictEqual(globalState.getRedshiftConnection(testArn1), 'DELETE_CONNECTION') + assert.deepStrictEqual(globalState.getRedshiftConnection(testArn2), undefined) + await globalState.saveRedshiftConnection(testArn1, fakeCxn1) + await globalState.saveRedshiftConnection(testArn2, fakeCxn2) + assert.deepStrictEqual(globalState.getRedshiftConnection(testArn1), fakeCxn1) + assert.deepStrictEqual(globalState.getRedshiftConnection(testArn2), fakeCxn2) + }) + + it('validates state', async () => { + const testArn1 = 'arn:foo/bar/baz/1' + const testArn2 = 'arn:foo/bar/baz/2' + await globalState.saveRedshiftConnection(testArn1, 'foo' as any) + await globalState.saveRedshiftConnection(testArn2, 99 as any) + + // Assert that bad state was set. + assert.deepStrictEqual(globalState.get('aws.redshift.connections'), { + [testArn1]: 'foo', + [testArn2]: 99, + }) + + // Bad state is logged and returns undefined. + assert.deepStrictEqual(globalState.getRedshiftConnection(testArn1), undefined) + assert.deepStrictEqual(globalState.getRedshiftConnection(testArn2), undefined) + await globalState.saveRedshiftConnection(testArn2, fakeCxn2) + assert.deepStrictEqual(globalState.getRedshiftConnection(testArn2), fakeCxn2) + + // Stored state is now "partially bad". + assert.deepStrictEqual(globalState.get('aws.redshift.connections'), { + [testArn1]: 'foo', + [testArn2]: fakeCxn2, + }) + }) + }) +}) diff --git a/packages/core/src/test/shared/settingsConfiguration.test.ts b/packages/core/src/test/shared/settings.test.ts similarity index 100% rename from packages/core/src/test/shared/settingsConfiguration.test.ts rename to packages/core/src/test/shared/settings.test.ts