refactor(slug): extract shared slugify util + scope adminPhotos category lookup (#525)

Folds all three follow-up items tracked in #525 into one commit:

1. Mirror PR #500's category scoping on adminPhotos.js. The admin
   upload route at adminPhotos.js:231 still accepted any category_id
   without event scoping — quietly less strict than the public v1
   API after #500 landed. Same one-liner fix (event_id OR is_global)
   with a matching 400 response shape so admin + v1 stay consistent.

2. Extract a shared slugify() in backend/src/utils/slug.js with the
   NFD-strip-combining-marks fix from #502, and route 5 callers
   through it:
     - adminEvents.js (event-name slug)
     - events.js      (event-create slug)
     - v1/events.js   (replaces local slugify helper)
     - adminArchives.js (archive→category slug)
   For pure-ASCII input the output is byte-identical to each old
   inline pipeline, so existing slugs in the DB keep round-tripping
   cleanly via lookup. Accented inputs now transliterate (Família
   → familia) instead of dropping the diacritic (Família → f-mlia).
   adminCategories.js stays with its own pipeline (underscores-as-
   word-chars semantics differ from the events-style transform —
   changing would silently shift wedding_party → wedding-party on
   new inserts). xmpGenerator.sanitizeKeyword stays unchanged for
   the same compat-cautious reason.

3. Cover the v1 upload happy path. Existing test only exercised the
   400-out-of-scope branch. Add two happy-path cases that stub
   sharp / generateThumbnail / storage.putFromFile and pin the
   response shape (id, category_id, type, etc.) plus the collage-
   slug → type='collage' flip. Temp file recreated in beforeEach
   because the handler unlinks it on success.

Tests:
- New slug.test.js: 22 cases pinning ASCII parity with the legacy
  pipeline (so the refactor is provably non-breaking for existing
  data) and the corrected accent handling across de/es/fr/nl/pt
  inputs, plus CJK and edge-case behaviour.
- events.category.test.js: 4 tests total (2 existing + 2 new happy
  path).
- galleryOgService.shareImage.test.js: 11 (3 added in #521 + 8 pre-
  existing) still pass.

37 tests pass across the three touched files.

Refs: #525, follows up #500 and #502
This commit is contained in:
Paul Nothaft
2026-05-18 23:50:35 +02:00
parent 4b4ecfdf71
commit e8c2212dad
8 changed files with 277 additions and 19 deletions
+93
View File
@@ -0,0 +1,93 @@
/**
* Tests for the shared slug util extracted in #525 from the inline
* pipelines in adminEvents.js, events.js, v1/events.js, adminArchives.js.
*
* Two contracts to pin:
* 1. ASCII inputs produce byte-identical output to the previous
* inline pipelines, so existing event/archive slugs in the DB
* keep resolving via the same lookup path after the refactor.
* 2. Accented characters (Portuguese, German, French, Spanish) are
* transliterated to their ASCII bases (Decoração → decoracao)
* instead of being dropped (Decoração → decorao) as the legacy
* pipelines did — same fix as #502 for category slugs.
*/
const { slugify } = require('../slug');
describe('slugify — ASCII parity with the legacy event-style pipeline', () => {
// Replays the exact transformation used by adminEvents.js before the
// refactor: lowercase → replace [^a-z0-9] with '-' → collapse → trim.
const legacy = (s) =>
String(s).toLowerCase()
.replace(/[^a-z0-9]/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
const samples = [
'Wedding 2026',
' Hello World ',
'birthday-party-42',
'event_with_underscores',
'CamelCase Event Name',
'',
'event.with.dots',
'event!@#$%^&*()chars',
'2026-06-12',
];
it.each(samples)('matches legacy output for ASCII input: %j', (input) => {
expect(slugify(input)).toBe(legacy(input));
});
});
describe('slugify — accented characters (the #502 fix, now shared)', () => {
// The legacy pipeline produced f-mlia for "Família" because the í
// got replaced with '-' rather than being NFD-normalised to 'i'.
// These tests pin the corrected behaviour across the locales the
// app already ships in (de, es, fr, nl, pt, ru).
it.each([
['Decoração', 'decoracao'],
['Família', 'familia'],
['Recepção', 'recepcao'],
['Über uns', 'uber-uns'],
['Niño', 'nino'],
['Fête de famille', 'fete-de-famille'],
['L\'Évènement', 'l-evenement'],
['Crème Brûlée', 'creme-brulee'],
])('transliterates %j → %j', (input, expected) => {
expect(slugify(input)).toBe(expected);
});
it('CJK and other scripts without NFD decompositions still strip cleanly', () => {
// NFD doesn't decompose Chinese characters to ASCII, so they get
// dropped by the [^a-z0-9]+ replace. Output is sensible if not
// perfect — the surrounding ASCII tokens survive.
expect(slugify('Photo 混合 Test')).toBe('photo-test');
// Pure-CJK names collapse to empty after trim — caller's job to
// handle (typically by appending a uniqueness suffix).
expect(slugify('婚礼')).toBe('');
});
});
describe('slugify — input edge cases', () => {
it('returns empty string for null / undefined / empty', () => {
expect(slugify(null)).toBe('');
expect(slugify(undefined)).toBe('');
expect(slugify('')).toBe('');
});
it('coerces non-string input to string before slugifying', () => {
expect(slugify(2026)).toBe('2026');
expect(slugify(true)).toBe('true');
});
it('collapses any run of non-alphanumeric chars into a single dash', () => {
expect(slugify('a!@#$%b')).toBe('a-b');
expect(slugify('a b\t\nc')).toBe('a-b-c');
});
it('trims leading and trailing dashes', () => {
expect(slugify('---hello---')).toBe('hello');
expect(slugify('!!!world!!!')).toBe('world');
});
});