test(phase-02): add Nyquist validation tests — fill SEC-05, AUTH-08, SEC-03 and frontend gaps

8 test files, 60 new tests (14 backend + 46 frontend). All green.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curo1305
2026-05-31 12:04:21 +02:00
parent 6c79f92d70
commit d98e3ab7a1
8 changed files with 1381 additions and 0 deletions
@@ -0,0 +1,139 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
// Mock api/client.js
vi.mock('../../../api/client.js', () => ({
adminListUsers: vi.fn(),
adminUpdateAiConfig: vi.fn(),
}))
import AdminAiConfigTab from '../AdminAiConfigTab.vue'
import * as api from '../../../api/client.js'
function makeUser(overrides = {}) {
return {
id: overrides.id ?? 'user-1',
email: overrides.email ?? 'alice@example.com',
handle: overrides.handle ?? 'alice',
role: 'user',
is_active: true,
ai_provider: overrides.ai_provider ?? null,
ai_model: overrides.ai_model ?? null,
...overrides,
}
}
beforeEach(() => {
vi.clearAllMocks()
})
// ── onMounted: calls adminListUsers() ─────────────────────────────────────────
describe('AdminAiConfigTab — onMounted', () => {
it('calls adminListUsers() on mount', async () => {
api.adminListUsers.mockResolvedValue({ items: [] })
mount(AdminAiConfigTab)
await flushPromises()
expect(api.adminListUsers).toHaveBeenCalledTimes(1)
})
it('shows "No users yet" empty state when no users', async () => {
api.adminListUsers.mockResolvedValue({ items: [] })
const w = mount(AdminAiConfigTab)
await flushPromises()
expect(w.text()).toContain('No users yet')
})
it('renders a row per user', async () => {
api.adminListUsers.mockResolvedValue({
items: [
makeUser({ id: 'u1', email: 'alice@example.com' }),
makeUser({ id: 'u2', email: 'bob@example.com' }),
],
})
const w = mount(AdminAiConfigTab)
await flushPromises()
expect(w.text()).toContain('alice@example.com')
expect(w.text()).toContain('bob@example.com')
})
it('pre-populates existing ai_provider and ai_model in inputs', async () => {
api.adminListUsers.mockResolvedValue({
items: [makeUser({ id: 'u1', ai_provider: 'openai', ai_model: 'gpt-4o' })],
})
const w = mount(AdminAiConfigTab)
await flushPromises()
// The select should have openai selected
const select = w.find('select')
expect(select.element.value).toBe('openai')
// The model input should have gpt-4o
const modelInput = w.find('input[type="text"]')
expect(modelInput.element.value).toBe('gpt-4o')
})
})
// ── saveConfig: calls adminUpdateAiConfig(id, provider, model) ────────────────
describe('AdminAiConfigTab — saveConfig', () => {
it('calls adminUpdateAiConfig with user id, provider, and model on Save click', async () => {
api.adminListUsers.mockResolvedValue({
items: [makeUser({ id: 'u1', email: 'alice@example.com', ai_provider: '', ai_model: '' })],
})
api.adminUpdateAiConfig.mockResolvedValue({})
const w = mount(AdminAiConfigTab)
await flushPromises()
// Select a provider
const select = w.find('select')
await select.setValue('anthropic')
// Enter a model
const modelInput = w.find('input[type="text"]')
await modelInput.setValue('claude-3-5-sonnet')
// Click Save
const saveBtn = w.findAll('button').find(b => b.text().includes('Save'))
expect(saveBtn).toBeTruthy()
await saveBtn.trigger('click')
await flushPromises()
expect(api.adminUpdateAiConfig).toHaveBeenCalledWith('u1', 'anthropic', 'claude-3-5-sonnet')
})
it('shows "Saved" confirmation text after successful save', async () => {
api.adminListUsers.mockResolvedValue({
items: [makeUser({ id: 'u1', email: 'alice@example.com' })],
})
api.adminUpdateAiConfig.mockResolvedValue({})
const w = mount(AdminAiConfigTab)
await flushPromises()
const saveBtn = w.findAll('button').find(b => b.text().includes('Save'))
await saveBtn.trigger('click')
await flushPromises()
expect(w.text()).toContain('Saved')
})
it('passes null for empty provider string to API', async () => {
api.adminListUsers.mockResolvedValue({
items: [makeUser({ id: 'u1', ai_provider: null, ai_model: null })],
})
api.adminUpdateAiConfig.mockResolvedValue({})
const w = mount(AdminAiConfigTab)
await flushPromises()
// Select is empty string '' — saveConfig converts '' to null
const saveBtn = w.findAll('button').find(b => b.text().includes('Save'))
await saveBtn.trigger('click')
await flushPromises()
// Called with null for both empty provider and model
expect(api.adminUpdateAiConfig).toHaveBeenCalledWith('u1', null, null)
})
})
@@ -0,0 +1,147 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
// Mock api/client.js
vi.mock('../../../api/client.js', () => ({
adminListUsers: vi.fn(),
adminGetUserQuota: vi.fn(),
adminUpdateQuota: vi.fn(),
}))
import AdminQuotasTab from '../AdminQuotasTab.vue'
import * as api from '../../../api/client.js'
const MB = 1048576
function makeUser(id, email) {
return { id, email, handle: email.split('@')[0], role: 'user', is_active: true }
}
function makeQuota(userId, usedMB, limitMB) {
return {
user_id: userId,
used_bytes: usedMB * MB,
limit_bytes: limitMB * MB,
}
}
beforeEach(() => {
vi.clearAllMocks()
})
// ── onMounted: loads users + quotas ──────────────────────────────────────────
describe('AdminQuotasTab — onMounted', () => {
it('calls adminListUsers() on mount', async () => {
api.adminListUsers.mockResolvedValue({ items: [] })
mount(AdminQuotasTab)
await flushPromises()
expect(api.adminListUsers).toHaveBeenCalledTimes(1)
})
it('shows "No users yet" empty state when API returns empty list', async () => {
api.adminListUsers.mockResolvedValue({ items: [] })
const w = mount(AdminQuotasTab)
await flushPromises()
expect(w.text()).toContain('No users yet')
})
it('calls adminGetUserQuota for each user', async () => {
api.adminListUsers.mockResolvedValue({
items: [makeUser('u1', 'alice@example.com'), makeUser('u2', 'bob@example.com')],
})
api.adminGetUserQuota
.mockResolvedValueOnce(makeQuota('u1', 10, 100))
.mockResolvedValueOnce(makeQuota('u2', 50, 200))
mount(AdminQuotasTab)
await flushPromises()
expect(api.adminGetUserQuota).toHaveBeenCalledWith('u1')
expect(api.adminGetUserQuota).toHaveBeenCalledWith('u2')
})
it('displays quota data in the table', async () => {
api.adminListUsers.mockResolvedValue({
items: [makeUser('u1', 'alice@example.com')],
})
api.adminGetUserQuota.mockResolvedValue(makeQuota('u1', 10, 100))
const w = mount(AdminQuotasTab)
await flushPromises()
expect(w.text()).toContain('alice@example.com')
// 10 MB used, 100 MB limit
expect(w.text()).toContain('10 MB')
expect(w.text()).toContain('100 MB')
})
})
// ── saveQuota: calls adminUpdateQuota(id, bytes) ──────────────────────────────
describe('AdminQuotasTab — saveQuota', () => {
it('calls adminUpdateQuota with user id and new limit in bytes on save', async () => {
api.adminListUsers.mockResolvedValue({
items: [makeUser('u1', 'alice@example.com')],
})
api.adminGetUserQuota.mockResolvedValue(makeQuota('u1', 10, 100))
api.adminUpdateQuota.mockResolvedValue({
user_id: 'u1',
used_bytes: 10 * MB,
limit_bytes: 200 * MB,
warning: false,
})
const w = mount(AdminQuotasTab)
await flushPromises()
// Click "Edit" button
const editBtn = w.find('button')
expect(editBtn.text()).toContain('Edit')
await editBtn.trigger('click')
await w.vm.$nextTick()
// Change the limit input to 200 MB
const input = w.find('input[type="number"]')
await input.setValue(200)
// Click "Save"
const saveBtn = w.findAll('button').find(b => b.text().includes('Save'))
await saveBtn.trigger('click')
await flushPromises()
expect(api.adminUpdateQuota).toHaveBeenCalledWith('u1', 200 * MB)
})
it('shows warning text when API response has warning: true', async () => {
api.adminListUsers.mockResolvedValue({
items: [makeUser('u1', 'alice@example.com')],
})
api.adminGetUserQuota.mockResolvedValue(makeQuota('u1', 90, 100))
api.adminUpdateQuota.mockResolvedValue({
user_id: 'u1',
used_bytes: 90 * MB,
limit_bytes: 50 * MB, // below current usage
warning: true,
})
const w = mount(AdminQuotasTab)
await flushPromises()
// Enter edit mode
await w.find('button').trigger('click')
await w.vm.$nextTick()
// Set limit below current usage
const input = w.find('input[type="number"]')
await input.setValue(50)
// Save
const saveBtn = w.findAll('button').find(b => b.text().includes('Save'))
await saveBtn.trigger('click')
await flushPromises()
// Warning text must appear
expect(w.text()).toMatch(/below current usage|uploads will be blocked/i)
})
})
@@ -0,0 +1,146 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
// Mock api/client.js
vi.mock('../../../api/client.js', () => ({
adminListUsers: vi.fn(),
adminDeactivateUser: vi.fn(),
adminReactivateUser: vi.fn(),
adminDeleteUser: vi.fn(),
adminResetUserPassword: vi.fn(),
adminCreateUser: vi.fn(),
}))
import AdminUsersTab from '../AdminUsersTab.vue'
import * as api from '../../../api/client.js'
function makeUser(overrides = {}) {
return {
id: overrides.id ?? 'user-1',
email: overrides.email ?? 'alice@example.com',
handle: overrides.handle ?? 'alice',
role: overrides.role ?? 'user',
is_active: overrides.is_active ?? true,
totp_enabled: false,
created_at: '2026-01-01T00:00:00Z',
...overrides,
}
}
beforeEach(() => {
vi.clearAllMocks()
})
// ── onMounted: calls adminListUsers() ─────────────────────────────────────────
describe('AdminUsersTab — onMounted', () => {
it('calls adminListUsers() on mount', async () => {
api.adminListUsers.mockResolvedValue({ items: [] })
mount(AdminUsersTab)
await flushPromises()
expect(api.adminListUsers).toHaveBeenCalledTimes(1)
})
it('populates user list from API response', async () => {
api.adminListUsers.mockResolvedValue({
items: [makeUser({ id: 'u1', email: 'alice@example.com' })],
})
const w = mount(AdminUsersTab)
await flushPromises()
expect(w.text()).toContain('alice@example.com')
})
it('shows "No users yet" empty state when API returns empty list', async () => {
api.adminListUsers.mockResolvedValue({ items: [] })
const w = mount(AdminUsersTab)
await flushPromises()
expect(w.text()).toContain('No users yet')
})
it('does NOT show "No users yet" when users are present', async () => {
api.adminListUsers.mockResolvedValue({
items: [makeUser()],
})
const w = mount(AdminUsersTab)
await flushPromises()
expect(w.text()).not.toContain('No users yet')
})
})
// ── Deactivate flow ───────────────────────────────────────────────────────────
describe('AdminUsersTab — deactivateUser', () => {
it('calls adminDeactivateUser(id) when user confirms deactivation', async () => {
const user = makeUser({ id: 'target-id', is_active: true })
api.adminListUsers.mockResolvedValue({ items: [user] })
api.adminDeactivateUser.mockResolvedValue({ is_active: false })
const w = mount(AdminUsersTab)
await flushPromises()
// Click "Deactivate" to enter confirmation state
const deactivateBtn = w.findAll('button').find(b => b.text().trim() === 'Deactivate')
expect(deactivateBtn).toBeTruthy()
await deactivateBtn.trigger('click')
await w.vm.$nextTick()
// Now click the confirmation "Deactivate" button in the inline panel
const confirmBtn = w.findAll('button').find(b => b.text().trim() === 'Deactivate')
expect(confirmBtn).toBeTruthy()
await confirmBtn.trigger('click')
await flushPromises()
expect(api.adminDeactivateUser).toHaveBeenCalledWith('target-id')
})
it('marks user as inactive in UI after deactivation', async () => {
const user = makeUser({ id: 'u1', is_active: true })
api.adminListUsers.mockResolvedValue({ items: [user] })
api.adminDeactivateUser.mockResolvedValue({ is_active: false })
const w = mount(AdminUsersTab)
await flushPromises()
// Trigger deactivate flow
const deactivateBtn = w.findAll('button').find(b => b.text().trim() === 'Deactivate')
await deactivateBtn.trigger('click')
await w.vm.$nextTick()
const confirmBtn = w.findAll('button').find(b => b.text().trim() === 'Deactivate')
await confirmBtn.trigger('click')
await flushPromises()
// The row should now show "Deactivated" status
expect(w.text()).toContain('Deactivated')
})
})
// ── Multiple users rendered ───────────────────────────────────────────────────
describe('AdminUsersTab — user list rendering', () => {
it('renders all users returned by API', async () => {
api.adminListUsers.mockResolvedValue({
items: [
makeUser({ id: 'u1', email: 'alice@example.com' }),
makeUser({ id: 'u2', email: 'bob@example.com' }),
],
})
const w = mount(AdminUsersTab)
await flushPromises()
expect(w.text()).toContain('alice@example.com')
expect(w.text()).toContain('bob@example.com')
})
it('shows user count', async () => {
api.adminListUsers.mockResolvedValue({
items: [makeUser(), makeUser({ id: 'u2', email: 'b@b.com' })],
})
const w = mount(AdminUsersTab)
await flushPromises()
expect(w.text()).toContain('2 users')
})
})