credentialVaultRoutes.test.ts 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839
  1. // @vitest-environment node
  2. import { afterEach, expect, it, vi } from 'vitest'
  3. import type Koa from 'koa'
  4. import { mkdtempSync, rmSync } from 'node:fs'
  5. import { tmpdir } from 'node:os'
  6. import { join } from 'node:path'
  7. import request from 'supertest'
  8. import { createApplication, type ApplicationRuntime } from '../../src/server/app'
  9. import { LocalAuthStore } from '../../src/server/localAuth'
  10. import type { ServerConfig } from '../../src/server/config'
  11. const runtimes:ApplicationRuntime[]=[],homes:string[]=[]
  12. function context():Koa.Context{const headers:Record<string,any>={};return {state:{},get:()=>'',set:(k:string,v:any)=>{headers[k.toLowerCase()]=v},response:{headers}} as any}
  13. function cookie(response:any){const values=response.headers['set-cookie'];return (Array.isArray(values)?values:[values]).filter(Boolean).map((v:string)=>v.split(';')[0]).join('; ')}
  14. async function fixture(){
  15. const home=mkdtempSync(join(tmpdir(),'yaoyao-vault-routes-'));homes.push(home)
  16. const auth=new LocalAuthStore(home),login=context();auth.setupAdmin(login,'dummy-admin','DUMMY-only-login-password')
  17. const config={host:'127.0.0.1',port:15300,upstream:new URL('http://127.0.0.1:9119'),allowedHosts:new Set(),home,mediaRoot:join(home,'media'),attachmentsRoot:join(home,'attachments'),imagesRoot:join(home,'images'),mediaOwner:'dummy',allowInsecureLan:false,insecureLan:false,production:true} as ServerConfig
  18. const fetchImpl=vi.fn(async()=>new Response(JSON.stringify({profiles:['default']}),{headers:{'content-type':'application/json'}})) as unknown as typeof fetch
  19. const runtime=createApplication({config,auth,fetchImpl});runtimes.push(runtime)
  20. const callback=runtime.app.callback(),bootstrap=await request(callback).get('/api/app/bootstrap?inspectOnly=1').set('Host','127.0.0.1:15300').set('Cookie',cookie(login.response)).expect(200)
  21. const cookies=cookie(login.response)+'; '+cookie(bootstrap),token=bootstrap.body.csrfToken
  22. return {runtime,callback,fetchImpl,cookies,token,mutation:(path:string)=>request(callback).post(path).set('Host','127.0.0.1:15300').set('Cookie',cookies).set('Origin','http://127.0.0.1:15300').set('X-CSRF-Token',token)}
  23. }
  24. afterEach(()=>{runtimes.splice(0).forEach(r=>r.close());homes.splice(0).forEach(h=>rmSync(h,{recursive:true,force:true}));vi.restoreAllMocks()})
  25. it('vault routes enforce authentication, exact Origin and CSRF and stay offline without configuration',async()=>{
  26. const f=await fixture()
  27. await request(f.callback).get('/api/app/vault').set('Host','127.0.0.1:15300').expect(401)
  28. const status=await request(f.callback).get('/api/app/vault').set('Host','127.0.0.1:15300').set('Cookie',f.cookies).expect(200)
  29. expect(status.body).toMatchObject({online:false,unlocked:false,execution:'disabled',entries:[]});expect(status.headers['cache-control']).toBe('no-store')
  30. await request(f.callback).post('/api/app/vault/unlock').set('Host','127.0.0.1:15300').set('Cookie',f.cookies).set('Origin','http://127.0.0.1:15300').send({password:'DUMMY-master'}).expect(403)
  31. await f.mutation('/api/app/vault/unlock').set('Origin','https://evil.test').send({password:'DUMMY-master'}).expect(403)
  32. const response=await f.mutation('/api/app/vault/unlock').send({password:'DUMMY-master'}).expect(503)
  33. expect(response.body.code).toBe('vault_offline');expect(JSON.stringify(response.body)).not.toContain('DUMMY-master');expect(f.fetchImpl).not.toHaveBeenCalled()
  34. })
  35. it.each(['/api/app/logout','/auth/logout'])('logout path %s revokes vault before invalidating the authenticated session',async path=>{
  36. const f=await fixture(),logout=vi.spyOn(f.runtime.credentialVault,'logout').mockImplementation(async(owner,c)=>{expect(f.runtime.auth.currentFromCookieHeader(c)?.id).toBe(owner)})
  37. await f.mutation(path).send({}).expect(200);expect(logout).toHaveBeenCalledOnce()
  38. await request(f.callback).get('/api/app/vault').set('Host','127.0.0.1:15300').set('Cookie',f.cookies).expect(401)
  39. })