Skip to content

Commit

Permalink
test(notifications): add e2e tests
Browse files Browse the repository at this point in the history
  • Loading branch information
hayemaxi committed Nov 19, 2024
1 parent 87ea4d9 commit b627d4a
Show file tree
Hide file tree
Showing 12 changed files with 250 additions and 21 deletions.
14 changes: 11 additions & 3 deletions packages/amazonq/src/extensionNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ 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 {
RemoteFetcher,
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 @@ -75,10 +79,13 @@ async function activateAmazonQNode(context: vscode.ExtensionContext) {
...authState,
})

await activateNotifications(context, authState, getAuthState)
await activateNotifications(context, authState, getAuthState, {
fetcher: new RemoteFetcher(),
storageKey: 'aws.notifications',
})
}

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 +154,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()
}
9 changes: 9 additions & 0 deletions packages/amazonq/test/e2e/notifications/notifications.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 { getNotificationsE2ESuite } from 'aws-core-vscode/test'
import { getAuthState } from '../../../src/extensionNode'

getNotificationsE2ESuite(getAuthState)
11 changes: 8 additions & 3 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 All @@ -60,6 +60,7 @@ import { activate as activateThreatComposerEditor } from './threatComposer/activ
import { isSsoConnection, hasScopes } from './auth/connection'
import { CrashMonitoring, setContext } from './shared'
import { AuthFormId } from './login/webview/vue/types'
import { RemoteFetcher } from './notifications/controller'

let localize: nls.LocalizeFunc

Expand Down Expand Up @@ -246,7 +247,10 @@ export async function activate(context: vscode.ExtensionContext) {
...authState,
})

await activateNotifications(context, authState, getAuthState)
await activateNotifications(context, authState, getAuthState, {
fetcher: new RemoteFetcher(),
storageKey: 'aws.notifications',
})
} catch (error) {
const stacktrace = (error as Error).stack?.split('\n')
// truncate if the stacktrace is unusually long
Expand All @@ -270,6 +274,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 +343,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
19 changes: 15 additions & 4 deletions packages/core/src/notifications/activation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import * as vscode from 'vscode'
import { DevSettings } from '../shared/settings'
import { NotificationsController } from './controller'
import { NotificationsController, ControllerOptions } from './controller'
import { NotificationsNode } from './panelNode'
import { RuleEngine, getRuleContext } from './rules'
import globals from '../shared/extensionGlobals'
Expand All @@ -15,6 +15,7 @@ import { oneMinute } from '../shared/datetime'

/** Time in MS to poll for emergency notifications */
const emergencyPollTime = oneMinute * 10
let interval: NodeJS.Timer

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

const controller = new NotificationsController(panelNode)
const controller = new NotificationsController({ node: panelNode, ...options })
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')
}
18 changes: 13 additions & 5 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 @@ -40,23 +46,25 @@ import { telemetry } from '../shared/telemetry/telemetry'
*/
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
3 changes: 2 additions & 1 deletion packages/core/src/notifications/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
* SPDX-License-Identifier: Apache-2.0
*/

export { activate } from './activation'
export { activate, deactivate } from './activation'
export { RemoteFetcher } from './controller'
12 changes: 10 additions & 2 deletions packages/core/src/notifications/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,13 @@ export class RuleEngine {
if (condition.additionalCriteria) {
for (const criteria of condition.additionalCriteria) {
if (!this.evaluateRule(criteria)) {
logger.verbose('notification id: (%s) did NOT pass criteria check: %O', id, criteria)
// We want to see nested objects. It is not deep.
// eslint-disable-next-line aws-toolkits/no-json-stringify-in-log
logger.verbose(
'notification id: (%s) did NOT pass criteria check: %s',
id,
JSON.stringify(criteria)
)
return false
}
logger.debug('notification id: (%s) passed criteria check: %O', id, criteria)
Expand Down Expand Up @@ -201,7 +207,9 @@ export async function getRuleContext(context: vscode.ExtensionContext, authState
}

const { activeExtensions, ...loggableRuleContext } = ruleContext
logger.debug('getRuleContext() determined rule context: %O', loggableRuleContext)
// We want to see the nested objects. It is not deep.
// eslint-disable-next-line aws-toolkits/no-json-stringify-in-log
logger.debug('getRuleContext() determined rule context: %s', JSON.stringify(loggableRuleContext))

return ruleContext
}
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 { getNotificationsE2ESuite } from '../testE2E/notifications/notifications.test'
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
Loading

0 comments on commit b627d4a

Please sign in to comment.