-
Notifications
You must be signed in to change notification settings - Fork 647
fix(slack): use environment id for slack connection id with legacy fallback #5559
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
base: master
Are you sure you want to change the base?
Changes from all commits
be63746
48f3c0c
7fcb84e
9b4fa78
6bb40be
1f39216
6fff7c9
fc6f123
14fb655
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest'; | ||
|
|
||
| import db from '@nangohq/database'; | ||
| import { seeders } from '@nangohq/shared'; | ||
|
|
||
| import { envs } from '../../../env.js'; | ||
| import { authenticateUser, isSuccess, runServer, shouldBeProtected, shouldRequireQueryEnv } from '../../../utils/tests.js'; | ||
|
|
||
| const route = '/api/v1/environments/current'; | ||
| let api: Awaited<ReturnType<typeof runServer>>; | ||
|
|
||
| describe(`GET ${route}`, () => { | ||
| beforeAll(async () => { | ||
| api = await runServer(); | ||
| }); | ||
|
|
||
| afterAll(() => { | ||
| api.server.close(); | ||
| }); | ||
|
|
||
| it('should be protected', async () => { | ||
| // @ts-expect-error query params are required | ||
| const res = await api.fetch(route, { method: 'GET', query: { env: 'dev' } }); | ||
|
|
||
| shouldBeProtected(res); | ||
| }); | ||
|
|
||
| it('should enforce env query params', async () => { | ||
| const { secret } = await seeders.seedAccountEnvAndUser(); | ||
|
|
||
| const res = await api.fetch(route, { | ||
| method: 'GET', | ||
| token: secret.secret | ||
| }); | ||
|
|
||
| shouldRequireQueryEnv(res); | ||
| }); | ||
|
|
||
| it('should return environment and account data', async () => { | ||
| const { env, account, user } = await seeders.seedAccountEnvAndUser(); | ||
| const session = await authenticateUser(api, user); | ||
|
|
||
| const res = await api.fetch(route, { | ||
| method: 'GET', | ||
| // @ts-expect-error query params are required | ||
| query: { env: env.name }, | ||
| session | ||
| }); | ||
|
|
||
| expect(res.res.status).toBe(200); | ||
| isSuccess(res.json); | ||
| expect(res.json).toMatchObject({ | ||
| environmentAndAccount: { | ||
| environment: expect.objectContaining({ | ||
| name: env.name | ||
| }), | ||
| env_variables: [], | ||
| uuid: account.uuid, | ||
| name: account.name, | ||
| email: user.email, | ||
| slack_notifications_channel: '' | ||
| } | ||
| }); | ||
| }); | ||
|
|
||
| describe('slack_notifications_channel', () => { | ||
| let adminProdEnv: Awaited<ReturnType<typeof seeders.createEnvironmentSeed>>; | ||
| let originalAdminUUID: string | undefined; | ||
|
|
||
| beforeAll(async () => { | ||
| const { account: adminAccount } = await seeders.seedAccountEnvAndUser(); | ||
| adminProdEnv = await seeders.createEnvironmentSeed(adminAccount.id, 'prod'); | ||
| await seeders.createConfigSeed(adminProdEnv, 'slack', 'slack'); | ||
| (envs as any).NANGO_ADMIN_UUID = adminAccount.uuid; | ||
| }); | ||
|
|
||
| beforeEach(() => { | ||
| originalAdminUUID = envs.NANGO_ADMIN_UUID; | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| (envs as any).NANGO_ADMIN_UUID = originalAdminUUID; | ||
| }); | ||
|
|
||
| it('should return channel using new ID-based connection', async () => { | ||
| const { account: customerAccount, env: customerEnv, user } = await seeders.seedAccountEnvAndUser(); | ||
| await db.knex('_nango_environments').where({ id: customerEnv.id }).update({ slack_notifications: true }); | ||
|
|
||
| await seeders.createConnectionSeed({ | ||
| env: adminProdEnv, | ||
| provider: 'slack', | ||
| connectionId: `account-${customerAccount.uuid}-${customerEnv.id}`, | ||
| connectionConfig: { 'incoming_webhook.channel': '#new-format-alerts' } | ||
| }); | ||
|
|
||
| const session = await authenticateUser(api, user); | ||
| const res = await api.fetch(route, { | ||
| method: 'GET', | ||
| // @ts-expect-error query params are required | ||
| query: { env: customerEnv.name }, | ||
| session | ||
| }); | ||
|
|
||
| expect(res.res.status).toBe(200); | ||
| isSuccess(res.json); | ||
| expect(res.json.environmentAndAccount.slack_notifications_channel).toBe('#new-format-alerts'); | ||
| }); | ||
|
|
||
| it('should return channel using legacy name-based connection', async () => { | ||
| const { account: customerAccount, env: customerEnv, user } = await seeders.seedAccountEnvAndUser(); | ||
| await db.knex('_nango_environments').where({ id: customerEnv.id }).update({ slack_notifications: true }); | ||
|
|
||
| await seeders.createConnectionSeed({ | ||
| env: adminProdEnv, | ||
| provider: 'slack', | ||
| connectionId: `account-${customerAccount.uuid}-${customerEnv.name}`, | ||
| connectionConfig: { 'incoming_webhook.channel': '#legacy-format-alerts' } | ||
| }); | ||
|
|
||
| const session = await authenticateUser(api, user); | ||
| const res = await api.fetch(route, { | ||
| method: 'GET', | ||
| // @ts-expect-error query params are required | ||
| query: { env: customerEnv.name }, | ||
| session | ||
| }); | ||
|
|
||
| expect(res.res.status).toBe(200); | ||
| isSuccess(res.json); | ||
| expect(res.json.environmentAndAccount.slack_notifications_channel).toBe('#legacy-format-alerts'); | ||
| }); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ import { | |
| connectionService, | ||
| environmentService, | ||
| externalWebhookService, | ||
| generateLegacySlackConnectionId, | ||
| generateSlackConnectionId, | ||
| getGlobalWebhookReceiveUrl, | ||
| getWebsocketsPath | ||
|
|
@@ -45,16 +46,23 @@ export const getEnvironment = asyncWrapper<GetEnvironment>(async (req, res) => { | |
|
|
||
| let slack_notifications_channel = ''; | ||
| if (environment.slack_notifications) { | ||
| const connectionId = generateSlackConnectionId(account.uuid, environment.name); | ||
| const integrationId = envs.NANGO_SLACK_INTEGRATION_KEY; | ||
| const env = 'prod'; | ||
| const info = await accountService.getAccountAndEnvironmentIdByUUID(envs.NANGO_ADMIN_UUID!, env); | ||
| if (info) { | ||
| const connectionConfig = await connectionService.getConnectionConfig({ | ||
| // Try new ID-based connection ID first, fall back to legacy name-based for existing installations | ||
| let connectionConfig = await connectionService.getConnectionConfig({ | ||
| provider_config_key: integrationId, | ||
| environment_id: info.environmentId, | ||
| connection_id: connectionId | ||
| connection_id: generateSlackConnectionId(account.uuid, environment.id) | ||
| }); | ||
| if (!connectionConfig || !connectionConfig['incoming_webhook.channel']) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. bc getConnectionConfig can either return an empty object or a null, see this. From my understanding, I could be wrong here since my JS is a bit rusty, when there is no record in the DB we return an empty object, but when there is a record we just return the
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indeed this was the original pattern, see this |
||
| connectionConfig = await connectionService.getConnectionConfig({ | ||
| provider_config_key: integrationId, | ||
| environment_id: info.environmentId, | ||
| connection_id: generateLegacySlackConnectionId(account.uuid, environment.name) | ||
| }); | ||
| } | ||
| if (connectionConfig && connectionConfig['incoming_webhook.channel']) { | ||
| slack_notifications_channel = connectionConfig['incoming_webhook.channel']; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -69,7 +69,8 @@ interface PostSlackMessageResponse { | |
| }; | ||
| } | ||
|
|
||
| export const generateSlackConnectionId = (accountUUID: string, environmentName: string) => `account-${accountUUID}-${environmentName}`; | ||
| export const generateSlackConnectionId = (accountUUID: string, environmentId: number) => `account-${accountUUID}-${environmentId}`; | ||
| export const generateLegacySlackConnectionId = (accountUUID: string, environmentName: string) => `account-${accountUUID}-${environmentName}`; | ||
|
|
||
| /** | ||
| * _nango_slack_notifications | ||
|
|
@@ -597,14 +598,21 @@ export class SlackService { | |
| return Err('failed_to_get_integration'); | ||
| } | ||
|
|
||
| const slackConnectionId = generateSlackConnectionId(account.uuid, environment.name); | ||
| // Try new ID-based connection ID first, fall back to legacy name-based for existing installations | ||
| let slackConnectionResult = await connectionService.getConnection( | ||
| generateSlackConnectionId(account.uuid, environment.id), | ||
| this.integrationKey, | ||
| adminRes.environment.id | ||
| ); | ||
| if (slackConnectionResult.error?.type === 'unknown_connection') { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we be consistent and use the same fallback condition here and in getEnvironment
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let me know if Im missing something here, but both are not semantically equivalent. While this one just makes a look up to the db for checking if a connection exists for enabling the dual look up (originally there was an expectation that this function should always return a record and should be attributed to the connection slack one, otherwise this would mean that the dataset was corrupted or there was a bug - which is what is happening Today) the getEnvironment one uses the We could try to use in both cases |
||
| slackConnectionResult = await connectionService.getConnection( | ||
| generateLegacySlackConnectionId(account.uuid, environment.name), | ||
| this.integrationKey, | ||
| adminRes.environment.id | ||
| ); | ||
| } | ||
|
|
||
| // we get the connection on the nango admin account to be able to send the notification | ||
| const { | ||
| success: connectionSuccess, | ||
| error: slackConnectionError, | ||
| response: slackConnection | ||
| } = await connectionService.getConnection(slackConnectionId, this.integrationKey, adminRes.environment.id); | ||
| const { success: connectionSuccess, error: slackConnectionError, response: slackConnection } = slackConnectionResult; | ||
|
|
||
| if (!connectionSuccess || !slackConnection) { | ||
| logger.error(slackConnectionError); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would create the admin account and slack config in beforeAll so it doesn't need to be done in each test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point done here.