web-player: share form limits between client and server and extract form parsers
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { ALBUM_EARLIEST_YEAR } from '$lib/validation';
|
||||
import { parseAlbumForm } from './albumForm';
|
||||
|
||||
function form(fields: Record<string, string>): FormData {
|
||||
const data = new FormData();
|
||||
for (const [key, value] of Object.entries(fields)) data.set(key, value);
|
||||
return data;
|
||||
}
|
||||
|
||||
describe('parseAlbumForm', () => {
|
||||
it('parses a valid form and trims values', () => {
|
||||
expect(
|
||||
parseAlbumForm(form({ title: ' Title ', description: ' d ', releaseYear: '2020' }))
|
||||
).toEqual({ body: { title: 'Title', description: 'd', releaseYear: 2020 } });
|
||||
});
|
||||
it('turns empty optional fields into null', () => {
|
||||
expect(parseAlbumForm(form({ title: 'T' }))).toEqual({
|
||||
body: { title: 'T', description: null, releaseYear: null }
|
||||
});
|
||||
});
|
||||
it('rejects empty or too long titles', () => {
|
||||
expect(parseAlbumForm(form({ title: ' ' }))).toHaveProperty('failMessage');
|
||||
expect(parseAlbumForm(form({ title: 'x'.repeat(201) }))).toHaveProperty('failMessage');
|
||||
});
|
||||
it('rejects too long descriptions', () => {
|
||||
expect(parseAlbumForm(form({ title: 'T', description: 'x'.repeat(257) }))).toHaveProperty(
|
||||
'failMessage'
|
||||
);
|
||||
});
|
||||
it('rejects invalid years', () => {
|
||||
for (const releaseYear of ['abc', '2020.5', String(ALBUM_EARLIEST_YEAR - 1), '9999']) {
|
||||
expect(parseAlbumForm(form({ title: 'T', releaseYear }))).toHaveProperty('failMessage');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { formString } from '$lib/server/api';
|
||||
import { ALBUM_EARLIEST_YEAR, LIMITS } from '$lib/validation';
|
||||
|
||||
interface AlbumFormBody {
|
||||
title: string;
|
||||
description: string | null;
|
||||
releaseYear: number | null;
|
||||
}
|
||||
|
||||
export function parseAlbumForm(form: FormData): { body: AlbumFormBody } | { failMessage: string } {
|
||||
const title = formString(form, 'title').trim();
|
||||
if (title === '' || title.length > LIMITS.albumTitle) {
|
||||
return { failMessage: `El título es obligatorio (máx. ${LIMITS.albumTitle} caracteres).` };
|
||||
}
|
||||
|
||||
const description = formString(form, 'description').trim();
|
||||
if (description.length > LIMITS.albumDescription) {
|
||||
return {
|
||||
failMessage: `La descripción no puede pasar de ${LIMITS.albumDescription} caracteres.`
|
||||
};
|
||||
}
|
||||
|
||||
const rawYear = formString(form, 'releaseYear').trim();
|
||||
const latestYear = new Date().getFullYear() + 1;
|
||||
let releaseYear: number | null = null;
|
||||
|
||||
if (rawYear !== '') {
|
||||
const parsed = Number(rawYear);
|
||||
if (!Number.isInteger(parsed) || parsed < ALBUM_EARLIEST_YEAR || parsed > latestYear) {
|
||||
return { failMessage: `El año debe estar entre ${ALBUM_EARLIEST_YEAR} y ${latestYear}.` };
|
||||
}
|
||||
releaseYear = parsed;
|
||||
}
|
||||
|
||||
return {
|
||||
body: {
|
||||
title,
|
||||
description: description === '' ? null : description,
|
||||
releaseYear
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { LIMITS } from '$lib/validation';
|
||||
import { parsePlaylistForm } from './playlistForm';
|
||||
|
||||
function form(fields: Record<string, string>): FormData {
|
||||
const data = new FormData();
|
||||
for (const [key, value] of Object.entries(fields)) data.set(key, value);
|
||||
return data;
|
||||
}
|
||||
|
||||
describe('parsePlaylistForm', () => {
|
||||
it('parses a valid form', () => {
|
||||
expect(
|
||||
parsePlaylistForm(form({ name: ' Mix ', description: ' d ', visibility: 'public' }))
|
||||
).toEqual({ body: { name: 'Mix', description: 'd', visibility: 'Public', cover: null } });
|
||||
});
|
||||
it('defaults to private', () => {
|
||||
const parsed = parsePlaylistForm(form({ name: 'Mix' }));
|
||||
expect(parsed).toMatchObject({ body: { visibility: 'Private', description: '' } });
|
||||
});
|
||||
it('rejects empty or too long names keeping the typed values', () => {
|
||||
expect(parsePlaylistForm(form({ name: ' ', description: 'd' }))).toMatchObject({
|
||||
failMessage: expect.any(String),
|
||||
description: 'd'
|
||||
});
|
||||
expect(parsePlaylistForm(form({ name: 'x'.repeat(LIMITS.playlistName + 1) }))).toHaveProperty(
|
||||
'failMessage'
|
||||
);
|
||||
});
|
||||
it('rejects too long descriptions', () => {
|
||||
const description = 'x'.repeat(LIMITS.playlistDescription + 1);
|
||||
expect(parsePlaylistForm(form({ name: 'Mix', description }))).toHaveProperty('failMessage');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { formFile, formString } from '$lib/server/api';
|
||||
import { LIMITS } from '$lib/validation';
|
||||
|
||||
interface PlaylistFormBody {
|
||||
name: string;
|
||||
description: string;
|
||||
visibility: 'Public' | 'Private';
|
||||
cover: File | null;
|
||||
}
|
||||
|
||||
export type PlaylistFormFailure = { failMessage: string; name: string; description: string };
|
||||
|
||||
export function parsePlaylistForm(
|
||||
form: FormData
|
||||
): { body: PlaylistFormBody } | PlaylistFormFailure {
|
||||
const name = formString(form, 'name').trim();
|
||||
const description = formString(form, 'description').trim();
|
||||
|
||||
if (name === '' || name.length > LIMITS.playlistName) {
|
||||
return {
|
||||
failMessage: `El nombre es obligatorio (máx. ${LIMITS.playlistName} caracteres).`,
|
||||
name,
|
||||
description
|
||||
};
|
||||
}
|
||||
if (description.length > LIMITS.playlistDescription) {
|
||||
return {
|
||||
failMessage: `La descripción no puede pasar de ${LIMITS.playlistDescription} caracteres.`,
|
||||
name,
|
||||
description
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
body: {
|
||||
name,
|
||||
description,
|
||||
visibility: formString(form, 'visibility') === 'public' ? 'Public' : 'Private',
|
||||
cover: formFile(form, 'cover')
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { LIMITS } from '$lib/validation';
|
||||
import { parseTrackUploadForm } from './trackUploadForm';
|
||||
|
||||
function form(fields: Record<string, string | string[] | File>): FormData {
|
||||
const data = new FormData();
|
||||
for (const [key, value] of Object.entries(fields)) {
|
||||
if (Array.isArray(value)) value.forEach((item) => data.append(key, item));
|
||||
else data.set(key, value);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
const audio = new File(['a'], 'a.mp3');
|
||||
const cover = new File(['c'], 'c.png');
|
||||
|
||||
describe('parseTrackUploadForm', () => {
|
||||
it('parses a valid form and deduplicates tags', () => {
|
||||
const parsed = parseTrackUploadForm(
|
||||
form({ title: ' Song ', audio, cover, tags: ['rock', 'rock', 'pop'], isExplicit: 'on' })
|
||||
);
|
||||
expect(parsed).toMatchObject({
|
||||
body: { title: 'Song', tags: ['rock', 'pop'], isExplicit: true }
|
||||
});
|
||||
});
|
||||
it('validates each field', () => {
|
||||
const ok = { title: 'Song', audio, cover, tags: ['rock'] };
|
||||
expect(parseTrackUploadForm(form({ ...ok, title: '' }))).toHaveProperty('failMessage');
|
||||
expect(
|
||||
parseTrackUploadForm(form({ ...ok, title: 'x'.repeat(LIMITS.trackTitle + 1) }))
|
||||
).toHaveProperty('failMessage');
|
||||
expect(parseTrackUploadForm(form({ ...ok, audio: '' }))).toHaveProperty('failMessage');
|
||||
expect(parseTrackUploadForm(form({ ...ok, cover: '' }))).toHaveProperty('failMessage');
|
||||
expect(parseTrackUploadForm(form({ ...ok, tags: [] }))).toHaveProperty('failMessage');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { formFile, formString } from '$lib/server/api';
|
||||
import { LIMITS } from '$lib/validation';
|
||||
|
||||
interface TrackUploadBody {
|
||||
title: string;
|
||||
audio: File;
|
||||
cover: File;
|
||||
tags: string[];
|
||||
isExplicit: boolean;
|
||||
}
|
||||
|
||||
export function parseTrackUploadForm(
|
||||
form: FormData
|
||||
): { body: TrackUploadBody } | { failMessage: string } {
|
||||
const title = formString(form, 'title').trim();
|
||||
if (title === '' || title.length > LIMITS.trackTitle) {
|
||||
return { failMessage: `El título es obligatorio (máx. ${LIMITS.trackTitle} caracteres).` };
|
||||
}
|
||||
|
||||
const audio = formFile(form, 'audio');
|
||||
if (!audio) return { failMessage: 'Selecciona un archivo de audio.' };
|
||||
|
||||
const cover = formFile(form, 'cover');
|
||||
if (!cover) return { failMessage: 'Selecciona una portada.' };
|
||||
|
||||
const tags = [...new Set(form.getAll('tags').map(String))];
|
||||
if (tags.length === 0) return { failMessage: 'Elige al menos un género.' };
|
||||
|
||||
return { body: { title, audio, cover, tags, isExplicit: form.get('isExplicit') === 'on' } };
|
||||
}
|
||||
Reference in New Issue
Block a user