Skip to content

Commit bc553ae

Browse files
authored
Merge pull request #7080 from LibreSign/backport/7074/stable33
[stable33] feat: implement more e2e tests
2 parents 1b44d59 + 183b6ff commit bc553ae

12 files changed

Lines changed: 621 additions & 51 deletions

playwright/e2e/multi-signer-sequential.spec.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,23 @@ test('request signatures from two signers in sequential order', async ({ page })
9999
await expect(page.getByText('Not signed yet')).toBeVisible()
100100

101101
// Now that signer01 has signed, signer02 must receive their notification.
102-
await waitForEmailTo(mailpit, 'signer02@libresign.coop', 'LibreSign: There is a file for you to sign')
102+
const email02 = await waitForEmailTo(mailpit, 'signer02@libresign.coop', 'LibreSign: There is a file for you to sign')
103103

104104
const afterSecond = await mailpit.searchMessages({ query: 'subject:"LibreSign: There is a file for you to sign"' })
105105
expect(afterSecond.messages).toHaveLength(2)
106+
107+
// Signer02 signs via their email link.
108+
// The admin is still logged out from the signer01 step, so this is unauthenticated.
109+
const signLink02 = extractSignLink(email02.Text)
110+
if (!signLink02) throw new Error('Sign link for signer02 not found in email')
111+
await page.goto(signLink02)
112+
await page.getByRole('button', { name: 'Sign the document.' }).click()
113+
await page.getByRole('button', { name: 'Sign document' }).click()
114+
await page.waitForURL('**/validation/**')
115+
await expect(page.getByText('This document is valid')).toBeVisible()
116+
117+
// Both signers must appear as signed in the final validation view.
118+
await expect(page.getByText('Signer 01')).toBeVisible()
119+
await expect(page.getByText('Signer 02')).toBeVisible()
120+
await expect(page.getByText('Not signed yet')).not.toBeVisible()
106121
})
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { expect, test } from '@playwright/test'
7+
import { login } from '../support/nc-login'
8+
import { configureOpenSsl, setAppConfig } from '../support/nc-provisioning'
9+
import { createMailpitClient, waitForEmailTo } from '../support/mailpit'
10+
11+
/**
12+
* After an admin sends a signature request, they can re-notify a signer who
13+
* has not yet signed by using the "Send reminder" action in the signer row.
14+
* This test verifies that clicking "Send reminder" causes a second notification
15+
* email to be delivered to the signer's mailbox.
16+
*/
17+
test('admin can send a reminder to a pending signer', async ({ page }) => {
18+
await login(
19+
page.request,
20+
process.env.NEXTCLOUD_ADMIN_USER ?? 'admin',
21+
process.env.NEXTCLOUD_ADMIN_PASSWORD ?? 'admin',
22+
)
23+
24+
await configureOpenSsl(page.request, 'LibreSign Test', {
25+
C: 'BR',
26+
OU: ['Organization Unit'],
27+
ST: 'Rio de Janeiro',
28+
O: 'LibreSign',
29+
L: 'Rio de Janeiro',
30+
})
31+
32+
await setAppConfig(
33+
page.request,
34+
'libresign',
35+
'identify_methods',
36+
JSON.stringify([
37+
{ name: 'account', enabled: false, mandatory: false },
38+
{ name: 'email', enabled: true, mandatory: true, signatureMethods: { clickToSign: { enabled: true } }, can_create_account: false },
39+
]),
40+
)
41+
42+
const mailpit = createMailpitClient()
43+
await mailpit.deleteMessages()
44+
45+
await page.goto('./apps/libresign')
46+
await page.getByRole('button', { name: 'Upload from URL' }).click()
47+
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')
48+
await page.getByRole('button', { name: 'Send' }).click()
49+
50+
// Only the email method is active — no tabs in the Add signer dialog
51+
await page.getByRole('button', { name: 'Add signer' }).click()
52+
await page.getByPlaceholder('Email').click()
53+
await page.getByPlaceholder('Email').pressSequentially('signer01@libresign.coop', { delay: 50 })
54+
await page.getByRole('option', { name: 'signer01@libresign.coop' }).click()
55+
await page.getByRole('textbox', { name: 'Signer name' }).fill('Signer 01')
56+
await page.getByRole('button', { name: 'Save' }).click()
57+
58+
await page.getByRole('button', { name: 'Request signatures' }).click()
59+
await page.getByRole('button', { name: 'Send' }).click()
60+
61+
// Confirm the initial notification email arrived — one email so far.
62+
await waitForEmailTo(mailpit, 'signer01@libresign.coop', 'LibreSign: There is a file for you to sign')
63+
const afterInitial = await mailpit.searchMessages({ query: 'to:signer01@libresign.coop subject:"LibreSign: There is a file for you to sign"' })
64+
expect(afterInitial.messages).toHaveLength(1)
65+
66+
// Find the signer row and click "Send reminder" from its action menu.
67+
// The signer row renders as NcListItem with force-display-actions, so the
68+
// three-dots NcActions toggle is always visible (aria-label="Actions").
69+
await page.locator('li').filter({ hasText: 'Signer 01' }).getByRole('button', { name: 'Actions' }).click()
70+
await page.getByRole('menuitem', { name: 'Send reminder' }).click()
71+
72+
// The reminder uses a different subject: "LibreSign: Changes into a file for you to sign".
73+
await waitForEmailTo(mailpit, 'signer01@libresign.coop', 'LibreSign: Changes into a file for you to sign')
74+
const afterReminder = await mailpit.searchMessages({ query: 'to:signer01@libresign.coop subject:"LibreSign: Changes into a file for you to sign"' })
75+
expect(afterReminder.messages).toHaveLength(1)
76+
})
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { test, expect } from '@playwright/test'
7+
import { login } from '../support/nc-login'
8+
import { configureOpenSsl, setAppConfig } from '../support/nc-provisioning'
9+
import { createMailpitClient, waitForEmailTo, extractSignLink, extractTokenFromEmail } from '../support/mailpit'
10+
11+
/**
12+
* An authenticated Nextcloud user can sign a document via the email+token
13+
* identify method when the signer's email matches their Nextcloud account email.
14+
*
15+
* The admin's Nextcloud account email is admin@email.tld. This test adds that
16+
* same email as the signer's email, keeps the admin logged in, and verifies the
17+
* full email-token flow succeeds (the backend allows it because the session
18+
* email matches the signer email in throwIfIsAuthenticatedWithDifferentAccount).
19+
*/
20+
test('sign document with email token as authenticated signer', async ({ page }) => {
21+
await login(
22+
page.request,
23+
process.env.NEXTCLOUD_ADMIN_USER ?? 'admin',
24+
process.env.NEXTCLOUD_ADMIN_PASSWORD ?? 'admin',
25+
)
26+
27+
await configureOpenSsl(page.request, 'LibreSign Test', {
28+
C: 'BR',
29+
OU: ['Organization Unit'],
30+
ST: 'Rio de Janeiro',
31+
O: 'LibreSign',
32+
L: 'Rio de Janeiro',
33+
})
34+
35+
await setAppConfig(
36+
page.request,
37+
'libresign',
38+
'identify_methods',
39+
JSON.stringify([
40+
{ name: 'account', enabled: false, mandatory: false },
41+
{ name: 'email', enabled: true, mandatory: true, signatureMethods: { emailToken: { enabled: true } }, can_create_account: false },
42+
]),
43+
)
44+
45+
const mailpit = createMailpitClient()
46+
await mailpit.deleteMessages()
47+
48+
await page.goto('./apps/libresign')
49+
await page.getByRole('button', { name: 'Upload from URL' }).click()
50+
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')
51+
await page.getByRole('button', { name: 'Send' }).click()
52+
53+
// Add the admin's own email as the signer.
54+
// Only the email method is active so there are no tabs in the Add signer dialog.
55+
await page.getByRole('button', { name: 'Add signer' }).click()
56+
await page.getByPlaceholder('Email').click()
57+
await page.getByPlaceholder('Email').pressSequentially('admin@email.tld', { delay: 50 })
58+
await page.getByRole('option', { name: 'admin@email.tld' }).click()
59+
await page.getByRole('textbox', { name: 'Signer name' }).fill('Admin')
60+
await page.getByRole('button', { name: 'Save' }).click()
61+
62+
await page.getByRole('button', { name: 'Request signatures' }).click()
63+
await page.getByRole('button', { name: 'Send' }).click()
64+
65+
// Get the sign link from the notification email sent to admin@email.tld.
66+
// The admin is intentionally NOT logged out — this tests the authenticated path.
67+
const notificationEmail = await waitForEmailTo(mailpit, 'admin@email.tld', 'LibreSign: There is a file for you to sign')
68+
const signLink = extractSignLink(notificationEmail.Text)
69+
if (!signLink) throw new Error('Sign link not found in notification email')
70+
71+
// Navigate to the sign link while still logged in as admin.
72+
// throwIfIsAuthenticatedWithDifferentAccount allows this because
73+
// admin@email.tld === the signer's email address.
74+
await page.goto(signLink)
75+
await page.getByRole('button', { name: 'Sign the document.' }).click()
76+
77+
// Complete the email token identification flow.
78+
// The email field may be pre-filled with the admin's address; fill() is safe either way.
79+
await page.getByRole('textbox', { name: 'Email' }).fill('admin@email.tld')
80+
await page.getByRole('button', { name: 'Send verification code' }).click()
81+
82+
const tokenEmail = await waitForEmailTo(mailpit, 'admin@email.tld', 'LibreSign: Code to sign file')
83+
const token = extractTokenFromEmail(tokenEmail.Text)
84+
if (!token) throw new Error('Token not found in email')
85+
await page.getByRole('textbox', { name: 'Enter your code' }).fill(token)
86+
await page.getByRole('button', { name: 'Validate code' }).click()
87+
88+
await expect(page.getByRole('heading', { name: 'Signature confirmation' })).toBeVisible()
89+
await expect(page.getByText('Your identity has been')).toBeVisible()
90+
await page.getByRole('button', { name: 'Sign document' }).click()
91+
await page.waitForURL('**/validation/**')
92+
await expect(page.getByText('This document is valid')).toBeVisible()
93+
await expect(page.getByText('Congratulations you have')).toBeVisible()
94+
})

playwright/e2e/sign-email-token-unauthenticated.spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,10 @@ test('sign document with email token as unauthenticated signer', async ({ page }
8282
await page.waitForURL('**/validation/**');
8383
await expect(page.getByText('This document is valid')).toBeVisible();
8484
await expect(page.getByText('Congratulations you have')).toBeVisible();
85+
86+
// Revisit the sign link after the document has been signed.
87+
// The signer must not be able to sign a second time.
88+
await page.goto(signLink)
89+
await expect(page.getByRole('button', { name: 'Sign the document.' })).not.toBeVisible({ timeout: 10_000 })
90+
await expect(page.getByText('This document is valid')).toBeVisible();
8591
});
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { expect, test } from '@playwright/test'
7+
import { login } from '../support/nc-login'
8+
import { configureOpenSsl, setAppConfig } from '../support/nc-provisioning'
9+
import { createMailpitClient, waitForEmailTo, extractSignLink } from '../support/mailpit'
10+
11+
/**
12+
* When an authenticated Nextcloud user visits a sign link that belongs to a
13+
* different email address, LibreSign must block the attempt with a clear error
14+
* message instead of silently failing or allowing the wrong user to sign.
15+
*
16+
* The admin is logged in as admin@email.tld but the email sign request is for
17+
* signer01@libresign.coop — they do NOT match, so the backend throws:
18+
* "This document is not yours. Log out and use the sign link again."
19+
*/
20+
test('authenticated user sees error when accessing another signer\'s email link', async ({ page }) => {
21+
await login(
22+
page.request,
23+
process.env.NEXTCLOUD_ADMIN_USER ?? 'admin',
24+
process.env.NEXTCLOUD_ADMIN_PASSWORD ?? 'admin',
25+
)
26+
27+
await configureOpenSsl(page.request, 'LibreSign Test', {
28+
C: 'BR',
29+
OU: ['Organization Unit'],
30+
ST: 'Rio de Janeiro',
31+
O: 'LibreSign',
32+
L: 'Rio de Janeiro',
33+
})
34+
35+
await setAppConfig(
36+
page.request,
37+
'libresign',
38+
'identify_methods',
39+
JSON.stringify([
40+
{ name: 'account', enabled: false, mandatory: false },
41+
{ name: 'email', enabled: true, mandatory: true, signatureMethods: { clickToSign: { enabled: true } }, can_create_account: false },
42+
]),
43+
)
44+
45+
const mailpit = createMailpitClient()
46+
await mailpit.deleteMessages()
47+
48+
await page.goto('./apps/libresign')
49+
await page.getByRole('button', { name: 'Upload from URL' }).click()
50+
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')
51+
await page.getByRole('button', { name: 'Send' }).click()
52+
53+
// Email signer — only the email method is active so there are no tabs in the Add signer dialog
54+
await page.getByRole('button', { name: 'Add signer' }).click()
55+
await page.getByPlaceholder('Email').click()
56+
await page.getByPlaceholder('Email').pressSequentially('signer01@libresign.coop', { delay: 50 })
57+
await page.getByRole('option', { name: 'signer01@libresign.coop' }).click()
58+
await page.getByRole('textbox', { name: 'Signer name' }).fill('Signer 01')
59+
await page.getByRole('button', { name: 'Save' }).click()
60+
61+
await page.getByRole('button', { name: 'Request signatures' }).click()
62+
await page.getByRole('button', { name: 'Send' }).click()
63+
64+
// Retrieve the sign link from the notification email sent to the signer.
65+
// The admin is intentionally NOT logged out — this simulates the wrong-session scenario.
66+
const email = await waitForEmailTo(mailpit, 'signer01@libresign.coop', 'LibreSign: There is a file for you to sign')
67+
const signLink = extractSignLink(email.Text)
68+
if (!signLink) throw new Error('Sign link not found in email')
69+
70+
// Admin is still logged in (admin@email.tld) but navigates to a link
71+
// that belongs to signer01@libresign.coop — the emails do NOT match.
72+
// The identity check runs on page load; the "Sign the document." button is
73+
// never rendered — the error is shown directly in the signing status panel.
74+
await page.goto(signLink)
75+
76+
// Backend must return the "wrong session" error via ACTION_DO_NOTHING.
77+
await expect(page.getByText('This document is not yours. Log out and use the sign link again.')).toBeVisible()
78+
})

src/ExternalApp.vue

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,22 @@
55

66
<template>
77
<div class="external-app">
8-
<router-view />
9-
<RightSidebar />
8+
<DefaultPageError v-if="isDoNothingError" />
9+
<router-view v-else />
10+
<RightSidebar v-if="!isDoNothingError" />
1011
</div>
1112
</template>
1213

1314
<script setup lang="ts">
14-
import { defineOptions } from 'vue'
15+
import { computed, defineOptions } from 'vue'
1516
1617
defineOptions({ name: 'LibreSignExternal' })
1718
19+
import DefaultPageError from './views/DefaultPageError.vue'
1820
import RightSidebar from './components/RightSidebar/RightSidebar.vue'
21+
import { initialActionCode, ACTION_CODES } from './helpers/ActionMapping'
22+
23+
const isDoNothingError = computed(() => initialActionCode.value === ACTION_CODES.DO_NOTHING)
1924
</script>
2025

2126
<style lang="scss">

0 commit comments

Comments
 (0)