Files
toor 01d9e301fc
Build / build (push) Canceled after 0s
Playwright / browser (push) Canceled after 0s
initialize Electron desktop app and remove unused files
2026-10-25 01:09:01 +08:00

292 lines
10 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
describe('user avatar storage and controller', () => {
let db: any = null
beforeEach(async () => {
vi.resetModules()
vi.stubEnv('AUTH_JWT_SECRET', 'test-secret')
const { DatabaseSync } = await import('node:sqlite')
db = new DatabaseSync(':memory:')
vi.doMock('../../packages/server/src/db/index', () => ({
getDb: () => db,
getStoragePath: () => ':memory:',
}))
})
afterEach(() => {
db?.close()
db = null
vi.doUnmock('../../packages/server/src/db/index')
vi.doUnmock('../../packages/server/src/services/hermes/hermes-profile')
vi.unstubAllEnvs()
vi.resetModules()
})
async function initUsers() {
const schemas = await import('../../packages/server/src/db/hermes/schemas')
schemas.initAllHermesTables()
return {
schemas,
users: await import('../../packages/server/src/db/hermes/users-store'),
ctrl: await import('../../packages/server/src/controllers/auth'),
}
}
function makeControllerCtx(user: any | null | undefined, body: any = {}) {
return {
state: user ? { user } : {},
request: { body },
status: 200,
body: null,
} as any
}
describe('users-store avatar helpers', () => {
it('returns an empty string for a brand new user', async () => {
const { users } = await initUsers()
const admin = users.bootstrapDefaultSuperAdmin('admin', '123456')!
expect(users.getUserAvatar(admin.id)).toBe('')
})
it('round-trips avatar JSON through set/getUserAvatar', async () => {
const { users } = await initUsers()
const admin = users.bootstrapDefaultSuperAdmin('admin', '123456')!
const payload = JSON.stringify({ type: 'image', dataUrl: 'data:image/png;base64,AAA' })
const ok = users.setUserAvatar(admin.id, payload)
expect(ok).toBe(true)
expect(users.getUserAvatar(admin.id)).toBe(payload)
})
it('setUserAvatar returns false when the user does not exist', async () => {
const { users } = await initUsers()
// create a user so the table exists, but use a clearly non-existent id
users.bootstrapDefaultSuperAdmin('admin', '123456')
expect(users.setUserAvatar(9999, '{"type":"default"}')).toBe(false)
expect(users.getUserAvatar(9999)).toBe('')
})
it('setUserAvatar returns false when given a non-numeric userId', async () => {
const { users } = await initUsers()
users.bootstrapDefaultSuperAdmin('admin', '123456')
expect(users.setUserAvatar('not-a-number', '{"type":"default"}')).toBe(false)
expect(users.setUserAvatar(-1, '{"type":"default"}')).toBe(false)
expect(users.setUserAvatar(0, '{"type":"default"}')).toBe(false)
})
it('persists the avatar in the users table', async () => {
const { users, schemas } = await initUsers()
const admin = users.bootstrapDefaultSuperAdmin('admin', '123456')!
const payload = JSON.stringify({ type: 'default' })
users.setUserAvatar(admin.id, payload)
const row = db
.prepare(`SELECT avatar FROM ${schemas.USERS_TABLE} WHERE id = ?`)
.get(admin.id) as { avatar?: string }
expect(row?.avatar).toBe(payload)
})
})
describe('getMyAvatar controller', () => {
it('returns 401 when the request is unauthenticated', async () => {
const { ctrl } = await initUsers()
const ctx = makeControllerCtx(null)
await ctrl.getMyAvatar(ctx)
expect(ctx.status).toBe(401)
expect(ctx.body).toEqual({ error: 'Unauthorized' })
})
it('returns the avatar string for the authenticated user', async () => {
const { users, ctrl } = await initUsers()
const admin = users.bootstrapDefaultSuperAdmin('admin', '123456')!
const payload = JSON.stringify({ type: 'image', dataUrl: 'data:image/png;base64,HI==' })
users.setUserAvatar(admin.id, payload)
const ctx = makeControllerCtx({ id: admin.id, username: 'admin', role: 'super_admin' })
await ctrl.getMyAvatar(ctx)
expect(ctx.status).toBe(200)
expect(ctx.body).toEqual({ avatar: payload })
})
it('returns an empty avatar string for an authenticated user without an avatar', async () => {
const { users, ctrl } = await initUsers()
const admin = users.bootstrapDefaultSuperAdmin('admin', '123456')!
const ctx = makeControllerCtx({ id: admin.id, username: 'admin', role: 'super_admin' })
await ctrl.getMyAvatar(ctx)
expect(ctx.status).toBe(200)
expect(ctx.body).toEqual({ avatar: '' })
})
})
describe('updateMyAvatar controller', () => {
it('persists a default avatar payload sent as a direct object', async () => {
const { users, ctrl } = await initUsers()
const admin = users.bootstrapDefaultSuperAdmin('admin', '123456')!
const ctx = makeControllerCtx(
{ id: admin.id, username: 'admin', role: 'super_admin' },
{ type: 'default' },
)
await ctrl.updateMyAvatar(ctx)
expect(ctx.status).toBe(200)
expect(ctx.body.success).toBe(true)
const stored = users.getUserAvatar(admin.id)
expect(JSON.parse(stored)).toEqual({ type: 'default' })
})
it('persists an image avatar payload sent as a direct object', async () => {
const { users, ctrl } = await initUsers()
const admin = users.bootstrapDefaultSuperAdmin('admin', '123456')!
const dataUrl = 'data:image/png;base64,iVBORw0KGgo='
const ctx = makeControllerCtx(
{ id: admin.id, username: 'admin', role: 'super_admin' },
{ type: 'image', dataUrl },
)
await ctrl.updateMyAvatar(ctx)
expect(ctx.status).toBe(200)
expect(ctx.body.success).toBe(true)
const stored = users.getUserAvatar(admin.id)
expect(JSON.parse(stored)).toEqual({ type: 'image', dataUrl })
})
it('persists a JSON-string avatar passed in the avatar field', async () => {
const { users, ctrl } = await initUsers()
const admin = users.bootstrapDefaultSuperAdmin('admin', '123456')!
const serialized = JSON.stringify({ type: 'default', seed: 'abc' })
const ctx = makeControllerCtx(
{ id: admin.id, username: 'admin', role: 'super_admin' },
{ avatar: serialized },
)
await ctrl.updateMyAvatar(ctx)
expect(ctx.status).toBe(200)
expect(ctx.body.avatar).toBe(serialized)
expect(users.getUserAvatar(admin.id)).toBe(serialized)
})
it('rejects an unknown type with HTTP 400', async () => {
const { users, ctrl } = await initUsers()
const admin = users.bootstrapDefaultSuperAdmin('admin', '123456')!
const ctx = makeControllerCtx(
{ id: admin.id, username: 'admin', role: 'super_admin' },
{ type: 'emoji', dataUrl: '🦊' },
)
await ctrl.updateMyAvatar(ctx)
expect(ctx.status).toBe(400)
expect(ctx.body.error).toMatch(/image.*default/)
expect(users.getUserAvatar(admin.id)).toBe('')
})
it('rejects an image payload whose dataUrl exceeds 500KB with HTTP 400', async () => {
const { users, ctrl } = await initUsers()
const admin = users.bootstrapDefaultSuperAdmin('admin', '123456')!
const oversized = 'data:image/png;base64,' + 'A'.repeat(500 * 1024 + 16)
const ctx = makeControllerCtx(
{ id: admin.id, username: 'admin', role: 'super_admin' },
{ type: 'image', dataUrl: oversized },
)
await ctrl.updateMyAvatar(ctx)
expect(ctx.status).toBe(400)
expect(ctx.body.error).toMatch(/too large|bytes/)
expect(users.getUserAvatar(admin.id)).toBe('')
})
it('rejects an image payload with no dataUrl with HTTP 400', async () => {
const { users, ctrl } = await initUsers()
const admin = users.bootstrapDefaultSuperAdmin('admin', '123456')!
const ctx = makeControllerCtx(
{ id: admin.id, username: 'admin', role: 'super_admin' },
{ type: 'image' },
)
await ctrl.updateMyAvatar(ctx)
expect(ctx.status).toBe(400)
expect(ctx.body.error).toMatch(/dataUrl/)
expect(users.getUserAvatar(admin.id)).toBe('')
})
it('rejects an image payload whose dataUrl is not a data URL with HTTP 400', async () => {
const { users, ctrl } = await initUsers()
const admin = users.bootstrapDefaultSuperAdmin('admin', '123456')!
const ctx = makeControllerCtx(
{ id: admin.id, username: 'admin', role: 'super_admin' },
{ type: 'image', dataUrl: 'https://example.com/avatar.png' },
)
await ctrl.updateMyAvatar(ctx)
expect(ctx.status).toBe(400)
expect(ctx.body.error).toMatch(/dataUrl/)
})
it('rejects a non-object body with HTTP 400', async () => {
const { ctrl } = await initUsers()
const ctx = makeControllerCtx(
{ id: 1, username: 'admin', role: 'super_admin' },
'just-a-string',
)
await ctrl.updateMyAvatar(ctx)
expect(ctx.status).toBe(400)
// A bare string is not valid JSON, so the controller rejects it via the JSON.parse path.
expect(ctx.body.error).toMatch(/not valid JSON|Invalid avatar payload|too large/)
})
it('rejects a malformed JSON string in the avatar field with HTTP 400', async () => {
const { ctrl } = await initUsers()
const ctx = makeControllerCtx(
{ id: 1, username: 'admin', role: 'super_admin' },
{ avatar: '{not valid json' },
)
await ctrl.updateMyAvatar(ctx)
expect(ctx.status).toBe(400)
expect(ctx.body.error).toMatch(/not valid JSON|Invalid/)
})
it('rejects a non-string seed with HTTP 400', async () => {
const { ctrl } = await initUsers()
const ctx = makeControllerCtx(
{ id: 1, username: 'admin', role: 'super_admin' },
{ type: 'default', seed: 123 },
)
await ctrl.updateMyAvatar(ctx)
expect(ctx.status).toBe(400)
expect(ctx.body.error).toMatch(/seed/)
})
it('returns 401 when the request is unauthenticated', async () => {
const { ctrl } = await initUsers()
const ctx = makeControllerCtx(null, { type: 'default' })
await ctrl.updateMyAvatar(ctx)
expect(ctx.status).toBe(401)
expect(ctx.body).toEqual({ error: 'Unauthorized' })
})
})
})