Catalog missing skin-site messages and check source IDs during i18n validation. Renew the saved JWT before loading session data, guard overlapping login/logout requests, and reload announcements after authentication with a retry action.
209 lines
7.2 KiB
TypeScript
209 lines
7.2 KiB
TypeScript
import assert from 'node:assert/strict'
|
|
import { registerHooks } from 'node:module'
|
|
import test from 'node:test'
|
|
|
|
type FetchHandler = (url: string, options: RequestInit) => Promise<Response>
|
|
const testGlobal = globalThis as unknown as { skinSiteTestFetch: FetchHandler }
|
|
const hooks = registerHooks({
|
|
resolve(specifier, context, nextResolve) {
|
|
if (specifier === '@tauri-apps/plugin-http') {
|
|
return {
|
|
url: 'data:text/javascript,export const fetch=(...args)=>globalThis.skinSiteTestFetch(...args)',
|
|
shortCircuit: true,
|
|
}
|
|
}
|
|
if (specifier.startsWith('@/')) {
|
|
return nextResolve(new URL(`../${specifier.slice(2)}.ts`, import.meta.url).href, context)
|
|
}
|
|
return nextResolve(specifier, context)
|
|
},
|
|
})
|
|
const session = await import('./skin-site-session.ts')
|
|
hooks.deregister()
|
|
|
|
const user = { uuid: 'owner', username: 'Player', permissions: [] }
|
|
const ok = (payload: unknown) => Response.json({ payload })
|
|
const storage = new Map<string, string>()
|
|
const tokenKey = 'starlight-skin-site-token'
|
|
let calls: Array<{ path: string; token: string | undefined }> = []
|
|
let respond: FetchHandler
|
|
|
|
test.beforeEach(() => {
|
|
storage.clear()
|
|
Object.defineProperty(globalThis, 'localStorage', {
|
|
configurable: true,
|
|
value: {
|
|
getItem: (key: string) => storage.get(key) ?? null,
|
|
setItem: (key: string, value: string) => storage.set(key, value),
|
|
removeItem: (key: string) => storage.delete(key),
|
|
},
|
|
})
|
|
calls = []
|
|
respond = async (url) => {
|
|
if (url.endsWith('/user/refresh')) return ok({ username: 'Player', jwt: 'fresh-token' })
|
|
if (url.endsWith('/user')) return ok(user)
|
|
return ok([])
|
|
}
|
|
testGlobal.skinSiteTestFetch = async (url, options) => {
|
|
calls.push({
|
|
path: new URL(url).pathname,
|
|
token: (options.headers as Record<string, string>)?.Authorization,
|
|
})
|
|
return respond(url, options)
|
|
}
|
|
session.resetSkinSiteSession()
|
|
})
|
|
|
|
test.afterEach(async () => {
|
|
session.signOutSkinSite()
|
|
await new Promise((resolve) => setImmediate(resolve))
|
|
Reflect.deleteProperty(globalThis, 'localStorage')
|
|
})
|
|
|
|
test('startup renews the stored JWT before loading the user with the new token', async () => {
|
|
storage.set(tokenKey, 'old-token')
|
|
await session.initSkinSiteSession()
|
|
assert.deepEqual(calls.slice(0, 2), [
|
|
{ path: '/starlight/user/refresh', token: 'Bearer old-token' },
|
|
{ path: '/starlight/user', token: 'Bearer fresh-token' },
|
|
])
|
|
assert.equal(storage.get(tokenKey), 'fresh-token')
|
|
assert.equal(session.skinSiteStatus.value, 'signed-in')
|
|
})
|
|
|
|
test('simultaneous startup consumers share one refresh', async () => {
|
|
storage.set(tokenKey, 'old-token')
|
|
await Promise.all([
|
|
session.initSkinSiteSession(),
|
|
session.initSkinSiteSession(),
|
|
session.waitForSkinSiteSession(),
|
|
])
|
|
assert.equal(calls.filter((call) => call.path.endsWith('/refresh')).length, 1)
|
|
})
|
|
|
|
test('startup without a stored token does not make authenticated requests', async () => {
|
|
await session.initSkinSiteSession()
|
|
assert.equal(session.skinSiteStatus.value, 'signed-out')
|
|
assert.deepEqual(calls, [])
|
|
})
|
|
|
|
test('an expired token is cleared when renewal is rejected', async () => {
|
|
storage.set(tokenKey, 'expired-token')
|
|
respond = async () => Response.json({ errorMessage: 'Expired' }, { status: 401 })
|
|
await session.initSkinSiteSession()
|
|
assert.equal(storage.has(tokenKey), false)
|
|
assert.equal(session.skinSiteStatus.value, 'signed-out')
|
|
assert.equal(calls.length, 1)
|
|
})
|
|
|
|
test('temporary renewal failure preserves the token and allows startup retry', async () => {
|
|
storage.set(tokenKey, 'old-token')
|
|
respond = async () => Response.json({ errorMessage: 'Unavailable' }, { status: 503 })
|
|
await session.initSkinSiteSession()
|
|
assert.equal(session.skinSiteStatus.value, 'error')
|
|
assert.equal(storage.get(tokenKey), 'old-token')
|
|
respond = async (url) =>
|
|
url.endsWith('/refresh')
|
|
? ok({ jwt: 'fresh-token' })
|
|
: url.endsWith('/user')
|
|
? ok(user)
|
|
: ok([])
|
|
await session.initSkinSiteSession()
|
|
assert.equal(session.skinSiteStatus.value, 'signed-in')
|
|
})
|
|
|
|
test('a late refresh cannot restore a session after sign-out', async () => {
|
|
storage.set(tokenKey, 'old-token')
|
|
let finish!: (response: Response) => void
|
|
respond = () =>
|
|
new Promise((resolve) => {
|
|
finish = resolve
|
|
})
|
|
const initializing = session.initSkinSiteSession()
|
|
await new Promise((resolve) => setImmediate(resolve))
|
|
session.signOutSkinSite()
|
|
finish(ok({ username: 'Player', jwt: 'late-token' }))
|
|
await initializing
|
|
assert.equal(storage.has(tokenKey), false)
|
|
assert.equal(session.skinSiteStatus.value, 'signed-out')
|
|
assert.equal(session.skinSiteUser.value, null)
|
|
})
|
|
|
|
test('a malformed refresh response never replaces the saved token', async () => {
|
|
storage.set(tokenKey, 'old-token')
|
|
respond = async () => ok({ username: 'Player' })
|
|
await session.initSkinSiteSession()
|
|
assert.equal(storage.get(tokenKey), 'old-token')
|
|
assert.equal(session.skinSiteStatus.value, 'error')
|
|
})
|
|
|
|
test('a failed sign-in attempt does not invalidate a pending startup renewal', async () => {
|
|
storage.set(tokenKey, 'old-token')
|
|
let finish!: (response: Response) => void
|
|
respond = async (url) => {
|
|
if (url.endsWith('/refresh'))
|
|
return new Promise((resolve) => {
|
|
finish = resolve
|
|
})
|
|
if (url.endsWith('/login'))
|
|
return Response.json({ errorMessage: 'Invalid password' }, { status: 401 })
|
|
return url.endsWith('/user') ? ok(user) : ok([])
|
|
}
|
|
const initializing = session.initSkinSiteSession()
|
|
await new Promise((resolve) => setImmediate(resolve))
|
|
await session.signInSkinSite({ username: 'Player', password: 'wrong', clientId: 'captcha' })
|
|
finish(ok({ username: 'Player', jwt: 'fresh-token' }))
|
|
await initializing
|
|
assert.equal(session.skinSiteStatus.value, 'signed-in')
|
|
assert.equal(storage.get(tokenKey), 'fresh-token')
|
|
})
|
|
|
|
test('rejection of an old startup token does not discard a new successful login', async () => {
|
|
storage.set(tokenKey, 'expired-token')
|
|
let finishRefresh!: (response: Response) => void
|
|
let finishLogin!: (response: Response) => void
|
|
respond = async (url) => {
|
|
if (url.endsWith('/refresh'))
|
|
return new Promise((resolve) => {
|
|
finishRefresh = resolve
|
|
})
|
|
if (url.endsWith('/login'))
|
|
return new Promise((resolve) => {
|
|
finishLogin = resolve
|
|
})
|
|
return url.endsWith('/user') ? ok(user) : ok([])
|
|
}
|
|
const initializing = session.initSkinSiteSession()
|
|
const signingIn = session.signInSkinSite({
|
|
username: 'Player',
|
|
password: 'valid',
|
|
clientId: 'captcha',
|
|
})
|
|
await new Promise((resolve) => setImmediate(resolve))
|
|
finishRefresh(Response.json({ errorMessage: 'Expired' }, { status: 401 }))
|
|
await initializing
|
|
finishLogin(ok({ username: 'Player', jwt: 'login-token' }))
|
|
await signingIn
|
|
assert.equal(session.skinSiteStatus.value, 'signed-in')
|
|
assert.equal(storage.get(tokenKey), 'login-token')
|
|
})
|
|
|
|
test('explicit sign-out discards a late successful login', async () => {
|
|
let finishLogin!: (response: Response) => void
|
|
respond = () =>
|
|
new Promise((resolve) => {
|
|
finishLogin = resolve
|
|
})
|
|
const signingIn = session.signInSkinSite({
|
|
username: 'Player',
|
|
password: 'valid',
|
|
clientId: 'captcha',
|
|
})
|
|
await new Promise((resolve) => setImmediate(resolve))
|
|
session.signOutSkinSite()
|
|
finishLogin(ok({ username: 'Player', jwt: 'late-login-token' }))
|
|
await signingIn
|
|
assert.equal(session.skinSiteStatus.value, 'signed-out')
|
|
assert.equal(storage.has(tokenKey), false)
|
|
})
|