Skip to content

Commit

Permalink
test(notifications): add integ tests with stubbed auth
Browse files Browse the repository at this point in the history
Create a fake auth instance so we can simulate activating the notifications module with a "working" auth, to see how it behaves and displays notifications.
  • Loading branch information
hayemaxi committed Nov 19, 2024
1 parent 87ea4d9 commit ce8b88d
Show file tree
Hide file tree
Showing 14 changed files with 298 additions and 18 deletions.
45 changes: 45 additions & 0 deletions packages/amazonq/.vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,51 @@
"order": 3
}
},
{
"name": "Integration Tests",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--disable-extension=amazonwebservices.aws-toolkit-vscode",
"${workspaceFolder}/../core/dist/src/testFixtures/workspaceFolder",
"--extensionDevelopmentPath=${workspaceFolder}",
"--extensionTestsPath=${workspaceFolder}/dist/test/integ/index.js"
],
"env": {
"DEVELOPMENT_PATH": "${workspaceFolder}",
"AWS_TOOLKIT_AUTOMATION": "local"
},
"outFiles": ["${workspaceFolder}/dist/**/*.js", "${workspaceFolder}/../core/dist/**/*.js"],
"preLaunchTask": "watch",
"presentation": {
"group": "6_IntegrationTests",
"order": 1
}
},
{
"name": "Integration Tests (current file)",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--disable-extension=amazonwebservices.aws-toolkit-vscode",
"${workspaceFolder}/../core/dist/src/testFixtures/workspaceFolder",
"--extensionDevelopmentPath=${workspaceFolder}",
"--extensionTestsPath=${workspaceFolder}/dist/test/integ/index.js"
],
"env": {
"TEST_FILE": "${relativeFile}",
"DEVELOPMENT_PATH": "${workspaceFolder}",
"AWS_TOOLKIT_AUTOMATION": "local"
},
"outFiles": ["${workspaceFolder}/dist/**/*.js", "${workspaceFolder}/../core/dist/**/*.js"],
"preLaunchTask": "watch",
"presentation": {
"group": "5_IntegrationTestsCurrentFile",
"order": 1
}
},
{
"name": "E2E Test (current file)",
"type": "extensionHost",
Expand Down
1 change: 1 addition & 0 deletions packages/amazonq/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"watch": "npm run clean && npm run buildScripts && tsc -watch -p ./",
"testCompile": "npm run clean && npm run buildScripts && npm run compileOnly",
"test": "npm run testCompile && c8 --allowExternal ts-node ../core/scripts/test/launchTest.ts unit dist/test/unit/index.js ../core/dist/src/testFixtures/workspaceFolder",
"testInteg": "npm run testCompile && c8 --allowExternal ts-node ../core/scripts/test/launchTest.ts integration dist/test/integ/index.js ../core/dist/src/testFixtures/workspaceFolder",
"testE2E": "npm run testCompile && c8 --allowExternal ts-node ../core/scripts/test/launchTest.ts e2e dist/test/e2e/index.js ../core/dist/src/testFixtures/workspaceFolder",
"testWeb": "npm run compileDev && c8 --allowExternal ts-node ../core/scripts/test/launchTest.ts web dist/test/web/testRunnerWebCore.js",
"webRun": "npx @vscode/test-web --open-devtools --browserOption=--disable-web-security --waitForDebugger=9222 --extensionDevelopmentPath=. .",
Expand Down
5 changes: 3 additions & 2 deletions packages/amazonq/src/extensionNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { Auth, AuthUtils, getTelemetryMetadataForConn, isAnySsoConnection } from
import api from './api'
import { activate as activateCWChat } from './app/chat/activation'
import { beta } from 'aws-core-vscode/dev'
import { activate as activateNotifications } from 'aws-core-vscode/notifications'
import { activate as activateNotifications, deactivate as deactivateNotifications } from 'aws-core-vscode/notifications'
import { AuthState, AuthUtil } from 'aws-core-vscode/codewhisperer'
import { telemetry, AuthUserState } from 'aws-core-vscode/telemetry'

Expand Down Expand Up @@ -78,7 +78,7 @@ async function activateAmazonQNode(context: vscode.ExtensionContext) {
await activateNotifications(context, authState, getAuthState)
}

async function getAuthState(): Promise<Omit<AuthUserState, 'source'>> {
export async function getAuthState(): Promise<Omit<AuthUserState, 'source'>> {
let authState: AuthState = 'disconnected'
try {
// May call connection validate functions that try to refresh the token.
Expand Down Expand Up @@ -147,4 +147,5 @@ async function setupDevMode(context: vscode.ExtensionContext) {
export async function deactivate() {
// Run concurrently to speed up execution. stop() does not throw so it is safe
await Promise.all([(await CrashMonitoring.instance())?.shutdown(), deactivateCommon()])
deactivateNotifications()
}
13 changes: 13 additions & 0 deletions packages/amazonq/test/integ/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*!
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

import { runTests } from 'aws-core-vscode/test'
import { VSCODE_EXTENSION_ID } from 'aws-core-vscode/utils'

export function run(): Promise<void> {
return runTests(process.env.TEST_DIR ?? 'test/integ', VSCODE_EXTENSION_ID.amazonq, [
'../../core/dist/src/testInteg/globalSetup.test.ts',
])
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*!
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0 fort
*/

import { getNotificationsSuite } from 'aws-core-vscode/test'
import { getAuthState } from '../../../src/extensionNode'

getNotificationsSuite(getAuthState)
5 changes: 3 additions & 2 deletions packages/core/src/extensionNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import * as beta from './dev/beta'
import { activate as activateApplicationComposer } from './applicationcomposer/activation'
import { activate as activateRedshift } from './awsService/redshift/activation'
import { activate as activateIamPolicyChecks } from './awsService/accessanalyzer/activation'
import { activate as activateNotifications } from './notifications/activation'
import { activate as activateNotifications, deactivate as deactivateNotifications } from './notifications/activation'
import { SchemaService } from './shared/schemas'
import { AwsResourceManager } from './dynamicResources/awsResourceManager'
import globals from './shared/extensionGlobals'
Expand Down Expand Up @@ -270,6 +270,7 @@ export async function deactivate() {
// Run concurrently to speed up execution. stop() does not throw so it is safe
await Promise.all([await (await CrashMonitoring.instance())?.shutdown(), deactivateCommon(), deactivateEc2()])
await globals.resourceManager.dispose()
deactivateNotifications()
}

async function handleAmazonQInstall() {
Expand Down Expand Up @@ -338,7 +339,7 @@ function recordToolkitInitialization(activationStartedOn: number, settingsValid:
}
}

async function getAuthState(): Promise<Omit<AuthUserState, 'source'>> {
export async function getAuthState(): Promise<Omit<AuthUserState, 'source'>> {
let authStatus: AuthStatus = 'notConnected'
const enabledConnections: Set<AuthFormId> = new Set()
const enabledScopes: Set<string> = new Set()
Expand Down
28 changes: 24 additions & 4 deletions packages/core/src/notifications/activation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,23 @@

import * as vscode from 'vscode'
import { DevSettings } from '../shared/settings'
import { NotificationsController } from './controller'
import { NotificationsController, ControllerOptions, RemoteFetcher } from './controller'
import { NotificationsNode } from './panelNode'
import { RuleEngine, getRuleContext } from './rules'
import globals from '../shared/extensionGlobals'
import { AuthState } from './types'
import { getLogger } from '../shared/logger/logger'
import { oneMinute } from '../shared/datetime'
import { globalKey } from '../shared/globalState'

/** Time in MS to poll for emergency notifications */
const emergencyPollTime = oneMinute * 10

/** Key in global state to store notification data */
const storageKey: globalKey = 'aws.notifications'

let interval: NodeJS.Timer

/**
* Activate the in-IDE notifications module and begin receiving notifications.
*
Expand All @@ -26,7 +32,8 @@ const emergencyPollTime = oneMinute * 10
export async function activate(
context: vscode.ExtensionContext,
initialState: AuthState,
authStateFn: () => Promise<AuthState>
authStateFn: () => Promise<AuthState>,
options?: Partial<Omit<ControllerOptions, 'node'>>
) {
// TODO: Currently gated behind feature-flag.
if (!DevSettings.instance.get('notifications', false)) {
Expand All @@ -36,16 +43,29 @@ export async function activate(
const panelNode = NotificationsNode.instance
panelNode.registerView(context)

const controller = new NotificationsController(panelNode)
const controller = new NotificationsController({
node: panelNode,
fetcher: options?.fetcher ?? new RemoteFetcher(),
storageKey: options?.storageKey ?? storageKey,
})
const engine = new RuleEngine(await getRuleContext(context, initialState))

await controller.pollForStartUp(engine)
await controller.pollForEmergencies(engine)

globals.clock.setInterval(async () => {
if (interval !== undefined) {
globals.clock.clearInterval(interval)
}

interval = globals.clock.setInterval(async () => {
const ruleContext = await getRuleContext(context, await authStateFn())
await controller.pollForEmergencies(new RuleEngine(ruleContext))
}, emergencyPollTime)

getLogger('notifications').debug('Activated in-IDE notifications polling module')
}

export function deactivate() {
globals.clock.clearInterval(interval)
getLogger('notifications').debug('Deactivated in-IDE notifications polling module')
}
19 changes: 13 additions & 6 deletions packages/core/src/notifications/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ import { FileResourceFetcher } from '../shared/resourcefetcher/fileResourceFetch
import { isAmazonQ } from '../shared/extensionUtilities'
import { telemetry } from '../shared/telemetry/telemetry'

export type ControllerOptions = {
node: NotificationsNode
storageKey: globalKey
fetcher: NotificationFetcher
}

/**
* Handles fetching and maintaining the state of in-IDE notifications.
* Notifications are constantly polled from a known endpoint and then stored in global state.
Expand All @@ -39,24 +45,25 @@ import { telemetry } from '../shared/telemetry/telemetry'
* Emergency notifications - fetched at a regular interval.
*/
export class NotificationsController {
public static readonly suggestedPollIntervalMs = 1000 * 60 * 10 // 10 minutes
public readonly storageKey: globalKey

/** Internal memory state that is written to global state upon modification. */
private readonly state: NotificationsState
private readonly notificationsNode: NotificationsNode
private readonly fetcher: NotificationFetcher

static #instance: NotificationsController | undefined

constructor(
private readonly notificationsNode: NotificationsNode,
private readonly fetcher: NotificationFetcher = new RemoteFetcher(),
public readonly storageKey: globalKey = 'aws.notifications'
) {
constructor(options: ControllerOptions) {
if (!NotificationsController.#instance) {
// Register on first creation only.
registerDismissCommand()
}
NotificationsController.#instance = this

this.notificationsNode = options.node
this.storageKey = options.storageKey
this.fetcher = options.fetcher
this.state = this.getDefaultState()
}

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/notifications/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
* SPDX-License-Identifier: Apache-2.0
*/

export { activate } from './activation'
export { activate, deactivate } from './activation'
1 change: 1 addition & 0 deletions packages/core/src/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export { getTestLogger } from './globalSetup.test'
export { testCommand } from './shared/vscode/testUtils'
export { FakeAwsContext } from './utilities/fakeAwsContext'
export { getTestWorkspaceFolder } from '../testInteg/integrationTestsUtilities'
export { getNotificationsSuite } from '../testInteg/notifications/suite'
export * from './codewhisperer/testUtil'
export * from './credentials/testUtil'
export * from './testUtil'
Expand Down
9 changes: 7 additions & 2 deletions packages/core/src/test/notifications/controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,13 @@ import {
import { HttpResourceFetcher } from '../../shared/resourcefetcher/httpResourceFetcher'
import { NotificationsNode } from '../../notifications/panelNode'
import { RuleEngine } from '../../notifications/rules'
import { globalKey } from '../../shared/globalState'

// one test node to use across different tests
export const panelNode: NotificationsNode = NotificationsNode.instance

const storageKey = 'aws.notifications.test' as globalKey

describe('Notifications Controller', function () {
const ruleEngine: RuleEngine = new RuleEngine({
ideVersion: '1.83.0',
Expand Down Expand Up @@ -90,7 +93,7 @@ describe('Notifications Controller', function () {
beforeEach(async function () {
await panelNode.setNotifications([], [])
fetcher = new TestFetcher()
controller = new NotificationsController(panelNode, fetcher, '_aws.test.notification' as any)
controller = new NotificationsController({ node: panelNode, fetcher, storageKey })

ruleEngineSpy = sinon.spy(ruleEngine, 'shouldDisplayNotification')
focusPanelSpy = sinon.spy(panelNode, 'focusPanel')
Expand Down Expand Up @@ -475,7 +478,9 @@ describe('Notifications Controller', function () {
throw new Error('test error')
}
})()
assert.doesNotThrow(() => new NotificationsController(panelNode, fetcher).pollForStartUp(ruleEngine))
assert.doesNotThrow(() =>
new NotificationsController({ node: panelNode, fetcher, storageKey }).pollForStartUp(ruleEngine)
)
assert.ok(wasCalled)
})

Expand Down
10 changes: 9 additions & 1 deletion packages/core/src/testInteg/notifications/notifications.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ import globals from '../../shared/extensionGlobals'
import assert from 'assert'
import { VSCODE_EXTENSION_ID } from '../../shared/extensions'
import sinon from 'sinon'
import { globalKey } from '../../shared/globalState'

/**
* Simple integ test to verify we can fetch and display from hosted files.
*/
describe('Notifications Integration Test', function () {
let fetcher: RemoteFetcher
let panelNode: NotificationsNode
Expand Down Expand Up @@ -43,7 +47,11 @@ describe('Notifications Integration Test', function () {
'https://idetoolkits-hostedfiles.amazonaws.com/Notifications/integ/VSCode/startup/1.x.json',
'https://idetoolkits-hostedfiles.amazonaws.com/Notifications/integ/VSCode/emergency/1.x.json'
)
controller = new NotificationsController(panelNode, fetcher)
controller = new NotificationsController({
node: panelNode,
fetcher,
storageKey: 'aws.notifications.test' as globalKey,
})
})

// Clear all global states after each test
Expand Down
Loading

0 comments on commit ce8b88d

Please sign in to comment.