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.
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user