From 4759f1f6105f6932524b2c8f71a12b8abea4cc41 Mon Sep 17 00:00:00 2001 From: cesnimda Date: Sun, 19 Jul 2026 16:47:32 +0200 Subject: [PATCH] fix(cv): support application variant deep links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Application Workspace CV section linked to /cv-builder?variant={id}. That route does not exist: the builder is mounted at /career/builder/:id and reads the variant from the path, not a query string. The button dead-ended. Corrected the href. No loading logic was added — the editor already loads the variant by id and already has a safe error state, and ownership is already enforced server-side, where CvVariantService scopes every read to the owner and the controller returns 404. Added tests for the deep-link entry point, which had none: the variant loads from the route, a missing variant shows the error state rather than an empty editor, and another user's variant is refused identically. The asset test now asserts the exact href, so a route that the router does not serve fails the build instead of shipping. 118 frontend tests and the production build pass. Co-Authored-By: Claude Opus 4.8 --- .../src/application-assets.test.tsx | 4 +- .../src/components/ApplicationAssets.tsx | 4 +- .../src/cv-builder-deep-link.test.tsx | 93 +++++++++++++++++++ 3 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 job-tracker-ui/src/cv-builder-deep-link.test.tsx diff --git a/job-tracker-ui/src/application-assets.test.tsx b/job-tracker-ui/src/application-assets.test.tsx index c0d8322..9f98b48 100644 --- a/job-tracker-ui/src/application-assets.test.tsx +++ b/job-tracker-ui/src/application-assets.test.tsx @@ -74,8 +74,10 @@ test("cv section shows the attached variant and the ones available to attach", a expect(await screen.findByText("Backend CV")).toBeInTheDocument(); expect(screen.getByText(/Theme nordic · version 4/)).toBeInTheDocument(); + // Must be the builder's real route (/career/builder/:id). An href the router does not serve + // silently dead-ends the user, which is exactly what shipped before this test existed. expect(screen.getByRole("link", { name: /Edit, preview and export/i })).toHaveAttribute( - "href", "/cv-builder?variant=3"); + "href", "/career/builder/3"); }); test("attaching a different variant only re-points the application", async () => { diff --git a/job-tracker-ui/src/components/ApplicationAssets.tsx b/job-tracker-ui/src/components/ApplicationAssets.tsx index 295fb80..c12a40e 100644 --- a/job-tracker-ui/src/components/ApplicationAssets.tsx +++ b/job-tracker-ui/src/components/ApplicationAssets.tsx @@ -148,7 +148,9 @@ export function ApplicationCvSection({ jobId }: { jobId: number }) { size="small" variant="outlined" endIcon={} - href={`/cv-builder?variant=${data.attachedVariantId}`} + // The builder's real route. It loads the variant from :id and enforces ownership + // server-side, so there is no second CV loading path here. + href={`/career/builder/${data.attachedVariantId}`} > Edit, preview and export diff --git a/job-tracker-ui/src/cv-builder-deep-link.test.tsx b/job-tracker-ui/src/cv-builder-deep-link.test.tsx new file mode 100644 index 0000000..90d6ac0 --- /dev/null +++ b/job-tracker-ui/src/cv-builder-deep-link.test.tsx @@ -0,0 +1,93 @@ +import React from 'react'; +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; + +import CvBuilderEditor from './views/CvBuilderEditor'; +import { I18nProvider } from './i18n/I18nProvider'; +import { ToastProvider } from './toast'; +import { api } from './api'; +import { emptyCvVariantSettings } from './cvBuilder'; + +// The Application Workspace CV section deep-links straight into the builder at +// /career/builder/:id. These tests cover that entry point: the variant loads, and a variant the +// server refuses (missing, or someone else's) lands on a safe error state rather than a blank editor. +jest.mock('./api', () => ({ + api: { + get: jest.fn(), + post: jest.fn(), + put: jest.fn(), + delete: jest.fn(), + interceptors: { request: { use: jest.fn() }, response: { use: jest.fn() } }, + }, + getApiErrorMessage: (_e: any, fallback?: string) => fallback || 'Request failed.', +})); + +const mockedApi = api as jest.Mocked; + +const variant = { + id: 3, + name: 'Backend CV', + settings: emptyCvVariantSettings(), + isPublic: false, + publicSlug: 'abc123', + version: 4, + jobApplicationId: 7, + updatedAtUtc: '2026-07-19T10:00:00Z', +}; + +function renderAt(id: number) { + return render( + + + + + } /> + + + + , + ); +} + +beforeEach(() => { + jest.clearAllMocks(); + mockedApi.post.mockResolvedValue({ data: { themeId: 'nordic', html: '

cv

', suggestedFileName: 'cv.pdf' } } as any); +}); + +function routeGet(onVariant: () => Promise) { + mockedApi.get.mockImplementation((url: string) => { + if (url === '/cv/variants/3') return onVariant(); + if (url === '/cv/themes') return Promise.resolve({ data: [] } as any); + if (url === '/cv/outline') return Promise.resolve({ data: { sections: [] } } as any); + return Promise.resolve({ data: [] } as any); + }); +} + +test('deep link loads the exact variant named in the route', async () => { + routeGet(() => Promise.resolve({ data: variant } as any)); + + renderAt(3); + + expect(await screen.findByDisplayValue('Backend CV')).toBeInTheDocument(); + expect(mockedApi.get).toHaveBeenCalledWith('/cv/variants/3'); +}); + +test('a variant that does not exist shows a safe error instead of an empty editor', async () => { + routeGet(() => Promise.reject(new Error('404'))); + + renderAt(3); + + expect(await screen.findByText(/Could not open this CV/i)).toBeInTheDocument(); + expect(screen.queryByDisplayValue('Backend CV')).not.toBeInTheDocument(); +}); + +test("another user's variant is refused by the server and never rendered", async () => { + // Ownership is enforced server-side: CvVariantService scopes every read to the owner and the + // controller returns 404, so the client only ever sees the same safe failure. + routeGet(() => Promise.reject({ response: { status: 404 } })); + + renderAt(3); + + expect(await screen.findByText(/Could not open this CV/i)).toBeInTheDocument(); +});