From 43fac1ab78d7d87e507d43dcf8e6fd98947cb39f Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 2 Mar 2026 17:27:38 -0300 Subject: [PATCH 01/18] test(e2e): add parallel multi-signer signature request test Upload PDF, add two email signers (signer01 and signer02), confirm the 'Sign in order' switch is visible and unchecked by default, send the request and verify via mailpit that both signers receive their notification email simultaneously, confirming parallel mode. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- playwright/e2e/multi-signer-parallel.spec.ts | 78 ++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 playwright/e2e/multi-signer-parallel.spec.ts diff --git a/playwright/e2e/multi-signer-parallel.spec.ts b/playwright/e2e/multi-signer-parallel.spec.ts new file mode 100644 index 0000000000..e4b7466fef --- /dev/null +++ b/playwright/e2e/multi-signer-parallel.spec.ts @@ -0,0 +1,78 @@ +/** + * SPDX-FileCopyrightText: 2026 LibreCode coop and contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { expect, test } from '@playwright/test' +import { login } from '../support/nc-login' +import { configureOpenSsl, setAppConfig } from '../support/nc-provisioning' +import { createMailpitClient, waitForEmailTo } from '../support/mailpit' + +test('request signatures from two signers in parallel', async ({ page }) => { + await login( + page.request, + process.env.NEXTCLOUD_ADMIN_USER ?? 'admin', + process.env.NEXTCLOUD_ADMIN_PASSWORD ?? 'admin', + ) + + await configureOpenSsl(page.request, 'LibreSign Test', { + C: 'BR', + OU: ['Organization Unit'], + ST: 'Rio de Janeiro', + O: 'LibreSign', + L: 'Rio de Janeiro', + }) + + await setAppConfig( + page.request, + 'libresign', + 'identify_methods', + JSON.stringify([ + { name: 'account', enabled: false, mandatory: false }, + { name: 'email', enabled: true, mandatory: true, signatureMethods: { clickToSign: { enabled: true } }, can_create_account: false }, + ]), + ) + + const mailpit = createMailpitClient() + await mailpit.deleteMessages() + + await page.goto('./apps/libresign') + await page.getByRole('button', { name: 'Upload from URL' }).click() + await page.getByRole('textbox', { name: 'URL of a PDF file' }).fill('https://raw.githubusercontent.com/LibreSign/libresign/main/tests/php/fixtures/pdfs/small_valid.pdf') + await page.getByRole('button', { name: 'Send' }).click() + + // Add first signer via email tab + await page.getByRole('button', { name: 'Add signer' }).click() + await page.getByRole('tab', { name: 'Email' }).click() + await page.getByPlaceholder('Email').fill('signer01@libresign.coop') + await page.getByRole('option', { name: 'signer01@libresign.coop' }).click() + await page.getByRole('textbox', { name: 'Signer name' }).fill('Signer 01') + await page.getByRole('button', { name: 'Save' }).click() + + // Add second signer via email tab + await page.getByRole('button', { name: 'Add signer' }).click() + await page.getByRole('tab', { name: 'Email' }).click() + await page.getByPlaceholder('Email').fill('signer02@libresign.coop') + await page.getByRole('option', { name: 'signer02@libresign.coop' }).click() + await page.getByRole('textbox', { name: 'Signer name' }).fill('Signer 02') + await page.getByRole('button', { name: 'Save' }).click() + + // With 2+ signers the "Sign in order" switch must be visible and unchecked by default, + // meaning parallel flow — both signers will be notified at the same time. + const signInOrderSwitch = page.getByRole('switch', { name: 'Sign in order' }) + await expect(signInOrderSwitch).toBeVisible() + await expect(signInOrderSwitch).not.toBeChecked() + + // Send the signature request + await page.getByRole('button', { name: 'Request signatures' }).click() + await page.getByRole('button', { name: 'Send' }).click() + + // In parallel mode both signers are notified simultaneously. + // Proof: wait for signer01's email, then verify that signer02's email also arrived. + await waitForEmailTo(mailpit, 'signer01@libresign.coop', 'LibreSign: There is a file for you to sign') + await waitForEmailTo(mailpit, 'signer02@libresign.coop', 'LibreSign: There is a file for you to sign') + + // Both emails arrived — both signers were notified at the same time, confirming parallel mode. + const result = await mailpit.searchMessages({ query: 'subject:"LibreSign: There is a file for you to sign"' }) + expect(result.messages).toHaveLength(2) +}) From ffa012f4aafb34e410c6b9941f144145fcc64db3 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 2 Mar 2026 17:27:38 -0300 Subject: [PATCH 02/18] test(e2e): add sequential multi-signer signature request test Upload PDF, add two email signers, enable 'Sign in order' switch, send the request and verify via mailpit that only signer01 receives an email immediately (signer02 is still in Draft). Then signer01 signs via the email link and the test verifies that signer02 now receives their notification, confirming sequential mode. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../e2e/multi-signer-sequential.spec.ts | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 playwright/e2e/multi-signer-sequential.spec.ts diff --git a/playwright/e2e/multi-signer-sequential.spec.ts b/playwright/e2e/multi-signer-sequential.spec.ts new file mode 100644 index 0000000000..7479cbd2bb --- /dev/null +++ b/playwright/e2e/multi-signer-sequential.spec.ts @@ -0,0 +1,91 @@ +/** + * SPDX-FileCopyrightText: 2026 LibreCode coop and contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { expect, test } from '@playwright/test' +import { login } from '../support/nc-login' +import { configureOpenSsl, setAppConfig } from '../support/nc-provisioning' +import { createMailpitClient, waitForEmailTo, extractSignLink } from '../support/mailpit' + +test('request signatures from two signers in sequential order', async ({ page }) => { + await login( + page.request, + process.env.NEXTCLOUD_ADMIN_USER ?? 'admin', + process.env.NEXTCLOUD_ADMIN_PASSWORD ?? 'admin', + ) + + await configureOpenSsl(page.request, 'LibreSign Test', { + C: 'BR', + OU: ['Organization Unit'], + ST: 'Rio de Janeiro', + O: 'LibreSign', + L: 'Rio de Janeiro', + }) + + await setAppConfig( + page.request, + 'libresign', + 'identify_methods', + JSON.stringify([ + { name: 'account', enabled: false, mandatory: false }, + { name: 'email', enabled: true, mandatory: true, signatureMethods: { clickToSign: { enabled: true } }, can_create_account: false }, + ]), + ) + + const mailpit = createMailpitClient() + await mailpit.deleteMessages() + + await page.goto('./apps/libresign') + await page.getByRole('button', { name: 'Upload from URL' }).click() + await page.getByRole('textbox', { name: 'URL of a PDF file' }).fill('https://raw.githubusercontent.com/LibreSign/libresign/main/tests/php/fixtures/pdfs/small_valid.pdf') + await page.getByRole('button', { name: 'Send' }).click() + + // Add first signer via email tab + await page.getByRole('button', { name: 'Add signer' }).click() + await page.getByRole('tab', { name: 'Email' }).click() + await page.getByPlaceholder('Email').fill('signer01@libresign.coop') + await page.getByRole('option', { name: 'signer01@libresign.coop' }).click() + await page.getByRole('textbox', { name: 'Signer name' }).fill('Signer 01') + await page.getByRole('button', { name: 'Save' }).click() + + // Add second signer via email tab + await page.getByRole('button', { name: 'Add signer' }).click() + await page.getByRole('tab', { name: 'Email' }).click() + await page.getByPlaceholder('Email').fill('signer02@libresign.coop') + await page.getByRole('option', { name: 'signer02@libresign.coop' }).click() + await page.getByRole('textbox', { name: 'Signer name' }).fill('Signer 02') + await page.getByRole('button', { name: 'Save' }).click() + + // Enable sequential signing — the switch must be accessible by role="switch" + const signInOrderSwitch = page.getByRole('switch', { name: 'Sign in order' }) + await expect(signInOrderSwitch).toBeVisible() + await signInOrderSwitch.click() + await expect(signInOrderSwitch).toBeChecked() + + // Send the signature request + await page.getByRole('button', { name: 'Request signatures' }).click() + await page.getByRole('button', { name: 'Send' }).click() + + // In sequential mode only signer01 (order 1) gets the email immediately. + // Proof: signer01's email arrives, but signer02's does NOT at this point. + const email01 = await waitForEmailTo(mailpit, 'signer01@libresign.coop', 'LibreSign: There is a file for you to sign') + + const afterFirst = await mailpit.searchMessages({ query: 'subject:"LibreSign: There is a file for you to sign"' }) + expect(afterFirst.messages).toHaveLength(1) + + // Signer01 signs via the link received in the email + const signLink = extractSignLink(email01.Text) + if (!signLink) throw new Error('Sign link not found in email') + await page.goto(signLink) + await page.getByRole('button', { name: 'Sign the document.' }).click() + await page.getByRole('button', { name: 'Sign document' }).click() + await page.waitForURL('**/validation/**') + await expect(page.getByText('This document is valid')).toBeVisible() + + // Now that signer01 has signed, signer02 must receive their notification. + await waitForEmailTo(mailpit, 'signer02@libresign.coop', 'LibreSign: There is a file for you to sign') + + const afterSecond = await mailpit.searchMessages({ query: 'subject:"LibreSign: There is a file for you to sign"' }) + expect(afterSecond.messages).toHaveLength(2) +}) From bef2cf979b0b3d889ebdb7c717a5bbe62548375f Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 2 Mar 2026 18:28:28 -0300 Subject: [PATCH 03/18] fix(signers): migrate draggable to vuedraggable 4 slot syntax vuedraggable 4 (Vue 3) removed the default slot in favour of a named #item slot. Using the old default-slot pattern causes computeNodes() to throw "draggable element must have an item slot", leaving componentStructure undefined and crashing the updated() hook. Changes: - add item-key="identify" (required by vuedraggable 4) - replace default slot with