diff --git a/apps/api/src/app/integrations/e2e/create-integration.e2e.ts b/apps/api/src/app/integrations/e2e/create-integration.e2e.ts index 72c87d6401c..646d2419949 100644 --- a/apps/api/src/app/integrations/e2e/create-integration.e2e.ts +++ b/apps/api/src/app/integrations/e2e/create-integration.e2e.ts @@ -505,6 +505,50 @@ describe('Create Integration - /integration (POST)', function () { expect(second.active).to.equal(false); expect(second.priority).to.equal(0); }); + + it('should not allow creating the same novu provider on same environment twice', async function () { + const inAppPayload = { + name: InAppProviderIdEnum.Novu, + providerId: InAppProviderIdEnum.Novu, + channel: ChannelTypeEnum.IN_APP, + credentials: {}, + active: true, + check: false, + }; + + const inAppResult = await session.testAgent.post('/v1/integrations').send(inAppPayload); + + expect(inAppResult.body.statusCode).to.equal(400); + expect(inAppResult.body.message).to.equal('One environment can only have one In app provider'); + + const emailPayload = { + name: EmailProviderIdEnum.Novu, + providerId: EmailProviderIdEnum.Novu, + channel: ChannelTypeEnum.EMAIL, + credentials: {}, + active: true, + check: false, + }; + + const emailResult = await session.testAgent.post('/v1/integrations').send(emailPayload); + + expect(emailResult.body.statusCode).to.equal(409); + expect(emailResult.body.message).to.equal('Integration with novu provider for email channel already exists'); + + const smsPayload = { + name: SmsProviderIdEnum.Novu, + providerId: SmsProviderIdEnum.Novu, + channel: ChannelTypeEnum.SMS, + credentials: {}, + active: true, + check: false, + }; + + const smsResult = await session.testAgent.post('/v1/integrations').send(smsPayload); + + expect(smsResult.body.statusCode).to.equal(409); + expect(smsResult.body.message).to.equal('Integration with novu provider for sms channel already exists'); + }); }); async function insertIntegrationTwice( diff --git a/apps/api/src/app/integrations/usecases/create-integration/create-integration.usecase.ts b/apps/api/src/app/integrations/usecases/create-integration/create-integration.usecase.ts index 2220db57af2..6d8163ce6dc 100644 --- a/apps/api/src/app/integrations/usecases/create-integration/create-integration.usecase.ts +++ b/apps/api/src/app/integrations/usecases/create-integration/create-integration.usecase.ts @@ -114,7 +114,7 @@ export class CreateIntegration { if (command.providerId === SmsProviderIdEnum.Novu || command.providerId === EmailProviderIdEnum.Novu) { const count = await this.integrationRepository.count({ _environmentId: command.environmentId, - providerId: EmailProviderIdEnum.Novu, + providerId: command.providerId, channel: command.channel, }); diff --git a/apps/web/cypress/tests/integrations-list-page.spec.ts b/apps/web/cypress/tests/integrations-list-page.spec.ts index e3684f0bc12..f90fbd0a27e 100644 --- a/apps/web/cypress/tests/integrations-list-page.spec.ts +++ b/apps/web/cypress/tests/integrations-list-page.spec.ts @@ -1000,4 +1000,43 @@ describe('Integrations List Page', function () { expect(el.get(0).innerText).to.eq('20 messages per month'); }); }); + + it('should not allow creating a novu provider for the same environment if it already exists', () => { + cy.intercept('*/integrations', async () => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + }).as('getIntegrations'); + cy.intercept('*/environments').as('getEnvironments'); + + cy.visit('/integrations'); + + cy.wait('@getIntegrations'); + cy.wait('@getEnvironments'); + + cy.getByTestId('add-provider').should('be.enabled').click(); + cy.getByTestId('select-provider-sidebar').should('be.visible'); + + cy.getByTestId(`provider-${EmailProviderIdEnum.Novu}`).contains('Novu').click(); + + cy.window().then((win) => { + if (win.isDarkTheme) { + cy.getByTestId(`selected-provider-image-${EmailProviderIdEnum.Novu}`).should( + 'have.attr', + 'src', + `/static/images/providers/dark/square/${EmailProviderIdEnum.Novu}.svg` + ); + return; + } + + cy.getByTestId(`selected-provider-image-${EmailProviderIdEnum.Novu}`).should( + 'have.attr', + 'src', + `/static/images/providers/light/square/${EmailProviderIdEnum.Novu}.svg` + ); + }); + cy.getByTestId('selected-provider-name').should('be.visible').contains('Novu'); + + cy.getByTestId('select-provider-sidebar-next').should('not.be.disabled').contains('Next').click(); + cy.getByTestId('novu-provider-error').contains('You can only create one Novu Email per environment.'); + cy.getByTestId('create-provider-instance-sidebar-create').should('be.disabled'); + }); }); diff --git a/apps/web/src/pages/integrations/components/multi-provider/CreateProviderInstanceSidebar.tsx b/apps/web/src/pages/integrations/components/multi-provider/CreateProviderInstanceSidebar.tsx index 39f08a02096..52c3d37a996 100644 --- a/apps/web/src/pages/integrations/components/multi-provider/CreateProviderInstanceSidebar.tsx +++ b/apps/web/src/pages/integrations/components/multi-provider/CreateProviderInstanceSidebar.tsx @@ -1,24 +1,24 @@ +import styled from '@emotion/styled'; import { ActionIcon, Group, Radio, Text } from '@mantine/core'; -import { useEffect, useMemo } from 'react'; +import { ChannelTypeEnum, ICreateIntegrationBodyDto, NOVU_PROVIDERS, providers } from '@novu/shared'; import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useEffect, useMemo } from 'react'; import { Controller, useForm } from 'react-hook-form'; -import styled from '@emotion/styled'; -import { ChannelTypeEnum, ICreateIntegrationBodyDto, InAppProviderIdEnum, providers } from '@novu/shared'; +import { createIntegration } from '../../../../api/integration'; +import { QueryKeys } from '../../../../api/query.keys'; +import { useSegment } from '../../../../components/providers/SegmentProvider'; +import { When } from '../../../../components/utils/When'; import { Button, colors, NameInput, Sidebar } from '../../../../design-system'; -import { ArrowLeft } from '../../../../design-system/icons'; import { inputStyles } from '../../../../design-system/config/inputs.styles'; +import { ArrowLeft } from '../../../../design-system/icons'; import { useFetchEnvironments } from '../../../../hooks/useFetchEnvironments'; -import { useSegment } from '../../../../components/providers/SegmentProvider'; -import { createIntegration } from '../../../../api/integration'; -import { IntegrationsStoreModalAnalytics } from '../../constants'; -import { errorMessage, successMessage } from '../../../../utils/notifications'; -import { QueryKeys } from '../../../../api/query.keys'; -import { ProviderImage } from './SelectProviderSidebar'; import { CHANNEL_TYPE_TO_STRING } from '../../../../utils/channels'; +import { errorMessage, successMessage } from '../../../../utils/notifications'; +import { IntegrationsStoreModalAnalytics } from '../../constants'; import type { IntegrationEntity } from '../../types'; import { useProviders } from '../../useProviders'; -import { When } from '../../../../components/utils/When'; +import { ProviderImage } from './SelectProviderSidebar'; interface ICreateProviderInstanceForm { name: string; @@ -67,8 +67,8 @@ export function CreateProviderInstanceSidebar({ const selectedEnvironmentId = watch('environmentId'); - const showInAppErrorMessage = useMemo(() => { - if (!provider || integrations.length === 0 || provider.id !== InAppProviderIdEnum.Novu) { + const showNovuProvidersErrorMessage = useMemo(() => { + if (!provider || integrations.length === 0 || !NOVU_PROVIDERS.includes(provider.id)) { return false; } @@ -170,7 +170,7 @@ export function CreateProviderInstanceSidebar({ Cancel