mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-15 11:00:47 +02:00
Add frontend autoformatting and set CI to require formatted code for all languages (#6052)
# Description of Changes Changes the strategy for autoformatting to reject PRs if they are not formatted correctly instead of allowing them to merge and then spawning a new PR to fix the formatting. The old strategy just caused more work for us because we'd have to manually approve the followup PR and get it merged, which required 2 reviewers so in practice it rarely got done and just meant everyone's PRs ended up containing reformatting for unrelated files, which makes code review unnecessarily difficult. If the PR's code is not formatted correctly after this PR, a comment will be added automatically to tell the author how to run the formatter script to fix their code so it can go in. This also enables autoformatting for the frontend code, using Prettier. I've enabled it for pretty much everything in the frontend folder, other than 3rd party files and files it doesn't make sense for. I also excluded Markdown because it sounds likely to be more annoying to have to autoformat the Markdown in the frontend folder but nowhere else. Open to changing this though if people disagree. > [!note] > > Advice to reviewers: The first commit contains all of the actual logic I've introduced (CI changes, Prettier config, etc.) > The second commit is just the reformatting of the entire frontend folder. > The first commit needs proper review, the second one just give it a spot-check that it's doing what you'd expect.
This commit is contained in:
@@ -1,23 +1,23 @@
|
||||
import { test, expect, type Page } from '@playwright/test';
|
||||
import path from 'path';
|
||||
import { test, expect, type Page } from "@playwright/test";
|
||||
import path from "path";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test fixtures — pre-generated keystores in test-fixtures/certs/
|
||||
// ---------------------------------------------------------------------------
|
||||
const CERTS_DIR = path.join(__dirname, '../test-fixtures/certs');
|
||||
const VALID_P12 = path.join(CERTS_DIR, 'valid-test.p12');
|
||||
const EXPIRED_P12 = path.join(CERTS_DIR, 'expired-test.p12');
|
||||
const NOT_YET_VALID_P12 = path.join(CERTS_DIR, 'not-yet-valid-test.p12');
|
||||
const VALID_JKS = path.join(CERTS_DIR, 'valid-test.jks');
|
||||
const CERTS_DIR = path.join(__dirname, "../test-fixtures/certs");
|
||||
const VALID_P12 = path.join(CERTS_DIR, "valid-test.p12");
|
||||
const EXPIRED_P12 = path.join(CERTS_DIR, "expired-test.p12");
|
||||
const NOT_YET_VALID_P12 = path.join(CERTS_DIR, "not-yet-valid-test.p12");
|
||||
const VALID_JKS = path.join(CERTS_DIR, "valid-test.jks");
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Stable mock data returned by the mocked backend
|
||||
// ---------------------------------------------------------------------------
|
||||
const MOCK_SESSION = {
|
||||
sessionId: 'test-session-id',
|
||||
documentName: 'test-document.pdf',
|
||||
status: 'IN_PROGRESS',
|
||||
ownerUsername: 'test-owner',
|
||||
sessionId: "test-session-id",
|
||||
documentName: "test-document.pdf",
|
||||
status: "IN_PROGRESS",
|
||||
ownerUsername: "test-owner",
|
||||
message: null,
|
||||
dueDate: null,
|
||||
participants: [],
|
||||
@@ -25,10 +25,10 @@ const MOCK_SESSION = {
|
||||
|
||||
const MOCK_PARTICIPANT = {
|
||||
id: 1,
|
||||
email: '[email protected]',
|
||||
name: 'Test User',
|
||||
status: 'PENDING',
|
||||
shareToken: 'test-token',
|
||||
email: "[email protected]",
|
||||
name: "Test User",
|
||||
status: "PENDING",
|
||||
shareToken: "test-token",
|
||||
expiresAt: null,
|
||||
hasCompleted: false,
|
||||
isExpired: false,
|
||||
@@ -40,30 +40,26 @@ const MOCK_PARTICIPANT = {
|
||||
// ---------------------------------------------------------------------------
|
||||
async function mockParticipantApis(page: Page) {
|
||||
// Mock auth so AppProviders/Landing don't redirect to /login
|
||||
await page.route('**/api/v1/auth/me', (route) =>
|
||||
await page.route("**/api/v1/auth/me", (route) =>
|
||||
route.fulfill({
|
||||
json: {
|
||||
id: 1,
|
||||
username: 'testuser',
|
||||
email: '[email protected]',
|
||||
roles: ['ROLE_USER'],
|
||||
username: "testuser",
|
||||
email: "[email protected]",
|
||||
roles: ["ROLE_USER"],
|
||||
},
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
await page.route('**/api/v1/workflow/participant/session**', (route) =>
|
||||
route.fulfill({ json: MOCK_SESSION })
|
||||
);
|
||||
await page.route('**/api/v1/workflow/participant/details**', (route) =>
|
||||
route.fulfill({ json: MOCK_PARTICIPANT })
|
||||
);
|
||||
await page.route("**/api/v1/workflow/participant/session**", (route) => route.fulfill({ json: MOCK_SESSION }));
|
||||
await page.route("**/api/v1/workflow/participant/details**", (route) => route.fulfill({ json: MOCK_PARTICIPANT }));
|
||||
// Minimal stub so the download-document call doesn't throw
|
||||
await page.route('**/api/v1/workflow/participant/document**', (route) =>
|
||||
await page.route("**/api/v1/workflow/participant/document**", (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/pdf',
|
||||
contentType: "application/pdf",
|
||||
body: Buffer.alloc(128),
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -72,8 +68,8 @@ async function mockParticipantApis(page: Page) {
|
||||
// the matching option — Mantine renders a custom combobox, not a native select.
|
||||
// ---------------------------------------------------------------------------
|
||||
async function selectCertType(page: Page, label: string) {
|
||||
await page.getByTestId('cert-type-select').click();
|
||||
await page.getByRole('option', { name: label }).click();
|
||||
await page.getByTestId("cert-type-select").click();
|
||||
await page.getByRole("option", { name: label }).click();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -82,7 +78,7 @@ async function selectCertType(page: Page, label: string) {
|
||||
async function uploadCertFile(page: Page, filePath: string) {
|
||||
// Mantine FileInput uses a visually hidden <input type="file">.
|
||||
// We click the visible button to expose it, then set files via the hidden input.
|
||||
const certFileInput = page.getByTestId('cert-file-input');
|
||||
const certFileInput = page.getByTestId("cert-file-input");
|
||||
await certFileInput.click();
|
||||
// After click, the file chooser or the hidden input becomes interactive.
|
||||
// Use the first file input on the page (Mantine places it near the button).
|
||||
@@ -93,41 +89,41 @@ async function uploadCertFile(page: Page, filePath: string) {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
test.describe('Certificate Validation — ParticipantView', () => {
|
||||
test.describe("Certificate Validation — ParticipantView", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await mockParticipantApis(page);
|
||||
});
|
||||
|
||||
// 1. Happy path — valid P12
|
||||
test('valid P12 cert shows green "Certificate valid until" feedback', async ({ page }) => {
|
||||
await page.route('**/api/v1/workflow/participant/validate-certificate', (route) =>
|
||||
await page.route("**/api/v1/workflow/participant/validate-certificate", (route) =>
|
||||
route.fulfill({
|
||||
json: {
|
||||
valid: true,
|
||||
subjectName: 'Test Signer',
|
||||
notAfter: '2027-01-01T00:00:00Z',
|
||||
notBefore: '2025-01-01T00:00:00Z',
|
||||
subjectName: "Test Signer",
|
||||
notAfter: "2027-01-01T00:00:00Z",
|
||||
notBefore: "2025-01-01T00:00:00Z",
|
||||
selfSigned: true,
|
||||
error: null,
|
||||
},
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
await page.goto('/workflow/sign/test-token');
|
||||
await page.goto("/workflow/sign/test-token");
|
||||
await page.waitForSelector('[data-testid="submit-signature-button"]');
|
||||
|
||||
await uploadCertFile(page, VALID_P12);
|
||||
await page.getByTestId('cert-password-input').fill('testpass');
|
||||
await page.getByTestId("cert-password-input").fill("testpass");
|
||||
|
||||
// Wait for debounce (600 ms) + network round-trip
|
||||
const feedback = page.getByTestId('cert-validation-feedback');
|
||||
await expect(feedback).toContainText('Certificate valid until', { timeout: 5000 });
|
||||
await expect(feedback).toContainText('Test Signer');
|
||||
const feedback = page.getByTestId("cert-validation-feedback");
|
||||
await expect(feedback).toContainText("Certificate valid until", { timeout: 5000 });
|
||||
await expect(feedback).toContainText("Test Signer");
|
||||
});
|
||||
|
||||
// 2. Wrong password — red error
|
||||
test('wrong password shows red error message', async ({ page }) => {
|
||||
await page.route('**/api/v1/workflow/participant/validate-certificate', (route) =>
|
||||
test("wrong password shows red error message", async ({ page }) => {
|
||||
await page.route("**/api/v1/workflow/participant/validate-certificate", (route) =>
|
||||
route.fulfill({
|
||||
json: {
|
||||
valid: false,
|
||||
@@ -135,24 +131,24 @@ test.describe('Certificate Validation — ParticipantView', () => {
|
||||
notAfter: null,
|
||||
notBefore: null,
|
||||
selfSigned: false,
|
||||
error: 'Invalid certificate password or corrupt keystore file',
|
||||
error: "Invalid certificate password or corrupt keystore file",
|
||||
},
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
await page.goto('/workflow/sign/test-token');
|
||||
await page.goto("/workflow/sign/test-token");
|
||||
await page.waitForSelector('[data-testid="submit-signature-button"]');
|
||||
|
||||
await uploadCertFile(page, VALID_P12);
|
||||
await page.getByTestId('cert-password-input').fill('wrongpass');
|
||||
await page.getByTestId("cert-password-input").fill("wrongpass");
|
||||
|
||||
const feedback = page.getByTestId('cert-validation-feedback');
|
||||
await expect(feedback).toContainText('Invalid certificate password', { timeout: 5000 });
|
||||
const feedback = page.getByTestId("cert-validation-feedback");
|
||||
await expect(feedback).toContainText("Invalid certificate password", { timeout: 5000 });
|
||||
});
|
||||
|
||||
// 3. Expired certificate
|
||||
test('expired cert shows "Certificate has expired" error', async ({ page }) => {
|
||||
await page.route('**/api/v1/workflow/participant/validate-certificate', (route) =>
|
||||
await page.route("**/api/v1/workflow/participant/validate-certificate", (route) =>
|
||||
route.fulfill({
|
||||
json: {
|
||||
valid: false,
|
||||
@@ -160,24 +156,24 @@ test.describe('Certificate Validation — ParticipantView', () => {
|
||||
notAfter: null,
|
||||
notBefore: null,
|
||||
selfSigned: false,
|
||||
error: 'Certificate has expired (expired: 2023-01-02 00:00:00 UTC)',
|
||||
error: "Certificate has expired (expired: 2023-01-02 00:00:00 UTC)",
|
||||
},
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
await page.goto('/workflow/sign/test-token');
|
||||
await page.goto("/workflow/sign/test-token");
|
||||
await page.waitForSelector('[data-testid="submit-signature-button"]');
|
||||
|
||||
await uploadCertFile(page, EXPIRED_P12);
|
||||
await page.getByTestId('cert-password-input').fill('testpass');
|
||||
await page.getByTestId("cert-password-input").fill("testpass");
|
||||
|
||||
const feedback = page.getByTestId('cert-validation-feedback');
|
||||
await expect(feedback).toContainText('Certificate has expired', { timeout: 5000 });
|
||||
const feedback = page.getByTestId("cert-validation-feedback");
|
||||
await expect(feedback).toContainText("Certificate has expired", { timeout: 5000 });
|
||||
});
|
||||
|
||||
// 4. Not-yet-valid certificate
|
||||
test('not-yet-valid cert shows "not yet valid" error', async ({ page }) => {
|
||||
await page.route('**/api/v1/workflow/participant/validate-certificate', (route) =>
|
||||
await page.route("**/api/v1/workflow/participant/validate-certificate", (route) =>
|
||||
route.fulfill({
|
||||
json: {
|
||||
valid: false,
|
||||
@@ -185,46 +181,46 @@ test.describe('Certificate Validation — ParticipantView', () => {
|
||||
notAfter: null,
|
||||
notBefore: null,
|
||||
selfSigned: false,
|
||||
error: 'Certificate is not yet valid (valid from: 2027-01-01 00:00:00 UTC)',
|
||||
error: "Certificate is not yet valid (valid from: 2027-01-01 00:00:00 UTC)",
|
||||
},
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
await page.goto('/workflow/sign/test-token');
|
||||
await page.goto("/workflow/sign/test-token");
|
||||
await page.waitForSelector('[data-testid="submit-signature-button"]');
|
||||
|
||||
await uploadCertFile(page, NOT_YET_VALID_P12);
|
||||
await page.getByTestId('cert-password-input').fill('testpass');
|
||||
await page.getByTestId("cert-password-input").fill("testpass");
|
||||
|
||||
const feedback = page.getByTestId('cert-validation-feedback');
|
||||
await expect(feedback).toContainText('not yet valid', { timeout: 5000 });
|
||||
const feedback = page.getByTestId("cert-validation-feedback");
|
||||
await expect(feedback).toContainText("not yet valid", { timeout: 5000 });
|
||||
});
|
||||
|
||||
// 5. Submit button disabled while validating
|
||||
test('submit button is disabled while validation is in flight', async ({ page }) => {
|
||||
test("submit button is disabled while validation is in flight", async ({ page }) => {
|
||||
// Slow response so we can assert the disabled state mid-flight
|
||||
await page.route('**/api/v1/workflow/participant/validate-certificate', async (route) => {
|
||||
await page.route("**/api/v1/workflow/participant/validate-certificate", async (route) => {
|
||||
await new Promise((r) => setTimeout(r, 1500));
|
||||
await route.fulfill({
|
||||
json: {
|
||||
valid: true,
|
||||
subjectName: 'Test Signer',
|
||||
notAfter: '2027-01-01T00:00:00Z',
|
||||
notBefore: '2025-01-01T00:00:00Z',
|
||||
subjectName: "Test Signer",
|
||||
notAfter: "2027-01-01T00:00:00Z",
|
||||
notBefore: "2025-01-01T00:00:00Z",
|
||||
selfSigned: true,
|
||||
error: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/workflow/sign/test-token');
|
||||
await page.goto("/workflow/sign/test-token");
|
||||
await page.waitForSelector('[data-testid="submit-signature-button"]');
|
||||
|
||||
await uploadCertFile(page, VALID_P12);
|
||||
await page.getByTestId('cert-password-input').fill('testpass');
|
||||
await page.getByTestId("cert-password-input").fill("testpass");
|
||||
|
||||
// Shortly after typing, validation is in flight — button must be disabled
|
||||
const submitBtn = page.getByTestId('submit-signature-button');
|
||||
const submitBtn = page.getByTestId("submit-signature-button");
|
||||
await expect(submitBtn).toBeDisabled({ timeout: 3000 });
|
||||
|
||||
// After validation completes the button should be re-enabled
|
||||
@@ -232,49 +228,49 @@ test.describe('Certificate Validation — ParticipantView', () => {
|
||||
});
|
||||
|
||||
// 6. SERVER type — no validation call made, button stays enabled
|
||||
test('SERVER cert type skips validation and keeps submit enabled', async ({ page }) => {
|
||||
test("SERVER cert type skips validation and keeps submit enabled", async ({ page }) => {
|
||||
let validateCalled = false;
|
||||
await page.route('**/api/v1/workflow/participant/validate-certificate', (route) => {
|
||||
await page.route("**/api/v1/workflow/participant/validate-certificate", (route) => {
|
||||
validateCalled = true;
|
||||
return route.fulfill({ json: { valid: true } });
|
||||
});
|
||||
|
||||
await page.goto('/workflow/sign/test-token');
|
||||
await page.goto("/workflow/sign/test-token");
|
||||
await page.waitForSelector('[data-testid="submit-signature-button"]');
|
||||
|
||||
await selectCertType(page, 'Server Certificate (if available)');
|
||||
await selectCertType(page, "Server Certificate (if available)");
|
||||
|
||||
// Wait longer than debounce to confirm no call is made
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
expect(validateCalled).toBe(false);
|
||||
await expect(page.getByTestId('submit-signature-button')).toBeEnabled();
|
||||
await expect(page.getByTestId("submit-signature-button")).toBeEnabled();
|
||||
});
|
||||
|
||||
// 7. Bonus — valid JKS keystore
|
||||
test('valid JKS keystore shows green feedback', async ({ page }) => {
|
||||
await page.route('**/api/v1/workflow/participant/validate-certificate', (route) =>
|
||||
test("valid JKS keystore shows green feedback", async ({ page }) => {
|
||||
await page.route("**/api/v1/workflow/participant/validate-certificate", (route) =>
|
||||
route.fulfill({
|
||||
json: {
|
||||
valid: true,
|
||||
subjectName: 'JKS Signer',
|
||||
notAfter: '2027-01-01T00:00:00Z',
|
||||
notBefore: '2025-01-01T00:00:00Z',
|
||||
subjectName: "JKS Signer",
|
||||
notAfter: "2027-01-01T00:00:00Z",
|
||||
notBefore: "2025-01-01T00:00:00Z",
|
||||
selfSigned: true,
|
||||
error: null,
|
||||
},
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
await page.goto('/workflow/sign/test-token');
|
||||
await page.goto("/workflow/sign/test-token");
|
||||
await page.waitForSelector('[data-testid="submit-signature-button"]');
|
||||
|
||||
await selectCertType(page, 'JKS Keystore');
|
||||
await selectCertType(page, "JKS Keystore");
|
||||
await uploadCertFile(page, VALID_JKS);
|
||||
await page.getByTestId('cert-password-input').fill('jkspass');
|
||||
await page.getByTestId("cert-password-input").fill("jkspass");
|
||||
|
||||
const feedback = page.getByTestId('cert-validation-feedback');
|
||||
await expect(feedback).toContainText('Certificate valid until', { timeout: 5000 });
|
||||
await expect(feedback).toContainText('JKS Signer');
|
||||
const feedback = page.getByTestId("cert-validation-feedback");
|
||||
await expect(feedback).toContainText("Certificate valid until", { timeout: 5000 });
|
||||
await expect(feedback).toContainText("JKS Signer");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user