Skip to content

Commit

Permalink
Ajout de la possibilité de créer plusieurs webhooks par entreprise (#…
Browse files Browse the repository at this point in the history
…3228)

* feat: implemented new limit per company

* test: added integration tests

* test: added more tests

* lint

* fix: sonar cloud warning fix

* refactor: grouping code for clarity

* refactor: grouping code for clarity

* lint

* feat: added prisma migration

* fix: token is required for create input

* fix: if one company's webhook fails, should still call the others

* fix: fixed use-case

* feat: minor improvements

* fix: fixed migration script that failed because of prisma error

* fix: fixed flaky tests
  • Loading branch information
GaelFerrand authored Apr 11, 2024
1 parent d57a9ff commit 83f3689
Show file tree
Hide file tree
Showing 14 changed files with 1,117 additions and 93 deletions.
126 changes: 112 additions & 14 deletions back/src/common/redis/__tests__/redisWebhooksettings.integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,29 @@ import { resetDatabase } from "../../../../integration-tests/helper";
import { companyFactory } from "../../../__tests__/factories";
import { prisma } from "@td/prisma";

const sort = array => array.sort((a, b) => a.localeCompare(b));

export const expectCompanyWebhookSettingsEndpointUrisToBe = async (
companyOrgId,
expectedUris
) => {
const webhookSettings = await getWebhookSettings(companyOrgId);

expect(webhookSettings.length).toBe(expectedUris.length);

const uris = webhookSettings.map(w => w.endpointUri);

expect(sort(uris)).toEqual(sort(expectedUris));
};

describe("webhooksettings redis", () => {
afterEach(async () => {
await resetDatabase();
await clearWebhookSetting();
});
it("should deactivate db whebook and remove redis webhook", async () => {

it("should deactivate db webhook and remove redis webhook", async () => {
// Given
const company = await companyFactory();

const whs = await webhookSettingFactory({
Expand All @@ -22,26 +39,107 @@ describe("webhooksettings redis", () => {
endpointUri: "https://lorem.ipsum"
});
expect(whs.activated).toBe(true);
let redisWhs = await getWebhookSettings(company.orgId);

expect(redisWhs.length).toBe(1);
expect(redisWhs[0].endpointUri).toBe("https://lorem.ipsum");
await expectCompanyWebhookSettingsEndpointUrisToBe(company.orgId, [
"https://lorem.ipsum"
]);

// When
await handleWebhookFail(company.orgId, "https://lorem.ipsum");
await handleWebhookFail(company.orgId, "https://lorem.ipsum");
await handleWebhookFail(company.orgId, "https://lorem.ipsum");
await handleWebhookFail(company.orgId, "https://lorem.ipsum");
await handleWebhookFail(company.orgId, "https://lorem.ipsum");

await handleWebhookFail(company.orgId);
await handleWebhookFail(company.orgId);
await handleWebhookFail(company.orgId);
await handleWebhookFail(company.orgId);
await handleWebhookFail(company.orgId);
redisWhs = await getWebhookSettings(company.orgId);
expect(redisWhs.length).toBe(1);
expect(redisWhs[0].endpointUri).toBe("https://lorem.ipsum");
await handleWebhookFail(company.orgId);
// Then
await expectCompanyWebhookSettingsEndpointUrisToBe(company.orgId, [
"https://lorem.ipsum"
]);

// Go over limit
await handleWebhookFail(company.orgId, "https://lorem.ipsum");

// Webhook should be removed
const updatedWhs = await prisma.webhookSetting.findUniqueOrThrow({
where: { id: whs.id }
});
expect(updatedWhs.activated).toBe(false);
redisWhs = await getWebhookSettings(company.orgId);
const redisWhs = await getWebhookSettings(company.orgId);
expect(redisWhs.length).toBe(0);
});

it("should deactivate targeted webhook and no other", async () => {
// Given
const company1 = await companyFactory({ webhookSettingsLimit: 2 });
const whs1 = await webhookSettingFactory({
company: company1,
token: "secret",
endpointUri: "https://url1.fr"
});
const whs2 = await webhookSettingFactory({
company: company1,
token: "secret",
endpointUri: "https://url2.fr"
});

const company2 = await companyFactory({ webhookSettingsLimit: 2 });
const whs3 = await webhookSettingFactory({
company: company2,
token: "secret",
endpointUri: "https://url3.fr"
});

await expectCompanyWebhookSettingsEndpointUrisToBe(company1.orgId, [
"https://url1.fr",
"https://url2.fr"
]);

await expectCompanyWebhookSettingsEndpointUrisToBe(company2.orgId, [
"https://url3.fr"
]);

// When
await handleWebhookFail(company1.orgId, "https://url2.fr");
await handleWebhookFail(company1.orgId, "https://url2.fr");
await handleWebhookFail(company1.orgId, "https://url2.fr");
await handleWebhookFail(company1.orgId, "https://url2.fr");
await handleWebhookFail(company1.orgId, "https://url2.fr");

// Then
await expectCompanyWebhookSettingsEndpointUrisToBe(company1.orgId, [
"https://url1.fr",
"https://url2.fr"
]);

await expectCompanyWebhookSettingsEndpointUrisToBe(company2.orgId, [
"https://url3.fr"
]);

// Go over limit
await handleWebhookFail(company1.orgId, "https://url2.fr");

// Then
const updatedWhs1 = await prisma.webhookSetting.findUniqueOrThrow({
where: { id: whs1.id, endpointUri: whs1.endpointUri }
});
expect(updatedWhs1.activated).toBe(true);

const updatedWhs = await prisma.webhookSetting.findUniqueOrThrow({
where: { id: whs2.id, endpointUri: whs2.endpointUri }
});
expect(updatedWhs.activated).toBe(false);

const updatedWhs3 = await prisma.webhookSetting.findUniqueOrThrow({
where: { id: whs3.id, endpointUri: whs3.endpointUri }
});
expect(updatedWhs3.activated).toBe(true);

await expectCompanyWebhookSettingsEndpointUrisToBe(company1.orgId, [
"https://url1.fr"
]);

await expectCompanyWebhookSettingsEndpointUrisToBe(company2.orgId, [
"https://url3.fr"
]);
});
});
59 changes: 38 additions & 21 deletions back/src/common/redis/webhooksettings.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { redisClient } from "./redis";
import { WebhookSetting } from "@prisma/client";

import { prisma } from "@td/prisma";
type WebhookInfo = { endpointUri: string; token: string };

export type WebhookInfo = { endpointUri: string; token: string };

export const WEBHOOK_SETTING_CACHE_KEY = "webhooks_setting";
const WEBHOOK_FAIL_CACHE_KEY = "webhook_fail";
Expand All @@ -18,26 +18,24 @@ export async function getWebhookSettings(
orgId: string
): Promise<WebhookInfo[]> {
const key = genWebhookKey(orgId);

const storedWebhooks = await redisClient.smembers(key);

return storedWebhooks
.map(el => el.split(separator))
.map(el => ({ endpointUri: el[0], token: el[1] }));
}

const smember = (webhookSetting: WebhookSetting) =>
`${webhookSetting.endpointUri}${separator}${webhookSetting.token}`;

/**
* Store a redis webhook setting - key : uri|token
*/
export async function setWebhookSetting(
webhookSetting: WebhookSetting
): Promise<void> {
const key = genWebhookKey(webhookSetting.orgId);

await redisClient.sadd(
key,
`${webhookSetting.endpointUri}${separator}${webhookSetting.token}`
);
await redisClient.sadd(key, smember(webhookSetting));
}

/**Delete all redis webhooks settings */
Expand All @@ -59,31 +57,50 @@ export async function clearWebhookSetting(cursor = 0): Promise<void> {
}
}

export async function delWebhookSetting(orgId: string): Promise<void> {
const key = genWebhookKey(orgId);
await redisClient.del(key);
export async function delWebhookSetting(
webhookSetting: WebhookSetting
): Promise<void> {
const key = genWebhookKey(webhookSetting.orgId);
await redisClient.srem(key, smember(webhookSetting));
}
// How many failed webhook for a given orgId before deactivation
const WEBHOOK_FAIL_ACCEPTED = parseInt(
process.env.WEBHOOK_FAIL_ACCEPTED || "5",
10
); // how many failed webhook ofor a given orgId before deactivation
);
// How long after the last fail the counter is reset
const WEBHOOK_FAIL_RESET_DELAY = parseInt(
process.env.WEBHOOK_FAIL_RESET_DELAY || "600",
10
); // how long after the last fail the counter is reset
);

const genWebhookFailKey = (orgId: string): string =>
`${WEBHOOK_FAIL_CACHE_KEY}:${orgId}`;
const genWebhookFailKey = (orgId: string, endpointUri: string): string =>
`${WEBHOOK_FAIL_CACHE_KEY}:${orgId}:${endpointUri}`;

export async function handleWebhookFail(orgId: string): Promise<void> {
const key = genWebhookFailKey(orgId);
export async function handleWebhookFail(
orgId: string,
endpointUri: string
): Promise<void> {
const key = genWebhookFailKey(orgId, endpointUri);
const failCount = await redisClient.get(key);
if (failCount && parseInt(failCount, 10) >= WEBHOOK_FAIL_ACCEPTED) {
await prisma.webhookSetting.update({
where: { orgId },
data: { activated: false }
const wehbookSetting = await prisma.webhookSetting.findFirst({
where: { endpointUri, orgId }
});
await delWebhookSetting(orgId);

if (wehbookSetting) {
await prisma.webhookSetting.update({
where: {
WebhookSetting_orgId_endpointUri_unique_together: {
orgId,
endpointUri
}
},
data: { activated: false }
});
await delWebhookSetting(wehbookSetting);
}

await redisClient.del(key);
return;
}
Expand Down
Loading

0 comments on commit 83f3689

Please sign in to comment.