localAuth.test.ts 4.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
  1. import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
  2. import { tmpdir } from 'node:os'
  3. import { join } from 'node:path'
  4. import type Koa from 'koa'
  5. import { afterEach, describe, expect, it } from 'vitest'
  6. import { LocalAuthStore } from '../../src/server/localAuth.js'
  7. const homes: string[] = []
  8. function context(cookie = ''): Koa.Context {
  9. const headers: Record<string, string | string[]> = {}
  10. return {
  11. state: {},
  12. get: (name: string) => name.toLowerCase() === 'cookie' ? cookie : '',
  13. set: (name: string, value: string | string[]) => { headers[name.toLowerCase()] = value },
  14. response: { headers },
  15. } as unknown as Koa.Context
  16. }
  17. function issuedCookie(ctx: Koa.Context): string {
  18. const value = ctx.response.headers['set-cookie']
  19. const raw = Array.isArray(value) ? value.at(-1) : String(value)
  20. return raw.split(';', 1)[0]!
  21. }
  22. afterEach(() => {
  23. for (const home of homes.splice(0)) rmSync(home, { recursive: true, force: true })
  24. })
  25. describe('8800 local users', () => {
  26. it('creates admin/admin and forces a credential change', () => {
  27. const home = mkdtempSync(join(tmpdir(), 'yaoyao-local-auth-'))
  28. homes.push(home)
  29. const store = new LocalAuthStore(home)
  30. const login = context()
  31. const admin = store.login(login, 'admin', 'admin')
  32. expect(admin).toMatchObject({ role: 'admin', mustChangePassword: true })
  33. const authenticated = context(issuedCookie(login))
  34. expect(() => store.require(authenticated)).toThrow(/修改初始密码/)
  35. const changed = store.changeCredentials(authenticated, 'admin', 'new-password', 'owner')
  36. expect(changed).toMatchObject({ username: 'owner', mustChangePassword: false })
  37. expect(store.require(context(issuedCookie(authenticated))).username).toBe('owner')
  38. const restarted = new LocalAuthStore(home)
  39. expect(restarted.require(context(issuedCookie(authenticated))).username).toBe('owner')
  40. })
  41. it('lets the administrator create, reset, disable and delete ordinary users', () => {
  42. const home = mkdtempSync(join(tmpdir(), 'yaoyao-local-auth-'))
  43. homes.push(home)
  44. const store = new LocalAuthStore(home)
  45. const first = context()
  46. store.login(first, 'admin', 'admin')
  47. const changedContext = context(issuedCookie(first))
  48. store.changeCredentials(changedContext, 'admin', 'new-password', 'admin')
  49. const admin = store.require(context(issuedCookie(changedContext)))
  50. const user = store.create(admin, 'member', 'temporary-password')
  51. expect(user.mustChangePassword).toBe(true)
  52. expect(store.updateUser(admin, user.id, { enabled: false }).enabled).toBe(false)
  53. store.deleteUser(admin, user.id)
  54. expect(store.list(admin).map(item => item.username)).toEqual(['admin'])
  55. })
  56. it('persists an account image without invalidating the existing session', () => {
  57. const home = mkdtempSync(join(tmpdir(), 'yaoyao-local-auth-')); homes.push(home)
  58. const store = new LocalAuthStore(home), login = context()
  59. store.login(login, 'admin', 'admin')
  60. const authenticated = context(issuedCookie(login))
  61. store.changeCredentials(authenticated, 'admin', 'new-password', 'admin')
  62. const active = context(issuedCookie(authenticated))
  63. const avatar = 'data:image/png;base64,aGVsbG8='
  64. expect(store.setAvatar(active, avatar).avatar).toBe(avatar)
  65. expect(store.require(active).avatar).toBe(avatar)
  66. expect(new LocalAuthStore(home).require(context(issuedCookie(authenticated))).avatar).toBe(avatar)
  67. expect(() => store.setAvatar(active, 'https://example.test/avatar.png')).toThrow(/PNG、JPEG/)
  68. expect(store.setAvatar(active, null).avatar).toBeUndefined()
  69. })
  70. it('encrypts the 9119 service password at rest', () => {
  71. const home = mkdtempSync(join(tmpdir(), 'yaoyao-local-auth-'))
  72. homes.push(home)
  73. const store = new LocalAuthStore(home)
  74. const first = context()
  75. store.login(first, 'admin', 'admin')
  76. const changedContext = context(issuedCookie(first))
  77. store.changeCredentials(changedContext, 'admin', 'new-password', 'admin')
  78. const admin = store.require(context(issuedCookie(changedContext)))
  79. store.setUpstreamCredentials(admin, { username: 'service', password: 'very-secret' })
  80. expect(store.upstreamCredentials()).toEqual({ username: 'service', password: 'very-secret' })
  81. expect(readFileSync(join(home, 'upstream-credentials.enc'), 'utf8')).not.toContain('very-secret')
  82. })
  83. })