-
Notifications
You must be signed in to change notification settings - Fork 10
CHI-3946-quick_dial_with_config #4622
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
stephenhand
merged 21 commits into
CHI-3946-quick_dial
from
CHI-3946-quick_dial_with_config
Aug 26, 2026
Merged
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
1f7c36e
Initial plan
Copilot 34834ef
Add private Twilio configuration endpoint to account-scoped lambda
Copilot ceb9315
Merge remote-tracking branch 'origin/copilot/add-private-configuratio…
stephenhand ff334e5
Redux & client support in plugin for 'private' configuration
stephenhand de981be
Fix lambda container build
stephenhand 70172cd
Add support for flex token in auth header for account-scoped lambda
stephenhand e2ccc89
Comment about deprecation
stephenhand 919562d
Move account scoped lambda service methods to separate file
stephenhand 522f248
Rename 'private twilio configuration' -> 'aselo twilio configuration'
stephenhand 5c89b88
Fix tests
stephenhand 9f204e0
Fix lint
stephenhand ff9c892
Finish wiring up configuration to quickdial dialog
stephenhand 95dc990
Merge branch 'master' into CHI-3946-quick_dial_with_config
stephenhand 280419c
test: fix config reducer expectations and add quick-dial unit coverage
Copilot 61eed07
Fix remaining fetchProtectedApi unit test expectation
Copilot 319aa1b
Merge branch 'CHI-3946-quick_dial' into CHI-3946-quick_dial_with_config
stephenhand c94c999
Merge branch 'master' into CHI-3946-quick_dial_with_config
stephenhand 9fe2cd0
Merge branch 'CHI-3946-quick_dial' into CHI-3946-quick_dial_with_config
stephenhand fb236e1
Fix merge issue
stephenhand ee5df12
Fix merge issue in tests
stephenhand 3788bd2
Merge branch 'CHI-3946-quick_dial' into CHI-3946-quick_dial_with_config
stephenhand File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
39 changes: 39 additions & 0 deletions
39
lambdas/account-scoped/src/configuration/getAseloTwilioConfiguration.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| /** | ||
| * Copyright (C) 2021-2023 Technology Matters | ||
| * This program is free software: you can redistribute it and/or modify | ||
| * it under the terms of the GNU Affero General Public License as published | ||
| * by the Free Software Foundation, either version 3 of the License, or | ||
| * (at your option) any later version. | ||
| * | ||
| * This program is distributed in the hope that it will be useful, | ||
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| * GNU Affero General Public License for more details. | ||
| * | ||
| * You should have received a copy of the GNU Affero General Public License | ||
| * along with this program. If not, see https://www.gnu.org/licenses/. | ||
| */ | ||
|
|
||
| import type { AccountSID } from '@tech-matters/twilio-types'; | ||
| import { getDocsBucketName } from '@tech-matters/twilio-configuration'; | ||
| import { getS3Object } from '@tech-matters/s3'; | ||
| import { newErr, newOk } from '@tech-matters/result-type'; | ||
| import { AccountScopedHandler } from '../httpTypes'; | ||
|
|
||
| const ASELO_TWILIO_CONFIGURATION_KEY = 'configuration/twilio.json'; | ||
|
|
||
| export const getAseloTwilioConfigurationHandler: AccountScopedHandler = async ( | ||
| _event, | ||
| accountSid: AccountSID, | ||
| ) => { | ||
| try { | ||
| const bucket = await getDocsBucketName(accountSid); | ||
| const content = await getS3Object(bucket, ASELO_TWILIO_CONFIGURATION_KEY); | ||
| return newOk(JSON.parse(content)); | ||
| } catch (err: any) { | ||
| if (err?.name === 'NoSuchKey') { | ||
| return newOk({}); | ||
| } | ||
| return newErr({ message: err.message, error: { statusCode: 500, cause: err } }); | ||
| } | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
119 changes: 119 additions & 0 deletions
119
lambdas/account-scoped/tests/unit/configuration/getAseloTwilioConfiguration.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| /** | ||
| * Copyright (C) 2021-2023 Technology Matters | ||
| * This program is free software: you can redistribute it and/or modify | ||
| * it under the terms of the GNU Affero General Public License as published | ||
| * by the Free Software Foundation, either version 3 of the License, or | ||
| * (at your option) any later version. | ||
| * | ||
| * This program is distributed in the hope that it will be useful, | ||
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| * GNU Affero General Public License for more details. | ||
| * | ||
| * You should have received a copy of the GNU Affero General Public License | ||
| * along with this program. If not, see https://www.gnu.org/licenses/. | ||
| */ | ||
|
|
||
| import { getAseloTwilioConfigurationHandler } from '../../../src/configuration/getAseloTwilioConfiguration'; | ||
| import { getDocsBucketName } from '@tech-matters/twilio-configuration'; | ||
| import { getS3Object } from '@tech-matters/s3'; | ||
| import { isErr, isOk } from '@tech-matters/result-type'; | ||
| import { HttpRequest } from '../../../src/httpTypes'; | ||
| import { TEST_ACCOUNT_SID } from '../../testTwilioValues'; | ||
|
|
||
| jest.mock('@tech-matters/twilio-configuration', () => ({ | ||
| getDocsBucketName: jest.fn(), | ||
| })); | ||
|
|
||
| jest.mock('@tech-matters/s3', () => ({ | ||
| getS3Object: jest.fn(), | ||
| })); | ||
|
|
||
| const mockGetDocsBucketName = getDocsBucketName as jest.MockedFunction< | ||
| typeof getDocsBucketName | ||
| >; | ||
| const mockGetS3Object = getS3Object as jest.MockedFunction<typeof getS3Object>; | ||
|
|
||
| const TEST_BUCKET = 'test-docs-bucket'; | ||
|
|
||
| const createMockRequest = (): HttpRequest => ({ | ||
| method: 'GET', | ||
| headers: {}, | ||
| path: '/test', | ||
| query: {}, | ||
| body: {}, | ||
| }); | ||
|
|
||
| describe('getAseloTwilioConfigurationHandler', () => { | ||
| beforeEach(() => { | ||
| jest.clearAllMocks(); | ||
| mockGetDocsBucketName.mockResolvedValue(TEST_BUCKET); | ||
| }); | ||
|
|
||
| it('should return parsed JSON content when configuration file exists', async () => { | ||
| const config = { someKey: 'someValue', nested: { flag: true } }; | ||
| mockGetS3Object.mockResolvedValue(JSON.stringify(config)); | ||
|
|
||
| const result = await getAseloTwilioConfigurationHandler( | ||
| createMockRequest(), | ||
| TEST_ACCOUNT_SID, | ||
| ); | ||
|
|
||
| expect(isOk(result)).toBe(true); | ||
| if (isOk(result)) { | ||
| expect(result.data).toEqual(config); | ||
| } | ||
| expect(mockGetDocsBucketName).toHaveBeenCalledWith(TEST_ACCOUNT_SID); | ||
| expect(mockGetS3Object).toHaveBeenCalledWith( | ||
| TEST_BUCKET, | ||
| 'configuration/twilio.json', | ||
| ); | ||
| }); | ||
|
|
||
| it('should return an empty object when configuration file does not exist (NoSuchKey)', async () => { | ||
| const noSuchKeyError = Object.assign(new Error('The specified key does not exist.'), { | ||
| name: 'NoSuchKey', | ||
| }); | ||
| mockGetS3Object.mockRejectedValue(noSuchKeyError); | ||
|
|
||
| const result = await getAseloTwilioConfigurationHandler( | ||
| createMockRequest(), | ||
| TEST_ACCOUNT_SID, | ||
| ); | ||
|
|
||
| expect(isOk(result)).toBe(true); | ||
| if (isOk(result)) { | ||
| expect(result.data).toEqual({}); | ||
| } | ||
| }); | ||
|
|
||
| it('should return 500 on unexpected S3 error', async () => { | ||
| mockGetS3Object.mockRejectedValue(new Error('S3 service unavailable')); | ||
|
|
||
| const result = await getAseloTwilioConfigurationHandler( | ||
| createMockRequest(), | ||
| TEST_ACCOUNT_SID, | ||
| ); | ||
|
|
||
| expect(isErr(result)).toBe(true); | ||
| if (isErr(result)) { | ||
| expect(result.message).toBe('S3 service unavailable'); | ||
| expect(result.error.statusCode).toBe(500); | ||
| } | ||
| }); | ||
|
|
||
| it('should return 500 when getDocsBucketName fails', async () => { | ||
| mockGetDocsBucketName.mockRejectedValue(new Error('SSM parameter not found')); | ||
|
|
||
| const result = await getAseloTwilioConfigurationHandler( | ||
| createMockRequest(), | ||
| TEST_ACCOUNT_SID, | ||
| ); | ||
|
|
||
| expect(isErr(result)).toBe(true); | ||
| if (isErr(result)) { | ||
| expect(result.message).toBe('SSM parameter not found'); | ||
| expect(result.error.statusCode).toBe(500); | ||
| } | ||
| }); | ||
| }); |
118 changes: 118 additions & 0 deletions
118
lambdas/account-scoped/tests/unit/validation/flexToken.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| /** | ||
| * Copyright (C) 2021-2026 Technology Matters | ||
| * This program is free software: you can redistribute it and/or modify | ||
| * it under the terms of the GNU Affero General Public License as published | ||
| * by the Free Software Foundation, either version 3 of the License, or | ||
| * (at your option) any later version. | ||
| * | ||
| * This program is distributed in the hope that it will be useful, | ||
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| * GNU Affero General Public License for more details. | ||
| * | ||
| * You should have received a copy of the GNU Affero General Public License | ||
| * along with this program. If not, see https://www.gnu.org/licenses/. | ||
| */ | ||
|
|
||
| import { validator } from 'twilio-flex-token-validator'; | ||
| import { getAccountAuthToken } from '@tech-matters/twilio-configuration'; | ||
| import { isErr, isOk } from '@tech-matters/result-type'; | ||
| import { AccountScopedRoute, HttpRequest } from '../../../src/httpTypes'; | ||
| import { validateFlexTokenRequest } from '../../../src/validation/flexToken'; | ||
| import { TEST_ACCOUNT_SID } from '../../testTwilioValues'; | ||
|
|
||
| jest.mock('twilio-flex-token-validator', () => ({ | ||
| validator: jest.fn(), | ||
| })); | ||
|
|
||
| jest.mock('@tech-matters/twilio-configuration', () => ({ | ||
| getAccountAuthToken: jest.fn(), | ||
| })); | ||
|
|
||
| const mockValidator = validator as jest.MockedFunction<typeof validator>; | ||
| const mockGetAccountAuthToken = getAccountAuthToken as jest.MockedFunction< | ||
| typeof getAccountAuthToken | ||
| >; | ||
|
|
||
| const baseRequest: HttpRequest = { | ||
| method: 'GET', | ||
| headers: {}, | ||
| path: '/configuration/twilio', | ||
| query: {}, | ||
| body: {}, | ||
| }; | ||
|
|
||
| const routeContext = { | ||
| accountSid: TEST_ACCOUNT_SID, | ||
| } as AccountScopedRoute; | ||
|
|
||
| describe('validateFlexTokenRequest', () => { | ||
| beforeEach(() => { | ||
| jest.clearAllMocks(); | ||
| mockGetAccountAuthToken.mockResolvedValue('account-auth-token'); | ||
| mockValidator.mockResolvedValue({ | ||
| worker_sid: 'WK123', | ||
| roles: ['agent'], | ||
| } as any); | ||
| }); | ||
|
|
||
| test('accepts bearer token from authorization header', async () => { | ||
| const request = { | ||
| ...baseRequest, | ||
| headers: { | ||
| authorization: ['Bearer', 'from-header-token'].join(' '), | ||
| }, | ||
| body: {}, | ||
| }; | ||
|
|
||
| const result = await validateFlexTokenRequest({ tokenMode: 'agent' })( | ||
| request, | ||
| routeContext, | ||
| ); | ||
|
|
||
| expect(isOk(result)).toBe(true); | ||
| if (isOk(result) && 'tokenResult' in result.data) { | ||
| expect(result.data.tokenResult.worker_sid).toBe('WK123'); | ||
| } | ||
| expect(mockValidator).toHaveBeenCalledWith( | ||
| 'from-header-token', | ||
| TEST_ACCOUNT_SID, | ||
| 'account-auth-token', | ||
| ); | ||
| }); | ||
|
|
||
| test('falls back to Token from body when no authorization header is present', async () => { | ||
| const request = { | ||
| ...baseRequest, | ||
| body: { | ||
| Token: 'from-body-token', | ||
| }, | ||
| }; | ||
|
|
||
| const result = await validateFlexTokenRequest({ tokenMode: 'agent' })( | ||
| request, | ||
| routeContext, | ||
| ); | ||
|
|
||
| expect(isOk(result)).toBe(true); | ||
| expect(mockValidator).toHaveBeenCalledWith( | ||
| 'from-body-token', | ||
| TEST_ACCOUNT_SID, | ||
| 'account-auth-token', | ||
| ); | ||
| }); | ||
|
|
||
| test('returns missing-parameter error when no token is provided', async () => { | ||
| const result = await validateFlexTokenRequest({ tokenMode: 'agent' })( | ||
| baseRequest, | ||
| routeContext, | ||
| ); | ||
|
|
||
| expect(isErr(result)).toBe(true); | ||
| if (isErr(result)) { | ||
| expect(result.error.statusCode).toBe(400); | ||
| expect(result.message).toContain('Token body parameter missing'); | ||
| } | ||
| expect(mockValidator).not.toHaveBeenCalled(); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.