mirror of
https://github.com/documenso/documenso.git
synced 2025-11-24 21:51:40 +10:00
378 lines
12 KiB
TypeScript
378 lines
12 KiB
TypeScript
import { expect, test } from '@playwright/test';
|
|
import { WebhookCallStatus, WebhookTriggerEvents } from '@prisma/client';
|
|
|
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
|
import { prisma } from '@documenso/prisma';
|
|
import { seedBlankDocument } from '@documenso/prisma/seed/documents';
|
|
import { seedUser } from '@documenso/prisma/seed/users';
|
|
|
|
import { apiSignin, apiSignout } from '../fixtures/authentication';
|
|
import { expectTextToBeVisible } from '../fixtures/generic';
|
|
|
|
/**
|
|
* Helper function to seed a webhook directly in the database for testing.
|
|
*/
|
|
const seedWebhook = async ({
|
|
webhookUrl,
|
|
eventTriggers,
|
|
secret,
|
|
enabled,
|
|
userId,
|
|
teamId,
|
|
}: {
|
|
webhookUrl: string;
|
|
eventTriggers: WebhookTriggerEvents[];
|
|
secret?: string | null;
|
|
enabled?: boolean;
|
|
userId: number;
|
|
teamId: number;
|
|
}) => {
|
|
return await prisma.webhook.create({
|
|
data: {
|
|
webhookUrl,
|
|
eventTriggers,
|
|
secret: secret ?? null,
|
|
enabled: enabled ?? true,
|
|
userId,
|
|
teamId,
|
|
},
|
|
});
|
|
};
|
|
|
|
test('[WEBHOOKS]: create webhook', async ({ page }) => {
|
|
const { user, team } = await seedUser();
|
|
|
|
await apiSignin({
|
|
page,
|
|
email: user.email,
|
|
redirectPath: `/t/${team.url}/settings/webhooks`,
|
|
});
|
|
|
|
const webhookUrl = `https://example.com/webhook-${Date.now()}`;
|
|
|
|
// Click Create Webhook button
|
|
await page.getByRole('button', { name: 'Create Webhook' }).click();
|
|
|
|
// Fill in the form
|
|
await page.getByLabel('Webhook URL*').fill(webhookUrl);
|
|
|
|
// Select event trigger - click on the triggers field and select DOCUMENT_CREATED
|
|
await page.getByLabel('Triggers').click();
|
|
await page.waitForTimeout(200); // Wait for dropdown to open
|
|
await page.getByText('document.created').click();
|
|
|
|
// Click outside the triggers field to close the dropdown
|
|
await page.getByText('The URL for Documenso to send webhook events to.').click();
|
|
|
|
// Fill in the form
|
|
await page.getByLabel('Secret').fill('secret');
|
|
|
|
// Submit the form
|
|
await page.getByRole('button', { name: 'Create' }).click();
|
|
|
|
// Wait for success toast
|
|
await expectTextToBeVisible(page, 'Webhook created');
|
|
await expectTextToBeVisible(page, 'The webhook was successfully created.');
|
|
|
|
// Verify webhook appears in the list
|
|
await expect(page.getByText(webhookUrl)).toBeVisible();
|
|
|
|
// Directly check database
|
|
const dbWebhook = await prisma.webhook.findFirstOrThrow({
|
|
where: {
|
|
userId: user.id,
|
|
},
|
|
});
|
|
|
|
expect(dbWebhook?.eventTriggers).toEqual([WebhookTriggerEvents.DOCUMENT_CREATED]);
|
|
expect(dbWebhook?.secret).toBe('secret');
|
|
expect(dbWebhook?.enabled).toBe(true);
|
|
});
|
|
|
|
test('[WEBHOOKS]: view webhooks', async ({ page }) => {
|
|
const { user, team } = await seedUser();
|
|
|
|
const webhookUrl = `https://example.com/webhook-${Date.now()}`;
|
|
|
|
// Create a webhook via seeding
|
|
const webhook = await seedWebhook({
|
|
webhookUrl,
|
|
eventTriggers: [WebhookTriggerEvents.DOCUMENT_CREATED, WebhookTriggerEvents.DOCUMENT_SENT],
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
enabled: true,
|
|
});
|
|
|
|
await apiSignin({
|
|
page,
|
|
email: user.email,
|
|
redirectPath: `/t/${team.url}/settings/webhooks`,
|
|
});
|
|
|
|
// Verify webhook is visible in the table
|
|
await expect(page.getByText(webhookUrl)).toBeVisible();
|
|
await expect(page.getByText('Enabled')).toBeVisible();
|
|
await expect(page.getByText('2 Events')).toBeVisible();
|
|
|
|
// Click on webhook to navigate to detail page
|
|
await page.getByText(webhookUrl).click();
|
|
|
|
// Verify detail page shows webhook information
|
|
await page.waitForURL(`/t/${team.url}/settings/webhooks/${webhook.id}`);
|
|
await expect(page.getByText(webhookUrl)).toBeVisible();
|
|
await expect(page.getByText('Enabled')).toBeVisible();
|
|
});
|
|
|
|
test('[WEBHOOKS]: delete webhook', async ({ page }) => {
|
|
const { user, team } = await seedUser();
|
|
|
|
const webhookUrl = `https://example.com/webhook-${Date.now()}`;
|
|
|
|
// Create a webhook via seeding
|
|
const webhook = await seedWebhook({
|
|
webhookUrl,
|
|
eventTriggers: [WebhookTriggerEvents.DOCUMENT_CREATED],
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
});
|
|
|
|
await apiSignin({
|
|
page,
|
|
email: user.email,
|
|
redirectPath: `/t/${team.url}/settings/webhooks`,
|
|
});
|
|
|
|
// Verify webhook is visible
|
|
await expect(page.getByText(webhookUrl)).toBeVisible();
|
|
|
|
// Find the row with the webhook and click the action dropdown
|
|
const webhookRow = page.locator('tr', { hasText: webhookUrl });
|
|
await webhookRow.getByTestId('webhook-table-action-btn').click();
|
|
|
|
// Click Delete menu item
|
|
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
|
|
|
// Fill in confirmation field
|
|
const deleteMessage = `delete ${webhookUrl}`;
|
|
// The label contains "Confirm by typing:" followed by the delete message
|
|
await page.getByLabel(/Confirm by typing/).fill(deleteMessage);
|
|
|
|
// Click delete button
|
|
await page.getByRole('button', { name: 'Delete' }).click();
|
|
|
|
// Wait for success toast
|
|
await expectTextToBeVisible(page, 'Webhook deleted');
|
|
await expectTextToBeVisible(page, 'The webhook has been successfully deleted.');
|
|
|
|
// Verify webhook is removed from the list
|
|
await expect(page.getByText(webhookUrl)).not.toBeVisible();
|
|
});
|
|
|
|
test('[WEBHOOKS]: update webhook', async ({ page }) => {
|
|
const { user, team } = await seedUser();
|
|
|
|
const originalWebhookUrl = `https://example.com/webhook-original-${Date.now()}`;
|
|
const updatedWebhookUrl = `https://example.com/webhook-updated-${Date.now()}`;
|
|
|
|
// Create a webhook via seeding with initial values
|
|
const webhook = await seedWebhook({
|
|
webhookUrl: originalWebhookUrl,
|
|
eventTriggers: [WebhookTriggerEvents.DOCUMENT_CREATED, WebhookTriggerEvents.DOCUMENT_SENT],
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
enabled: true,
|
|
});
|
|
|
|
await apiSignin({
|
|
page,
|
|
email: user.email,
|
|
redirectPath: `/t/${team.url}/settings/webhooks`,
|
|
});
|
|
|
|
// Verify webhook is visible with original values
|
|
await expect(page.getByText(originalWebhookUrl)).toBeVisible();
|
|
await expect(page.getByText('Enabled')).toBeVisible();
|
|
await expect(page.getByText('2 Events')).toBeVisible();
|
|
|
|
// Find the row with the webhook and click the action dropdown
|
|
const webhookRow = page.locator('tr', { hasText: originalWebhookUrl });
|
|
await webhookRow.getByTestId('webhook-table-action-btn').click();
|
|
|
|
// Click Edit menu item
|
|
await page.getByRole('menuitem', { name: 'Edit' }).click();
|
|
|
|
// Wait for dialog to open
|
|
await page.waitForTimeout(200);
|
|
|
|
// Change the webhook URL
|
|
await page.getByLabel('Webhook URL').clear();
|
|
await page.getByLabel('Webhook URL').fill(updatedWebhookUrl);
|
|
|
|
// Disable the webhook (toggle the switch)
|
|
const enabledSwitch = page.getByLabel('Enabled');
|
|
const isChecked = await enabledSwitch.isChecked();
|
|
if (isChecked) {
|
|
await enabledSwitch.click();
|
|
}
|
|
|
|
// Change the event triggers - remove one existing event and add a new one
|
|
// The selected items are shown as badges with remove buttons
|
|
// Remove one of the existing events (DOCUMENT_SENT) by clicking its remove button
|
|
const removeButtons = page.locator('button[aria-label="Remove"]');
|
|
const removeButtonCount = await removeButtons.count();
|
|
|
|
// Remove the "DOCUMENT_SENT" event (this will remove one of the two)
|
|
if (removeButtonCount > 0) {
|
|
await removeButtons.nth(1).click();
|
|
await page.waitForTimeout(200);
|
|
}
|
|
|
|
// Add new event triggers
|
|
await page.getByLabel('Triggers').click();
|
|
await page.waitForTimeout(200);
|
|
|
|
// Select DOCUMENT_COMPLETED (this will be added to the remaining DOCUMENT_CREATED)
|
|
await page.getByText('document.completed').click();
|
|
await page.waitForTimeout(200);
|
|
|
|
// Click outside to close the dropdown
|
|
await page.getByText('The URL for Documenso to send webhook events to.').click();
|
|
|
|
// Submit the form
|
|
await page.getByRole('button', { name: 'Update' }).click();
|
|
|
|
// Wait for success toast
|
|
await expectTextToBeVisible(page, 'Webhook updated');
|
|
await expectTextToBeVisible(page, 'The webhook has been updated successfully.');
|
|
|
|
// Verify changes are reflected in the list
|
|
// The old URL should be gone and new URL should be visible
|
|
await expect(page.getByText(originalWebhookUrl)).not.toBeVisible();
|
|
await expect(page.getByText(updatedWebhookUrl)).toBeVisible();
|
|
// Verify webhook is disabled
|
|
await expect(page.getByText('Disabled')).toBeVisible();
|
|
// Verify event count is still 2 (one removed, one added - DOCUMENT_CREATED and DOCUMENT_COMPLETED)
|
|
await expect(page.getByText('2 Events')).toBeVisible();
|
|
|
|
// Check the database directly to verify
|
|
const dbWebhook = await prisma.webhook.findUnique({
|
|
where: {
|
|
id: webhook.id,
|
|
},
|
|
});
|
|
|
|
expect(dbWebhook?.eventTriggers).toEqual([
|
|
WebhookTriggerEvents.DOCUMENT_CREATED,
|
|
WebhookTriggerEvents.DOCUMENT_COMPLETED,
|
|
]);
|
|
expect(dbWebhook?.enabled).toBe(false);
|
|
expect(dbWebhook?.webhookUrl).toBe(updatedWebhookUrl);
|
|
expect(dbWebhook?.secret).toBe('');
|
|
});
|
|
|
|
test('[WEBHOOKS]: cannot see unrelated webhooks', async ({ page }) => {
|
|
// Create two separate users with teams
|
|
const user1Data = await seedUser();
|
|
const user2Data = await seedUser();
|
|
|
|
const webhookUrl1 = `https://example.com/webhook-team1-${Date.now()}`;
|
|
const webhookUrl2 = `https://example.com/webhook-team2-${Date.now()}`;
|
|
|
|
// Create webhooks for both teams with DOCUMENT_CREATED event
|
|
const webhook1 = await seedWebhook({
|
|
webhookUrl: webhookUrl1,
|
|
eventTriggers: [WebhookTriggerEvents.DOCUMENT_CREATED],
|
|
userId: user1Data.user.id,
|
|
teamId: user1Data.team.id,
|
|
enabled: true,
|
|
});
|
|
|
|
const webhook2 = await seedWebhook({
|
|
webhookUrl: webhookUrl2,
|
|
eventTriggers: [WebhookTriggerEvents.DOCUMENT_SENT],
|
|
userId: user2Data.user.id,
|
|
teamId: user2Data.team.id,
|
|
});
|
|
|
|
// Create a document on team1 to trigger the webhook
|
|
const document = await seedBlankDocument(user1Data.user, user1Data.team.id, {
|
|
createDocumentOptions: {
|
|
title: 'Test Document for Webhook',
|
|
},
|
|
});
|
|
|
|
// Create a webhook call for team1's webhook (simulating the webhook being triggered)
|
|
// Since webhooks are triggered via jobs which may not run in tests, we create the call directly
|
|
const webhookCall1 = await prisma.webhookCall.create({
|
|
data: {
|
|
webhookId: webhook1.id,
|
|
url: webhookUrl1,
|
|
event: WebhookTriggerEvents.DOCUMENT_CREATED,
|
|
status: WebhookCallStatus.SUCCESS,
|
|
responseCode: 200,
|
|
requestBody: {
|
|
event: WebhookTriggerEvents.DOCUMENT_CREATED,
|
|
payload: {
|
|
id: document.id,
|
|
title: document.title,
|
|
},
|
|
createdAt: new Date().toISOString(),
|
|
webhookEndpoint: webhookUrl1,
|
|
},
|
|
},
|
|
});
|
|
|
|
// Sign in as user1
|
|
await apiSignin({
|
|
page,
|
|
email: user1Data.user.email,
|
|
redirectPath: `/t/${user1Data.team.url}/settings/webhooks`,
|
|
});
|
|
|
|
// Verify user1 can see their webhook
|
|
await expect(page.getByText(webhookUrl1)).toBeVisible();
|
|
// Verify user1 cannot see user2's webhook
|
|
await expect(page.getByText(webhookUrl2)).not.toBeVisible();
|
|
|
|
// Navigate to team1's webhook logs page
|
|
await page.goto(
|
|
`${NEXT_PUBLIC_WEBAPP_URL()}/t/${user1Data.team.url}/settings/webhooks/${webhook1.id}`,
|
|
);
|
|
|
|
// Verify user1 can see their webhook logs
|
|
// The webhook call should be visible in the table
|
|
await expect(page.getByText(webhookCall1.id)).toBeVisible();
|
|
await expect(page.getByText('200')).toBeVisible(); // Response code
|
|
|
|
// Sign out and sign in as user2
|
|
await apiSignout({ page });
|
|
await apiSignin({
|
|
page,
|
|
email: user2Data.user.email,
|
|
redirectPath: `/t/${user2Data.team.url}/settings/webhooks`,
|
|
});
|
|
|
|
// Verify user2 can see their webhook
|
|
await expect(page.getByText(webhookUrl2)).toBeVisible();
|
|
// Verify user2 cannot see user1's webhook
|
|
await expect(page.getByText(webhookUrl1)).not.toBeVisible();
|
|
|
|
// Navigate to team2's webhook logs page
|
|
await page.goto(
|
|
`${NEXT_PUBLIC_WEBAPP_URL()}/t/${user2Data.team.url}/settings/webhooks/${webhook2.id}`,
|
|
);
|
|
|
|
// Verify user2 cannot see team1's webhook logs
|
|
// The webhook call from team1 should not be visible
|
|
await expect(page.getByText(webhookCall1.id)).not.toBeVisible();
|
|
|
|
// Attempt to access user1's webhook detail page directly via URL
|
|
await page.goto(
|
|
`${NEXT_PUBLIC_WEBAPP_URL()}/t/${user2Data.team.url}/settings/webhooks/${webhook1.id}`,
|
|
);
|
|
|
|
// Verify access is denied - should show error or redirect
|
|
// Based on the component, it shows a 404 error page
|
|
await expect(page.getByRole('heading', { name: 'Webhook not found' })).toBeVisible();
|
|
});
|