Files
portal/src/lib/server/system-name.spec.ts
T
NeilClack 8412dfb69c Portal: System Name PUT endpoint and license-validation status
Add PUT /api/v2/licenses/[licenseKey]/system-name (200/400 invalid_name/
404 unknown_license/409 name_taken, idempotent) and extend POST
/api/v2/license with an optional systemName and additive systemNameStatus
(ok/unchanged/conflict/invalid). Validation always succeeds; an omitted
field never changes or clears a name and empty/whitespace is invalid.
Adds server and helper tests covering trimming, bounds, case-insensitive
uniqueness, cross-account reuse, 404, and idempotency.
2026-09-29 09:51:54 -05:00

116 lines
3.2 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
vi.mock('$lib/db', () => ({
prisma: {
licenseKey: { findFirst: vi.fn(), update: vi.fn() }
}
}));
import { prisma } from '$lib/db';
import { applySystemName, normalizeSystemName, SYSTEM_NAME_MAX_LENGTH } from './system-name';
const keyFindFirst = prisma.licenseKey.findFirst as ReturnType<typeof vi.fn>;
const keyUpdate = prisma.licenseKey.update as ReturnType<typeof vi.fn>;
beforeEach(() => {
vi.clearAllMocks();
});
describe('normalizeSystemName', () => {
it('trims and returns a case-folded copy', () => {
expect(normalizeSystemName(' Living Room ')).toEqual({
ok: true,
name: 'Living Room',
lower: 'living room'
});
});
it('accepts the length bounds', () => {
expect(normalizeSystemName('a'.repeat(SYSTEM_NAME_MAX_LENGTH)).ok).toBe(true);
expect(normalizeSystemName('a')).toEqual({ ok: true, name: 'a', lower: 'a' });
});
it('rejects empty, whitespace, and out-of-range values', () => {
expect(normalizeSystemName('')).toEqual({ ok: false });
expect(normalizeSystemName(' ')).toEqual({ ok: false });
expect(normalizeSystemName('a'.repeat(SYSTEM_NAME_MAX_LENGTH + 1))).toEqual({ ok: false });
});
it('rejects non-strings', () => {
expect(normalizeSystemName(undefined)).toEqual({ ok: false });
expect(normalizeSystemName(null)).toEqual({ ok: false });
expect(normalizeSystemName(42)).toEqual({ ok: false });
});
});
describe('applySystemName', () => {
it('reports unchanged without writing when identical', async () => {
const status = await applySystemName(
{ id: 'k1', userId: 'u1', systemName: 'Living Room' },
'Living Room',
'living room'
);
expect(status).toBe('unchanged');
expect(keyFindFirst).not.toHaveBeenCalled();
expect(keyUpdate).not.toHaveBeenCalled();
});
it('reports conflict when another license on the account holds the name', async () => {
keyFindFirst.mockResolvedValue({ id: 'other' });
const status = await applySystemName(
{ id: 'k1', userId: 'u1', systemName: null },
'Den',
'den'
);
expect(status).toBe('conflict');
expect(keyUpdate).not.toHaveBeenCalled();
});
it('writes the name and its case-folded copy when free', async () => {
keyFindFirst.mockResolvedValue(null);
keyUpdate.mockResolvedValue({});
const status = await applySystemName(
{ id: 'k1', userId: 'u1', systemName: null },
'Den',
'den'
);
expect(status).toBe('ok');
expect(keyUpdate).toHaveBeenCalledWith({
where: { id: 'k1' },
data: { systemName: 'Den', systemNameLower: 'den' }
});
});
it('skips the uniqueness check for a license with no account', async () => {
keyUpdate.mockResolvedValue({});
const status = await applySystemName(
{ id: 'k1', userId: null, systemName: null },
'Den',
'den'
);
expect(status).toBe('ok');
expect(keyFindFirst).not.toHaveBeenCalled();
expect(keyUpdate).toHaveBeenCalled();
});
it('maps a unique-constraint violation to conflict', async () => {
keyFindFirst.mockResolvedValue(null);
keyUpdate.mockRejectedValue({ code: 'P2002' });
const status = await applySystemName(
{ id: 'k1', userId: 'u1', systemName: null },
'Den',
'den'
);
expect(status).toBe('conflict');
});
});