Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
8e3a4ba
feat: Add Step 3 - Signature confirmation to ModalEmailManager
vitormattos Feb 20, 2026
3fcea12
feat: Add Step 3 - Signature confirmation to ModalTokenManager
vitormattos Feb 20, 2026
957e171
test: Add unit tests for ModalEmailManager Step 3
vitormattos Feb 20, 2026
ccbaba8
test: Add unit tests for ModalTokenManager Step 3
vitormattos Feb 20, 2026
afdaa4c
test: Create SignFinalModals test file with proper imports
vitormattos Feb 20, 2026
dbad031
fix: Fix getCapabilities mock to enable signature creation tests
vitormattos Feb 20, 2026
506e88c
test: Fix Sign.spec.js getCapabilities mock
vitormattos Feb 20, 2026
f03a2c7
refactor(sign-file-controller): simplify verification code response m…
vitormattos Feb 27, 2026
5c0f790
refactor(sign): remove ModalEmailManager replaced by ModalVerificatio…
vitormattos Feb 27, 2026
f4902f7
refactor(sign): remove ModalTokenManager replaced by ModalVerificatio…
vitormattos Feb 27, 2026
0be6e1f
feat(sign): add unified ModalVerificationCode component
vitormattos Feb 27, 2026
d7a7728
refactor(sign): use ModalVerificationCode instead of separate modals
vitormattos Feb 27, 2026
ad9c438
test(sign): remove ModalEmailManager.spec.js
vitormattos Feb 27, 2026
d7e7867
test(sign): remove ModalEmailManager.spec.ts
vitormattos Feb 27, 2026
f4827ce
test(sign): remove ModalTokenManager.spec.js
vitormattos Feb 27, 2026
8e9e691
test(sign): remove dead SignFinalModals spec
vitormattos Feb 27, 2026
91b2ee8
test(sign): add ModalVerificationCode spec
vitormattos Feb 27, 2026
c9f667b
test(sign): remove invalid $emit mock from Sign spec
vitormattos Feb 27, 2026
1981d55
fix: return the kind of modal
vitormattos Feb 27, 2026
641bdc1
feat(sign): improve email verification step descriptions
vitormattos Feb 27, 2026
afe76cd
fix(ModalVerificationCode): keep modal open during signing request
vitormattos Mar 1, 2026
612b75c
fix: change the button name at step
vitormattos Mar 1, 2026
8a37c99
test(e2e): sign document with email token as unauthenticated signer
vitormattos Mar 1, 2026
88d1114
chore(deps): add mailpit-api as dev dependency
vitormattos Mar 1, 2026
43e017c
test(e2e): add mailpit helper for email-based test flows
vitormattos Mar 1, 2026
1b044ad
test(e2e): move testDir to playwright/e2e
vitormattos Mar 1, 2026
ed33bb4
fix: add spdx header
vitormattos Mar 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/Controller/SignFileController.php
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ private function getCode(SignRequest $signRequest): DataResponse {
signMethodName: $this->request->getParam('signMethod', ''),
identify: $this->request->getParam('identify', ''),
);
$message = $this->l10n->t('The code to sign file was successfully requested.');
$message = $this->l10n->t('Verification code sent.');
$statusCode = Http::STATUS_OK;
} catch (\Throwable $th) {
$message = $th->getMessage();
Expand Down
41 changes: 41 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,19 +78,20 @@
"npm": "^11.3.0"
},
"devDependencies": {
"@playwright/test": "^1.58.1",
"@nextcloud/browserslist-config": "^3.1.2",
"@nextcloud/eslint-config": "^8.4.2",
"@nextcloud/stylelint-config": "^3.2.1",
"@nextcloud/vite-config": "^2.5.2",
"@pinia/testing": "^1.0.3",
"@playwright/test": "^1.58.1",
"@testing-library/dom": "^10.4.1",
"@testing-library/vue": "^8.1.0",
"@vitejs/plugin-vue": "^6.0.3",
"@vitest/coverage-v8": "^4.0.18",
"@vue/test-utils": "^2.4.6",
"@vue/tsconfig": "^0.8.1",
"happy-dom": "^20.7.0",
"mailpit-api": "^1.7.2",
"openapi-typescript": "^7.13.0",
"typescript": "^5.9.3",
"vite": "^7.1.10",
Expand Down
2 changes: 1 addition & 1 deletion playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { defineConfig, devices } from '@playwright/test'
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './playwright',
testDir: './playwright/e2e',

/* Run tests in files in parallel */
fullyParallel: true,
Expand Down
85 changes: 85 additions & 0 deletions playwright/e2e/sign-email-token-unauthenticated.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { test, expect } from '@playwright/test';
import { login } from '../support/nc-login'
import { configureOpenSsl, setAppConfig } from '../support/nc-provisioning'
import { createMailpitClient, waitForEmailTo, extractSignLink, extractTokenFromEmail } from '../support/mailpit'

test('sign document with email token as unauthenticated signer', 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: true, mandatory: false },
{ name: 'email', enabled: true, mandatory: true, signatureMethods: { emailToken: { enabled: true } }, can_create_account: false },
]),
)

await page.goto('./apps/libresign')
await page.getByRole('button', { name: 'Upload from URL' }).click();
await page.getByRole('textbox', { name: 'URL of a PDF file' }).click();
await page.getByRole('textbox', { name: 'URL of a PDF file' }).fill('http://raw.githubusercontent.com/LibreSign/libresign/main/tests/php/fixtures/pdfs/small_valid.pdf');
await page.getByRole('button', { name: 'Send' }).click();
await page.getByRole('button', { name: 'Add signer' }).click();
await page.getByRole('tab', { name: 'Email' }).click();
await page.getByPlaceholder('Email').click();
await page.getByPlaceholder('Email').fill('[email protected]');
await page.getByRole('option', { name: '[email protected]' }).click();
await page.getByRole('textbox', { name: 'Signer name' }).click();
await page.getByRole('textbox', { name: 'Signer name' }).press('ControlOrMeta+a');
await page.getByRole('textbox', { name: 'Signer name' }).fill('Signer 01');
await page.getByRole('button', { name: 'Save' }).click();

const mailpit = createMailpitClient()
await mailpit.deleteMessages()

await page.getByRole('button', { name: 'Request signatures' }).click();
await page.getByRole('button', { name: 'Send' }).click();

// Logout before accessing the sign link to avoid session-related issues.
await page.getByRole('button', { name: 'Settings menu' }).click();
await page.getByRole('link', { name: 'Log out' }).click();

const email = await waitForEmailTo(mailpit, '[email protected]', 'LibreSign: There is a file for you to sign')
const signLink = extractSignLink(email.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('textbox', { name: 'Email' }).click();
await page.getByRole('textbox', { name: 'Email' }).fill('[email protected]');
await page.getByRole('button', { name: 'Send verification code' }).click();

const tokenEmail = await waitForEmailTo(mailpit, '[email protected]', 'LibreSign: Code to sign file')
const token = extractTokenFromEmail(tokenEmail.Text)
if (!token) throw new Error('Token not found in email')
await page.getByRole('textbox', { name: 'Enter your code' }).click();
await page.getByRole('textbox', { name: 'Enter your code' }).fill(token);
await page.getByRole('button', { name: 'Validate code' }).click();

await expect(page.getByRole('heading', { name: 'Signature confirmation' })).toBeVisible();
await expect(page.getByText('Step 3 of 3 - Signature')).toBeVisible();
await expect(page.getByText('Your identity has been')).toBeVisible();
await expect(page.getByText('You can now sign the document.')).toBeVisible();
await page.getByRole('button', { name: 'Sign document' }).click();
await page.waitForURL('**/validation/**');
await expect(page.getByText('This document is valid')).toBeVisible();
await expect(page.getByText('Congratulations you have')).toBeVisible();
});
2 changes: 1 addition & 1 deletion playwright/e2e/sign-herself-with-click-to-sign.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ test('sign herself with click to sign', async ({ page }) => {
await page.getByRole('button', { name: 'Send' }).click();
await page.getByRole('button', { name: 'Sign document' }).click();
await page.getByRole('button', { name: 'Sign the document.' }).click();
await page.getByRole('button', { name: 'Confirm' }).click();
await page.getByRole('button', { name: 'Sign document' }).click();
await page.waitForURL('**/validation/**');
await expect(page.getByText('This document is valid')).toBeVisible();
await page.getByRole('button', { name: 'Expand details' }).click();
Expand Down
80 changes: 80 additions & 0 deletions playwright/support/mailpit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { MailpitClient } from 'mailpit-api'

export type { MailpitClient }

type Message = Awaited<ReturnType<MailpitClient['getMessageSummary']>>

/** Creates a MailpitClient using MAILPIT_URL (default: http://localhost:8025). */
export function createMailpitClient(): MailpitClient {
return new MailpitClient(process.env.MAILPIT_URL ?? 'http://localhost:8025')
}

/** Fetches the latest email sent to `toAddress`, optionally filtered by `subject`. */
export async function getLatestEmailTo(
client: MailpitClient,
toAddress: string,
subject?: string,
): Promise<Message> {
const query = subject
? `to:${toAddress} subject:"${subject}"`
: `to:${toAddress}`
const result = await client.searchMessages({ query })
if (!result.messages || result.messages.length === 0) {
throw new Error(`No email found for "${toAddress}"${subject ? ` with subject "${subject}"` : ''}`)
}
return await client.getMessageSummary(result.messages[0].ID)
}

/**
* Polls MailPit until an email matching `toAddress` (and optional `subject`) is found,
* or until `timeout` ms elapse. Checks every `interval` ms (defaults: 30 s / 1 s).
*/
export async function waitForEmailTo(
client: MailpitClient,
toAddress: string,
subject?: string,
options?: { timeout?: number; interval?: number },
): Promise<Message> {
const timeout = options?.timeout ?? 30_000
const interval = options?.interval ?? 1_000
const deadline = Date.now() + timeout
while (Date.now() < deadline) {
try {
return await getLatestEmailTo(client, toAddress, subject)
} catch {
// email not arrived yet
}
await new Promise(resolve => setTimeout(resolve, interval))
}
throw new Error(
`Timeout (${timeout} ms) waiting for email to "${toAddress}"${
subject ? ` with subject "${subject}"` : ''
}`,
)
}

/** Extracts a LibreSign sign link from an email body matching /p/sign/{uuid}. */
export function extractSignLink(body: string): string | null {
const match = body.match(/\S+\/p\/sign\/[\w-]+/)
return match ? match[0] : null
}

/** Extracts a numeric token from an email body. Default pattern: 4-8 digit sequence. */
export function extractTokenFromEmail(
body: string,
pattern: RegExp = /Use this code to sign the document:[\s\S]*?(\d{6})/,
): string | null {
const match = body.match(pattern)
return match ? match[1] : null
}

/** Extracts the first URL from an email body (email.Text). */
export function extractLinkFromEmail(body: string): string | null {
const match = body.match(/https?:\/\/\S+/)
return match ? match[0] : null
}
Loading
Loading