password_manager/tests/lib/models/schema.test.js

245 lines
7.4 KiB
JavaScript

import { describe, it, expect } from 'vitest'
import {
generateId,
createEntry,
updateEntry,
createGroup,
validateEntry,
validateGroup,
createTrashGroup,
isTrashGroup,
TRASH_GROUP_ID,
TRASH_GROUP_NAME,
TRASH_GROUP_COLOR,
} from '../../../src/lib/models/schema.js'
describe('generateId', () => {
it('should generate unique IDs', () => {
const ids = new Set()
for (let i = 0; i < 100; i++) {
ids.add(generateId())
}
expect(ids.size).toBe(100)
})
it('should produce sortable IDs (timestamp-based)', () => {
const id1 = generateId()
// Small delay to ensure different timestamp
// IDs are base36 timestamps + random, so they should be lexicographically ordered
expect(id1).toMatch(/^[a-z0-9]+_[0-9a-f]{8}$/)
})
it('should contain underscore separator', () => {
const id = generateId()
const parts = id.split('_')
expect(parts).toHaveLength(2)
expect(parts[1]).toHaveLength(8)
})
})
describe('createEntry', () => {
it('should create an entry with required fields', () => {
const entry = createEntry({
title: 'GitHub',
encryptedPassword: 'encrypted-blob',
})
expect(entry.id).toBeDefined()
expect(entry.title).toBe('GitHub')
expect(entry.encryptedPassword).toBe('encrypted-blob')
expect(entry.createdAt).toBeDefined()
expect(entry.updatedAt).toBe(entry.createdAt)
})
it('should trim title and optional fields', () => {
const entry = createEntry({
title: ' GitHub ',
username: ' user@test.com ',
url: ' https://github.com ',
notes: ' some notes ',
encryptedPassword: 'encrypted-blob',
})
expect(entry.title).toBe('GitHub')
expect(entry.username).toBe('user@test.com')
expect(entry.url).toBe('https://github.com')
expect(entry.notes).toBe('some notes')
})
it('should set defaults for optional fields', () => {
const entry = createEntry({
title: 'Test',
encryptedPassword: 'encrypted-blob',
})
expect(entry.username).toBe('')
expect(entry.url).toBe('')
expect(entry.notes).toBe('')
expect(entry.groupId).toBe('')
expect(entry.tags).toEqual([])
})
it('should accept groupId and tags', () => {
const entry = createEntry({
title: 'Test',
encryptedPassword: 'encrypted-blob',
groupId: 'group-123',
tags: ['work', 'important'],
})
expect(entry.groupId).toBe('group-123')
expect(entry.tags).toEqual(['work', 'important'])
})
})
describe('updateEntry', () => {
it('should preserve id and createdAt', () => {
const existing = createEntry({
title: 'GitHub',
encryptedPassword: 'old-encrypted',
})
const updated = updateEntry(existing, { title: 'GitHub Pro' })
expect(updated.id).toBe(existing.id)
expect(updated.createdAt).toBe(existing.createdAt)
// updatedAt should be >= createdAt (ISO strings are lexicographically comparable)
expect(updated.updatedAt >= existing.updatedAt).toBe(true)
})
it('should update only specified fields', () => {
const existing = createEntry({
title: 'GitHub',
username: 'user',
encryptedPassword: 'old-encrypted',
url: 'https://github.com',
notes: 'old notes',
})
const updated = updateEntry(existing, { title: 'GitHub Pro' })
expect(updated.title).toBe('GitHub Pro')
expect(updated.username).toBe('user')
expect(updated.encryptedPassword).toBe('old-encrypted')
expect(updated.url).toBe('https://github.com')
expect(updated.notes).toBe('old notes')
})
it('should trim updated string fields', () => {
const existing = createEntry({
title: 'GitHub',
encryptedPassword: 'encrypted',
})
const updated = updateEntry(existing, { title: ' GitHub Pro ', notes: ' new notes ' })
expect(updated.title).toBe('GitHub Pro')
expect(updated.notes).toBe('new notes')
})
it('should handle undefined fields (no-op)', () => {
const existing = createEntry({
title: 'GitHub',
encryptedPassword: 'encrypted',
})
const updated = updateEntry(existing, { title: undefined })
expect(updated.title).toBe('GitHub')
})
})
describe('createGroup', () => {
it('should create a group with name and color', () => {
const group = createGroup('Work', '#ff0000')
expect(group.id).toBeDefined()
expect(group.name).toBe('Work')
expect(group.color).toBe('#ff0000')
expect(group.createdAt).toBeDefined()
})
it('should assign a random color when none provided', () => {
const group = createGroup('Personal')
expect(group.color).toMatch(/^#[0-9a-f]{6}$/)
})
it('should trim group name', () => {
const group = createGroup(' Work ')
expect(group.name).toBe('Work')
})
})
describe('validateEntry', () => {
it('should pass with valid data', () => {
const result = validateEntry({ title: 'GitHub', encryptedPassword: 'encrypted' })
expect(result.valid).toBe(true)
expect(result.errors).toEqual([])
})
it('should fail with empty title', () => {
const result = validateEntry({ title: '', encryptedPassword: 'encrypted' })
expect(result.valid).toBe(false)
expect(result.errors).toContain('Title is required')
})
it('should fail with whitespace-only title', () => {
const result = validateEntry({ title: ' ', encryptedPassword: 'encrypted' })
expect(result.valid).toBe(false)
expect(result.errors).toContain('Title is required')
})
it('should fail with missing encryptedPassword', () => {
const result = validateEntry({ title: 'GitHub' })
expect(result.valid).toBe(false)
expect(result.errors).toContain('Password is required')
})
it('should report multiple errors', () => {
const result = validateEntry({ title: '' })
expect(result.valid).toBe(false)
expect(result.errors.length).toBe(2)
})
})
describe('validateGroup', () => {
it('should pass with valid name', () => {
const result = validateGroup('Work')
expect(result.valid).toBe(true)
expect(result.errors).toEqual([])
})
it('should fail with empty name', () => {
const result = validateGroup('')
expect(result.valid).toBe(false)
expect(result.errors).toContain('Group name is required')
})
it('should fail with whitespace-only name', () => {
const result = validateGroup(' ')
expect(result.valid).toBe(false)
expect(result.errors).toContain('Group name is required')
})
it('should fail with name over 50 characters', () => {
const result = validateGroup('a'.repeat(51))
expect(result.valid).toBe(false)
expect(result.errors).toContain('Group name must be 50 characters or less')
})
it('should pass with exactly 50 characters', () => {
const result = validateGroup('a'.repeat(50))
expect(result.valid).toBe(true)
})
})
describe('createTrashGroup', () => {
it('should return a group with fixed trash ID', () => {
const group = createTrashGroup()
expect(group.id).toBe(TRASH_GROUP_ID)
expect(group.name).toBe(TRASH_GROUP_NAME)
expect(group.color).toBe(TRASH_GROUP_COLOR)
expect(group.createdAt).toBeDefined()
})
})
describe('isTrashGroup', () => {
it('should return true for trash group ID', () => {
expect(isTrashGroup(TRASH_GROUP_ID)).toBe(true)
})
it('should return false for other IDs', () => {
expect(isTrashGroup('group-123')).toBe(false)
expect(isTrashGroup('')).toBe(false)
expect(isTrashGroup('__trash')).toBe(false)
expect(isTrashGroup('__trash__extra')).toBe(false)
})
})