Skip to content

Commit f75a645

Browse files
test: add readable issue 7344 e2e regression
Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
1 parent b7d9daa commit f75a645

1 file changed

Lines changed: 233 additions & 0 deletions

File tree

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
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 type { APIRequestContext, Page } from '@playwright/test'
8+
import { configureOpenSsl, setAppConfig } from '../support/nc-provisioning'
9+
import { createMailpitClient, extractSignLink, waitForEmailTo } from '../support/mailpit'
10+
11+
/**
12+
* Issue #7344 in plain words:
13+
* an external signer receives an envelope with two PDFs, the visible signature
14+
* box exists only inside one child PDF, and the signer must still be able to
15+
* create the signature and finish the signing flow.
16+
*/
17+
18+
type EnvelopeSigningScenario = {
19+
envelopeName: string
20+
signerEmail: string
21+
signerName: string
22+
}
23+
24+
type OcsEnvelopeChildSigner = {
25+
signRequestId?: number
26+
email?: string
27+
displayName?: string
28+
}
29+
30+
type OcsEnvelopeChildFile = {
31+
id?: number
32+
name?: string
33+
signers?: OcsEnvelopeChildSigner[]
34+
}
35+
36+
type OcsEnvelopeResponse = {
37+
uuid?: string
38+
files?: OcsEnvelopeChildFile[]
39+
}
40+
41+
function createIssue7344Scenario(): EnvelopeSigningScenario {
42+
return {
43+
envelopeName: `Issue 7344 envelope ${Date.now()}`,
44+
signerEmail: 'signer01@libresign.coop',
45+
signerName: 'Signer 01',
46+
}
47+
}
48+
49+
async function requestLibreSignApiAsAdmin(
50+
request: APIRequestContext,
51+
method: 'POST' | 'PATCH',
52+
path: string,
53+
body: Record<string, unknown>,
54+
) {
55+
const adminUser = process.env.NEXTCLOUD_ADMIN_USER ?? 'admin'
56+
const adminPassword = process.env.NEXTCLOUD_ADMIN_PASSWORD ?? 'admin'
57+
const auth = 'Basic ' + Buffer.from(`${adminUser}:${adminPassword}`).toString('base64')
58+
const response = await request.fetch(`./ocs/v2.php/apps/libresign/api/v1${path}`, {
59+
method,
60+
headers: {
61+
'OCS-ApiRequest': 'true',
62+
Accept: 'application/json',
63+
Authorization: auth,
64+
'Content-Type': 'application/json',
65+
},
66+
data: JSON.stringify(body),
67+
failOnStatusCode: false,
68+
})
69+
70+
if (!response.ok()) {
71+
throw new Error(`LibreSign OCS request failed: ${method} ${path} -> ${response.status()} ${await response.text()}`)
72+
}
73+
74+
return response.json() as Promise<{ ocs: { data: OcsEnvelopeResponse } }>
75+
}
76+
77+
async function enableEnvelopeScenario(request: APIRequestContext) {
78+
await configureOpenSsl(request, 'LibreSign Test', {
79+
C: 'BR',
80+
OU: ['Organization Unit'],
81+
ST: 'Rio de Janeiro',
82+
O: 'LibreSign',
83+
L: 'Rio de Janeiro',
84+
})
85+
86+
await setAppConfig(request, 'libresign', 'envelope_enabled', '1')
87+
await setAppConfig(
88+
request,
89+
'libresign',
90+
'identify_methods',
91+
JSON.stringify([
92+
{ name: 'account', enabled: false, mandatory: false },
93+
{ name: 'email', enabled: true, mandatory: true, signatureMethods: { clickToSign: { enabled: true } }, can_create_account: false },
94+
]),
95+
)
96+
}
97+
98+
async function createEnvelopeWithVisibleSignatureRequirement(
99+
request: APIRequestContext,
100+
scenario: EnvelopeSigningScenario,
101+
) {
102+
const pdfResponse = await request.get('https://raw.githubusercontent.com/LibreSign/libresign/main/tests/php/fixtures/pdfs/small_valid.pdf', {
103+
failOnStatusCode: true,
104+
})
105+
const pdfBase64 = Buffer.from(await pdfResponse.body()).toString('base64')
106+
107+
const createResponse = await requestLibreSignApiAsAdmin(request, 'POST', '/request-signature', {
108+
name: scenario.envelopeName,
109+
files: [
110+
{ name: 'issue-7344-a.pdf', base64: pdfBase64 },
111+
{ name: 'issue-7344-b.pdf', base64: pdfBase64 },
112+
],
113+
signers: [{
114+
displayName: scenario.signerName,
115+
identifyMethods: [{
116+
method: 'email',
117+
value: scenario.signerEmail,
118+
mandatory: 1,
119+
}],
120+
}],
121+
})
122+
123+
const envelope = createResponse.ocs.data
124+
const targetFile = envelope.files?.[0]
125+
const targetSigner = targetFile?.signers?.find((signer) => {
126+
return signer.email === scenario.signerEmail || signer.displayName === scenario.signerName
127+
})
128+
129+
if (!envelope.uuid || !targetFile?.id || !targetSigner?.signRequestId) {
130+
throw new Error('Failed to create envelope payload for issue #7344 e2e test')
131+
}
132+
133+
await requestLibreSignApiAsAdmin(request, 'PATCH', '/request-signature', {
134+
uuid: envelope.uuid,
135+
status: 1,
136+
visibleElements: [{
137+
type: 'signature',
138+
fileId: targetFile.id,
139+
signRequestId: targetSigner.signRequestId,
140+
coordinates: {
141+
page: 1,
142+
left: 32,
143+
top: 48,
144+
width: 180,
145+
height: 64,
146+
},
147+
}],
148+
})
149+
}
150+
151+
async function waitForSignerInvitationLink(signerEmail: string) {
152+
const email = await waitForEmailTo(
153+
createMailpitClient(),
154+
signerEmail,
155+
'LibreSign: There is a file for you to sign',
156+
)
157+
const signLink = extractSignLink(email.Text)
158+
if (!signLink) {
159+
throw new Error('Sign link not found in email')
160+
}
161+
return signLink
162+
}
163+
164+
async function openInvitationAsExternalSigner(page: Page, signLink: string) {
165+
// API setup runs as admin. Clear cookies so the browser behaves like the real external signer.
166+
await page.context().clearCookies()
167+
await page.goto(signLink)
168+
}
169+
170+
async function defineVisibleSignature(page: Page) {
171+
const deleteSignatureButton = page.getByRole('button', { name: 'Delete signature' })
172+
await deleteSignatureButton.waitFor({ state: 'visible', timeout: 3_000 }).catch(() => null)
173+
if (await deleteSignatureButton.isVisible()) {
174+
await deleteSignatureButton.click()
175+
}
176+
177+
await expect(page.getByRole('button', { name: 'Define your signature.' })).toBeVisible()
178+
await page.getByRole('button', { name: 'Define your signature.' }).click()
179+
180+
const signatureDialog = page.getByRole('dialog', { name: 'Customize your signatures' })
181+
await expect(signatureDialog).toBeVisible()
182+
await signatureDialog.locator('canvas').click({
183+
position: {
184+
x: 156,
185+
y: 132,
186+
},
187+
})
188+
await signatureDialog.getByRole('button', { name: 'Save' }).click()
189+
190+
const confirmDialog = page.getByLabel('Confirm your signature')
191+
await expect(confirmDialog).toBeVisible()
192+
await confirmDialog.getByRole('button', { name: 'Save' }).click()
193+
194+
await expect(page.getByRole('button', { name: 'Sign the document.' })).toBeVisible()
195+
}
196+
197+
async function finishSigning(page: Page) {
198+
await page.getByRole('button', { name: 'Sign the document.' }).click()
199+
await page.getByRole('button', { name: 'Sign document' }).click()
200+
}
201+
202+
async function expectEnvelopeSigned(page: Page, envelopeName: string) {
203+
await page.waitForURL('**/validation/**')
204+
await expect(page.getByText('Envelope information')).toBeVisible()
205+
await expect(page.getByText('Documents in this envelope')).toBeVisible()
206+
await expect(page.getByText('Congratulations you have digitally signed a document using LibreSign')).toBeVisible()
207+
await expect(page.locator('h2.app-sidebar-header__mainname')).toHaveText(envelopeName)
208+
await expect(page.getByText('You need to define a visible signature or initials to sign this document.')).not.toBeVisible()
209+
}
210+
211+
test('unauthenticated signer can define a visible signature for an envelope with multiple PDFs', async ({ page }) => {
212+
const scenario = createIssue7344Scenario()
213+
const mailpit = createMailpitClient()
214+
215+
// 1. Prepare the signing rules used in this scenario.
216+
await enableEnvelopeScenario(page.request)
217+
218+
// 2. Start with a clean inbox and create the envelope already containing
219+
// a visible signature box inside the first PDF of the envelope.
220+
await mailpit.deleteMessages()
221+
await createEnvelopeWithVisibleSignatureRequirement(page.request, scenario)
222+
223+
// 3. Open the invitation exactly as the external signer would do.
224+
const signLink = await waitForSignerInvitationLink(scenario.signerEmail)
225+
await openInvitationAsExternalSigner(page, signLink)
226+
227+
// 4. Define the visible signature and complete the signing action.
228+
await defineVisibleSignature(page)
229+
await finishSigning(page)
230+
231+
// 5. Confirm the signer reaches the final success screen.
232+
await expectEnvelopeSigned(page, scenario.envelopeName)
233+
})

0 commit comments

Comments
 (0)