Files
shopdb-flask/plugins/tools/frontend/sharelink.spec.js
cproudlock 5e7e39a24c Share a code by link instead of by instructions
Sending somebody a label meant sending a screenshot and a list of settings to
retype, and they would get one of them wrong. Copy share link hands them a URL
that reopens the generator with the content, the label, the copy count and
every layout value that was actually changed.

The whole payload rides in the fragment. A fragment is never transmitted, so it
does not reach Flask, it is not in the IIS log, and the proxy between them never
sees it - which matters because the content is usually an asset number or a
serial, and those have no business scattered through logs. It is NOT secrecy:
the link sits in a chat message and in browser history, and anyone holding it
can read the content. Secure Send exists for the case where that matters, and
the note under the box says so.

Only values that differ from a first-time visitor's defaults are carried, so
the link stays short enough to paste into a chat box; a shared link then wins
over the recipient's own saved settings, because somebody following one is
asking to see THAT code rather than their own last one. An unknown key is
ignored rather than refused, so a link made by a newer version still opens on
an older one. The picture is never carried - a data URL runs to megabytes.

Values are coerced back to the type the field holds. Everything arrives from a
URL as a string, and a nudge of 0 plus '2' is '02'.

Proven end to end: a link made in one browser, opened in a clean context with
no saved settings, rendering the same code.
2026-08-28 11:28:06 -04:00

108 lines
3.8 KiB
JavaScript

// The share link has to survive a chat client, a recipient whose saved settings
// differ from the sender's, and a version of the page older than the link.
import { describe, it, expect } from 'vitest'
import { buildFragment, readFragment, coerce } from './sharelink'
const DEFAULTS = {
codetype: 'qr', errorcorrection: 'M', preset: 'zebra1x05',
labelwidth: 1.0, labelheight: 0.5, dpi: 203, layout: 'side',
}
function state (extra = {}) {
return { source: 'single', content: 'WJ-3204', ...DEFAULTS, ...extra }
}
describe('buildFragment', () => {
it('carries what was typed', () => {
const params = new URLSearchParams(buildFragment(state(), DEFAULTS).slice(1))
expect(params.get('c')).toBe('WJ-3204')
expect(params.get('s')).toBe('single')
})
it('leaves out layout values the sender never changed', () => {
const fragment = buildFragment(state(), DEFAULTS)
expect(fragment).not.toContain('labelwidth')
expect(fragment).not.toContain('dpi')
})
it('carries a layout value the sender did change', () => {
const fragment = buildFragment(state({ labelwidth: 2.0, dpi: 300 }), DEFAULTS)
const params = new URLSearchParams(fragment.slice(1))
expect(params.get('labelwidth')).toBe('2')
expect(params.get('dpi')).toBe('300')
})
it('says nothing about one copy, which is what everybody has', () => {
expect(buildFragment(state({ copies: 1 }), DEFAULTS)).not.toContain('n=')
expect(buildFragment(state({ copies: 4 }), DEFAULTS)).toContain('n=4')
})
it('is empty when there is nothing to share', () => {
// So the caller can disable its button rather than emit a bare '#'.
expect(buildFragment({}, DEFAULTS)).toBe('')
})
it('never carries the picture', () => {
// A data URL runs to megabytes; the link would not survive a chat box.
const fragment = buildFragment(state({ pictureUrl: 'data:image/png;base64,AAAA' }), DEFAULTS)
expect(fragment).not.toContain('data:image')
expect(fragment).not.toContain('pictureUrl')
})
})
describe('readFragment', () => {
it('reads back what buildFragment wrote', () => {
const sent = state({ content: 'Bay 310 / press 2', label: 'Press', copies: 3,
labelwidth: 2.0, layout: 'stack' })
const back = readFragment(buildFragment(sent, DEFAULTS))
expect(back.content).toBe('Bay 310 / press 2')
expect(back.label).toBe('Press')
expect(back.copies).toBe('3')
expect(back.labelwidth).toBe('2')
expect(back.layout).toBe('stack')
})
it('survives the characters people actually put in a label', () => {
const awkward = 'CNC #4 & 5 - 50% duty, "spare"'
const back = readFragment(buildFragment(state({ content: awkward }), DEFAULTS))
expect(back.content).toBe(awkward)
})
it('ignores a key it does not know', () => {
// A link made by a newer version must still open here, minus the parts
// this version cannot use.
const back = readFragment('#c=X&somethingnew=1')
expect(back.content).toBe('X')
expect(back.somethingnew).toBeUndefined()
})
it('is empty for an empty or absent fragment', () => {
expect(readFragment('')).toEqual({})
expect(readFragment('#')).toEqual({})
expect(readFragment(undefined)).toEqual({})
})
})
describe('coerce', () => {
it('keeps a number a number', () => {
// '0' + '2' is '02'; 0 + 2 is 2. The nudge fields are arithmetic.
expect(coerce('2', 0)).toBe(2)
expect(coerce('0.4286', 0.5)).toBeCloseTo(0.4286)
})
it('falls back to the current value when the link is nonsense', () => {
expect(coerce('not-a-number', 203)).toBe(203)
})
it('reads a boolean back as a boolean', () => {
expect(coerce('true', false)).toBe(true)
expect(coerce('false', true)).toBe(false)
})
it('leaves a string alone', () => {
expect(coerce('stack', 'side')).toBe('stack')
})
})