diff --git a/plugins/webhooks/server/api/webhookSubscriptions.test.ts b/plugins/webhooks/server/api/webhookSubscriptions.test.ts new file mode 100644 index 0000000000..200c0dee9c --- /dev/null +++ b/plugins/webhooks/server/api/webhookSubscriptions.test.ts @@ -0,0 +1,239 @@ +import { + buildAdmin, + buildUser, + buildWebhookSubscription, +} from "@server/test/factories"; +import { getTestServer } from "@server/test/support"; + +const server = getTestServer(); + +describe("#webhookSubscriptions.list", () => { + it("should fail with status 401 unauthorized when user token is missing", async () => { + const res = await server.post("/api/webhookSubscriptions.list", { + body: {}, + }); + const body = await res.json(); + + expect(res.status).toEqual(401); + expect(body.message).toEqual("Authentication required"); + }); + + it("should fail with status 403 forbidden for non-admin user", async () => { + const user = await buildUser(); + + const res = await server.post("/api/webhookSubscriptions.list", { + body: { token: user.getJwtToken() }, + }); + const body = await res.json(); + + expect(res.status).toEqual(403); + expect(body.message).toEqual("Admin role required"); + }); + + it("should return the webhook subscriptions for the user's team", async () => { + const user = await buildAdmin(); + const webhookSubscriptions = await Promise.all( + Array(20) + .fill(1) + .map(() => + buildWebhookSubscription({ + createdById: user.id, + teamId: user.teamId, + }) + ) + ); + + const res = await server.post("/api/webhookSubscriptions.list", { + body: { token: user.getJwtToken() }, + }); + const body = await res.json(); + + expect(res.status).toEqual(200); + expect(body.data.length).toEqual(webhookSubscriptions.length); + }); +}); + +describe("#webhookSubscriptions.create", () => { + it("should fail with status 401 unauthorized when user token is missing", async () => { + const res = await server.post("/api/webhookSubscriptions.create", { + body: {}, + }); + const body = await res.json(); + + expect(res.status).toEqual(401); + expect(body.message).toEqual("Authentication required"); + }); + + it("should fail with status 403 forbidden for non-admin user", async () => { + const user = await buildUser(); + + const res = await server.post("/api/webhookSubscriptions.create", { + body: { token: user.getJwtToken() }, + }); + const body = await res.json(); + + expect(res.status).toEqual(403); + expect(body.message).toEqual("Admin role required"); + }); + + it("should successfully create a webhook subscription", async () => { + const user = await buildAdmin(); + const name = "Test webhook"; + const url = "https://www.example.com"; + const events = ["comments"]; + const secret = "Test secret"; + + const res = await server.post("/api/webhookSubscriptions.create", { + body: { + token: user.getJwtToken(), + name, + url, + events, + secret, + }, + }); + const body = await res.json(); + const webhook = body.data; + + expect(res.status).toEqual(200); + expect(webhook.name).toEqual(name); + expect(webhook.url).toEqual(url); + expect(webhook.events).toEqual(events); + expect(webhook.secret).toEqual(secret); + expect(webhook.enabled).toEqual(true); + }); +}); + +describe("#webhookSubscriptions.update", () => { + it("should fail with status 401 unauthorized when user token is missing", async () => { + const res = await server.post("/api/webhookSubscriptions.update", { + body: {}, + }); + const body = await res.json(); + + expect(res.status).toEqual(401); + expect(body.message).toEqual("Authentication required"); + }); + + it("should fail with status 403 forbidden for non-admin user", async () => { + const user = await buildUser(); + + const res = await server.post("/api/webhookSubscriptions.update", { + body: { token: user.getJwtToken() }, + }); + const body = await res.json(); + + expect(res.status).toEqual(403); + expect(body.message).toEqual("Admin role required"); + }); + + it("should successfully update a webhook subscription", async () => { + const user = await buildAdmin(); + const name = "Updated webhook name"; + const url = "https://www.example.com/update"; + const events = ["comments"]; + + const existingWebhook = await buildWebhookSubscription({ + name: "Created webhook name", + url: "https://www.example.com/create", + events: ["*"], + createdById: user.id, + teamId: user.teamId, + }); + + const res = await server.post("/api/webhookSubscriptions.update", { + body: { + token: user.getJwtToken(), + id: existingWebhook.id, + name, + url, + events, + }, + }); + const body = await res.json(); + const webhook = body.data; + + expect(res.status).toEqual(200); + expect(webhook.name).toEqual(name); + expect(webhook.url).toEqual(url); + expect(webhook.events).toEqual(events); + expect(webhook.enabled).toEqual(true); + }); + + it("should activate a disabled webhook subscription when it's updated", async () => { + const user = await buildAdmin(); + const name = "Updated webhook name"; + const url = "https://www.example.com/update"; + const events = ["comments"]; + + const disabledWebhook = await buildWebhookSubscription({ + name: "Created webhook name", + url: "https://www.example.com/create", + events: ["*"], + createdById: user.id, + teamId: user.teamId, + enabled: false, + }); + + const res = await server.post("/api/webhookSubscriptions.update", { + body: { + token: user.getJwtToken(), + id: disabledWebhook.id, + name, + url, + events, + }, + }); + const body = await res.json(); + const webhook = body.data; + + expect(res.status).toEqual(200); + expect(webhook.name).toEqual(name); + expect(webhook.url).toEqual(url); + expect(webhook.events).toEqual(events); + expect(webhook.enabled).toEqual(true); + }); +}); + +describe("#webhookSubscriptions.delete", () => { + it("should fail with status 401 unauthorized when user token is missing", async () => { + const res = await server.post("/api/webhookSubscriptions.delete", { + body: {}, + }); + const body = await res.json(); + + expect(res.status).toEqual(401); + expect(body.message).toEqual("Authentication required"); + }); + + it("should fail with status 403 forbidden for non-admin user", async () => { + const user = await buildUser(); + + const res = await server.post("/api/webhookSubscriptions.delete", { + body: { token: user.getJwtToken() }, + }); + const body = await res.json(); + + expect(res.status).toEqual(403); + expect(body.message).toEqual("Admin role required"); + }); + + it("should successfully delete a webhook subscription", async () => { + const user = await buildAdmin(); + const createdWebhook = await buildWebhookSubscription({ + name: "Test webhook", + url: "https://www.example.com", + events: ["*"], + createdById: user.id, + teamId: user.teamId, + }); + + const res = await server.post("/api/webhookSubscriptions.delete", { + body: { token: user.getJwtToken(), id: createdWebhook.id }, + }); + const body = await res.json(); + + expect(res.status).toEqual(200); + expect(body.success).toEqual(true); + }); +}); diff --git a/plugins/webhooks/server/api/webhookSubscriptions.ts b/plugins/webhooks/server/api/webhookSubscriptions.ts index 27caa6ecbb..00f3da7aaf 100644 --- a/plugins/webhooks/server/api/webhookSubscriptions.ts +++ b/plugins/webhooks/server/api/webhookSubscriptions.ts @@ -5,7 +5,7 @@ import { UserRole } from "@shared/types"; import auth from "@server/middlewares/authentication"; import { transaction } from "@server/middlewares/transaction"; import validate from "@server/middlewares/validate"; -import { WebhookSubscription, Event } from "@server/models"; +import { WebhookSubscription } from "@server/models"; import { authorize } from "@server/policies"; import pagination from "@server/routes/api/middlewares/pagination"; import { APIContext } from "@server/types"; @@ -20,7 +20,9 @@ router.post( pagination(), async (ctx: APIContext) => { const { user } = ctx.state.auth; + authorize(user, "listWebhookSubscription", user.team); + const webhooks = await WebhookSubscription.findAll({ where: { teamId: user.teamId, @@ -43,34 +45,19 @@ router.post( validate(T.WebhookSubscriptionsCreateSchema), transaction(), async (ctx: APIContext) => { - const { transaction } = ctx.state; + const { name, url, secret, events } = ctx.input.body; const { user } = ctx.state.auth; + authorize(user, "createWebhookSubscription", user.team); - const { name, url, secret } = ctx.input.body; - const events: string[] = compact(ctx.input.body.events); - - const webhookSubscription = await WebhookSubscription.create( - { - name, - events, - createdById: user.id, - teamId: user.teamId, - url, - enabled: true, - secret: isEmpty(secret) ? undefined : secret, - }, - { transaction } - ); - - await Event.createFromContext(ctx, { - name: "webhookSubscriptions.create", - modelId: webhookSubscription.id, - data: { - name, - url, - events, - }, + const webhookSubscription = await WebhookSubscription.createWithCtx(ctx, { + name, + url, + events: compact(events), + enabled: true, + secret: isEmpty(secret) ? undefined : secret, + createdById: user.id, + teamId: user.teamId, }); ctx.body = { @@ -88,6 +75,7 @@ router.post( const { id } = ctx.input.body; const { user } = ctx.state.auth; const { transaction } = ctx.state; + const webhookSubscription = await WebhookSubscription.findByPk(id, { rejectOnEmpty: true, lock: transaction.LOCK.UPDATE, @@ -96,17 +84,7 @@ router.post( authorize(user, "delete", webhookSubscription); - await webhookSubscription.destroy({ transaction }); - - await Event.createFromContext(ctx, { - name: "webhookSubscriptions.delete", - modelId: webhookSubscription.id, - data: { - name: webhookSubscription.name, - url: webhookSubscription.url, - events: webhookSubscription.events, - }, - }); + await webhookSubscription.destroyWithCtx(ctx); ctx.body = { success: true, @@ -120,10 +98,10 @@ router.post( validate(T.WebhookSubscriptionsUpdateSchema), transaction(), async (ctx: APIContext) => { - const { id, name, url, secret } = ctx.input.body; + const { id, name, url, secret, events } = ctx.input.body; const { user } = ctx.state.auth; const { transaction } = ctx.state; - const events: string[] = compact(ctx.input.body.events); + const webhookSubscription = await WebhookSubscription.findByPk(id, { rejectOnEmpty: true, lock: transaction.LOCK.UPDATE, @@ -132,25 +110,12 @@ router.post( authorize(user, "update", webhookSubscription); - await webhookSubscription.update( - { - name, - url, - events, - enabled: true, - secret: isEmpty(secret) ? undefined : secret, - }, - { transaction } - ); - - await Event.createFromContext(ctx, { - name: "webhookSubscriptions.update", - modelId: webhookSubscription.id, - data: { - name: webhookSubscription.name, - url: webhookSubscription.url, - events: webhookSubscription.events, - }, + await webhookSubscription.updateWithCtx(ctx, { + name, + url, + events: compact(events), + enabled: true, + secret: isEmpty(secret) ? undefined : secret, }); ctx.body = { diff --git a/server/models/WebhookSubscription.ts b/server/models/WebhookSubscription.ts index 92d5bce563..38e227b044 100644 --- a/server/models/WebhookSubscription.ts +++ b/server/models/WebhookSubscription.ts @@ -44,6 +44,8 @@ class WebhookSubscription extends ParanoidModel< InferAttributes, Partial> > { + static eventNamespace = "webhookSubscriptions"; + @NotEmpty @Length({ max: 255, msg: "Webhook name be less than 255 characters" }) @Column