import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { flushPromises } from '@vue/test-utils' import { mapWinterUrl } from '../../src/app/winterUrl' import { clone, rosterFormSchemaFixture, rosterRecordFixture } from '../fixtures/typed' import { API, mountApp, resetState } from '../helpers' const ID = 'acme.demo.widgets' const LIST = '/acme/demo/widgets' describe('mapWinterUrl', () => { it('maps an empty, null or missing URL to the list', () => { expect(mapWinterUrl('', ID)).toBe(LIST) expect(mapWinterUrl(null, ID)).toBe(LIST) expect(mapWinterUrl(undefined, ID)).toBe(LIST) expect(mapWinterUrl(' ', ID)).toBe(LIST) }) it('maps the controller root and create', () => { expect(mapWinterUrl('acme/demo/widgets', ID)).toBe(LIST) expect(mapWinterUrl('/acme/demo/widgets/', ID)).toBe(LIST) expect(mapWinterUrl('acme/demo/widgets/create', ID)).toBe(`${LIST}/create`) }) it('substitutes :id in update/:id and accepts a literal numeric id', () => { expect(mapWinterUrl('acme/demo/widgets/update/:id', ID, 42)).toBe(`${LIST}/42`) expect(mapWinterUrl('acme/demo/widgets/update/:id', ID, '7')).toBe(`${LIST}/7`) expect(mapWinterUrl('acme/demo/widgets/update/9', ID)).toBe(`${LIST}/9`) }) it('substitutes :id in preview/:id and falls back to the list without a usable id (D-11)', () => { expect(mapWinterUrl('acme/demo/widgets/preview/:id', ID, 42)).toBe(`${LIST}/42/preview`) expect(mapWinterUrl('acme/demo/widgets/preview/9', ID)).toBe(`${LIST}/9/preview`) expect(mapWinterUrl('acme/demo/widgets/preview/:id', ID)).toBe(LIST) expect(mapWinterUrl('acme/demo/widgets/preview/abc', ID)).toBe(LIST) expect(mapWinterUrl('acme/demo/gadgets/preview/:id', ID, 1)).toBe(LIST) }) it('falls back to the list when update has no usable id', () => { expect(mapWinterUrl('acme/demo/widgets/update/:id', ID)).toBe(LIST) expect(mapWinterUrl('acme/demo/widgets/update/:id', ID, null)).toBe(LIST) expect(mapWinterUrl('acme/demo/widgets/update/:id', ID, 'abc')).toBe(LIST) expect(mapWinterUrl('acme/demo/widgets/update/1e3', ID)).toBe(LIST) }) it('never leaves the current controller for a foreign or absolute URL (T-10-20)', () => { expect(mapWinterUrl('acme/demo/gadgets/update/:id', ID, 1)).toBe(LIST) expect(mapWinterUrl('https://evil.example/acme/demo/widgets/create', ID)).toBe(LIST) expect(mapWinterUrl('//evil.example/x', ID)).toBe(LIST) expect(mapWinterUrl('javascript:alert(1)', ID)).toBe(LIST) }) it('ignores the query and hash, matches the owner case-insensitively and rejects unknown actions', () => { expect(mapWinterUrl('acme/demo/widgets/create?x=1#top', ID)).toBe(`${LIST}/create`) expect(mapWinterUrl('Acme/Demo/Widgets/update/:id', ID, 3)).toBe(`${LIST}/3`) expect(mapWinterUrl('acme/demo/widgets/history/3', ID)).toBe(LIST) expect(mapWinterUrl('acme/demo/widgets/update/3/extra', ID)).toBe(LIST) expect(mapWinterUrl('acme/demo/widgets/CREATE', ID)).toBe(LIST) }) it('answers the root for a malformed controller id', () => { expect(mapWinterUrl('acme/demo/widgets', 'acme.demo')).toBe('/') expect(mapWinterUrl('acme/demo/widgets', 'acme.demo.wid gets')).toBe('/') }) }) describe('preview URLs (UI-SPEC S3, D-11, T-12.1-16)', () => { const PEOPLE = `${API}/acme/roster/people` beforeEach(() => { resetState() }) afterEach(() => { document.body.innerHTML = '' }) it('backstop: mapWinterUrl maps preview/:id to the preview route, and opening the preview route of a form without a preview replaces it with the record route', async () => { const id = 'acme.roster.people' expect(mapWinterUrl('acme/roster/people/preview/:id', id, 12)).toBe('/acme/roster/people/12/preview') expect(mapWinterUrl('acme/roster/people/preview/12', id)).toBe('/acme/roster/people/12/preview') expect(mapWinterUrl('/Acme/Roster/People/preview/:id/', id, '12')).toBe('/acme/roster/people/12/preview') const plain = clone(rosterFormSchemaFixture) delete plain.data.preview const { wrapper, router, calls } = await mountApp('/acme/roster/people/1/preview', { [`GET ${PEOPLE}/schema/form`]: { body: plain }, [`GET ${PEOPLE}/1`]: { body: rosterRecordFixture }, }) await flushPromises() expect(router.currentRoute.value.name).toBe('record') expect(router.currentRoute.value.path).toBe('/acme/roster/people/1') // Replaced, not pushed: going back does not return to the preview URL. expect(wrapper.find('[data-preview]').exists()).toBe(false) expect(calls.some((request) => new URL(request.url).pathname.includes('/partials/'))).toBe(false) wrapper.unmount() // With a preview block the same URL stays on the preview route. const kept = await mountApp('/acme/roster/people/1/preview', { [`GET ${PEOPLE}/schema/form`]: { body: rosterFormSchemaFixture }, [`GET ${PEOPLE}/1`]: { body: rosterRecordFixture }, [`GET ${PEOPLE}/partials/status`]: { body: { data: { nodes: [] }, meta: {} } }, }) await flushPromises() expect(kept.router.currentRoute.value.name).toBe('preview') expect(kept.wrapper.find('[data-preview]').exists()).toBe(true) kept.wrapper.unmount() }) it('rejects a foreign controller, a foreign plugin and a foreign vendor', () => { expect(mapWinterUrl('acme/demo/gadgets/preview/:id', ID, 1)).toBe(LIST) expect(mapWinterUrl('acme/other/widgets/preview/:id', ID, 1)).toBe(LIST) expect(mapWinterUrl('evil/demo/widgets/preview/:id', ID, 1)).toBe(LIST) expect(mapWinterUrl('https://evil.example/acme/demo/widgets/preview/1', ID)).toBe(LIST) expect(mapWinterUrl('//evil.example/acme/demo/widgets/preview/1', ID)).toBe(LIST) }) it.each([['abc'], ['1e3'], ['-1'], ['1.5'], ['0x10'], [' 1'], ['1/..'], ['../1'], ['%31'], ['']])('rejects the non-numeric preview id %j', (bad) => { expect(mapWinterUrl(`acme/demo/widgets/preview/${bad}`, ID)).toBe(LIST) expect(mapWinterUrl('acme/demo/widgets/preview/:id', ID, bad)).toBe(LIST) }) it('accepts no extra segment, no other case of the action and no missing id', () => { expect(mapWinterUrl('acme/demo/widgets/preview', ID, 1)).toBe(LIST) expect(mapWinterUrl('acme/demo/widgets/preview/1/edit', ID)).toBe(LIST) expect(mapWinterUrl('acme/demo/widgets/Preview/1', ID)).toBe(LIST) expect(mapWinterUrl('acme/demo/widgets/preview/:id', ID, undefined)).toBe(LIST) expect(mapWinterUrl('acme/demo/widgets/preview/1?next=//evil.example#x', ID)).toBe(`${LIST}/1/preview`) }) })