245 lines
7.4 KiB
JavaScript
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)
|
|
})
|
|
})
|