|
|
|
@@ -11,6 +11,26 @@ const date = '2026-09-15T00:00:00+09:00'
|
|
|
|
|
let api, apiURL, routes, vite, node, httpOrigin
|
|
|
|
|
let failAPI = false
|
|
|
|
|
const requests = []
|
|
|
|
|
// Rails owns encryption and verification; this upstream fixture tests Node's forwarding contract.
|
|
|
|
|
const recoveryCode = 'test-recovery-secret'
|
|
|
|
|
const users = new Map ([
|
|
|
|
|
[recoveryCode, { id: 1, name: 'SSR account', role: 'admin' }],
|
|
|
|
|
['second-recovery-secret', { id: 2, name: 'Second account', role: 'member' }]])
|
|
|
|
|
const sessions = new Map ()
|
|
|
|
|
let sessionSequence = 0
|
|
|
|
|
const cookieName = '__Host-btrc-session'
|
|
|
|
|
const cookieFor = value => `${cookieName}=${value}`
|
|
|
|
|
const sessionValue = request => request.headers.cookie?.split (';')
|
|
|
|
|
.map (part => part.trim ()).find (part => part.startsWith (`${cookieName}=`))
|
|
|
|
|
?.slice (cookieName.length + 1)
|
|
|
|
|
const issueSession = (response, code = null) => {
|
|
|
|
|
const value = `opaque-rails-session-${++sessionSequence}`
|
|
|
|
|
sessions.set (value, { code, token: `csrf-${sessionSequence}` })
|
|
|
|
|
refreshCookie (response, value)
|
|
|
|
|
return value
|
|
|
|
|
}
|
|
|
|
|
const refreshCookie = (response, value) => response.setHeader ('Set-Cookie',
|
|
|
|
|
`${cookieFor (value)}; Path=/; Max-Age=630720000; HttpOnly; Secure; SameSite=Lax`)
|
|
|
|
|
const tag = locale => ({
|
|
|
|
|
id: 1, name: locale === 'ja' ? 'SSR タグ' : 'SSR シロシ', category: 'general',
|
|
|
|
|
deprecated_at: null, aliases: [], parents: [], children: [], post_count: 1,
|
|
|
|
@@ -40,7 +60,6 @@ const responseFor = url => {
|
|
|
|
|
const path = url.pathname
|
|
|
|
|
if (path === '/locales')
|
|
|
|
|
return { locales: [{ code: 'ja', name: '日本語' }, { code: 'ain-Kana', name: 'アイヌ' }] }
|
|
|
|
|
if (path === '/users/session') return { id: 1, name: 'SSR account', role: 'admin' }
|
|
|
|
|
if (path === '/users/settings') return { theme: 'system', wiki_editor_mode: 'split' }
|
|
|
|
|
if (path === '/users/theme_slots') return []
|
|
|
|
|
if (path === '/posts') return { posts: [post (locale)], count: 1 }
|
|
|
|
@@ -63,24 +82,98 @@ const responseFor = url => {
|
|
|
|
|
if (path.includes ('suppressions')) return { suppressions: [] }
|
|
|
|
|
if (path === '/theatres/1') return { id: 1, name: 'SSR theatre', created_at: date }
|
|
|
|
|
if (path === '/theatres/1/programmes') return []
|
|
|
|
|
if (path === '/theatres/1/comments') return []
|
|
|
|
|
if (path === '/theatres/1/post_selection_weights')
|
|
|
|
|
return { tag_penalties: [], lightest_posts: [], heaviest_posts: [] }
|
|
|
|
|
if (path === '/theatres/1/watching')
|
|
|
|
|
return { host_flg: false, post_id: null, post_started_at: null, post_elapsed_ms: null,
|
|
|
|
|
watching_users: [], skip_vote: {
|
|
|
|
|
votes_count: 0, required_count: 1, watching_users_count: 0, voted: false } }
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
before (async () => {
|
|
|
|
|
api = createServer ((request, response) => {
|
|
|
|
|
requests.push ({ method: request.method, url: request.url })
|
|
|
|
|
api = createServer (async (request, response) => {
|
|
|
|
|
requests.push ({ method: request.method, url: request.url,
|
|
|
|
|
cookie: request.headers.cookie, csrf: request.headers['x-csrf-token'],
|
|
|
|
|
transferCode: request.headers['x-transfer-code'] })
|
|
|
|
|
const url = new URL (request.url, 'http://localhost')
|
|
|
|
|
response.setHeader ('Content-Type', 'application/json')
|
|
|
|
|
response.setHeader ('Cache-Control', 'private, no-store')
|
|
|
|
|
if (failAPI) {
|
|
|
|
|
response.writeHead (503).end ()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if (url.pathname.startsWith ('/users')) {
|
|
|
|
|
const value = sessionValue (request)
|
|
|
|
|
const session = sessions.get (value)
|
|
|
|
|
const user = users.get (session?.code)
|
|
|
|
|
if (request.method === 'GET') {
|
|
|
|
|
if (url.pathname === '/users/csrf') {
|
|
|
|
|
const id = session != null ? value : issueSession (response)
|
|
|
|
|
response.end (JSON.stringify ({ token: sessions.get (id).token }))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if (user == null) {
|
|
|
|
|
response.writeHead (401).end ()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
refreshCookie (response, value)
|
|
|
|
|
const data = url.pathname === '/users/session' ? user
|
|
|
|
|
: (url.pathname === '/users/me' ? { ...user, inheritance_code: session.code }
|
|
|
|
|
: responseFor (url))
|
|
|
|
|
response.writeHead (data == null ? 404 : 200)
|
|
|
|
|
response.end (JSON.stringify (data))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if (![origin, httpOrigin].includes (request.headers.origin)
|
|
|
|
|
|| session == null || request.headers['x-csrf-token'] !== session.token) {
|
|
|
|
|
response.writeHead (403, { 'X-CSRF-Invalid': '1' }).end ()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
let body = ''
|
|
|
|
|
for await (const chunk of request) body += chunk
|
|
|
|
|
const params = JSON.parse (body || '{}')
|
|
|
|
|
if (request.method === 'POST' && url.pathname === '/users/verify') {
|
|
|
|
|
const code = params.restore && user != null ? session.code : params.code
|
|
|
|
|
const verified = users.get (code)
|
|
|
|
|
if (verified == null) {
|
|
|
|
|
response.end (JSON.stringify ({ valid: false }))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
sessions.delete (value)
|
|
|
|
|
issueSession (response, code)
|
|
|
|
|
response.end (JSON.stringify ({ valid: true,
|
|
|
|
|
user: { ...verified, inheritance_code: code } }))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if (request.method === 'DELETE' && url.pathname === '/users/session') {
|
|
|
|
|
sessions.delete (value)
|
|
|
|
|
response.writeHead (204, { 'Set-Cookie':
|
|
|
|
|
`${cookieName}=; Path=/; Max-Age=0; HttpOnly; Secure; SameSite=Lax` }).end ()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if (request.method === 'POST' && url.pathname === '/users/code/renew' && user != null) {
|
|
|
|
|
const code = `rotated-recovery-${sessionSequence}`
|
|
|
|
|
users.delete (session.code)
|
|
|
|
|
users.set (code, user)
|
|
|
|
|
sessions.delete (value)
|
|
|
|
|
issueSession (response, code)
|
|
|
|
|
response.end (JSON.stringify ({ code }))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
response.writeHead (404).end ()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const data = responseFor (url)
|
|
|
|
|
response.writeHead (failAPI ? 503 : (data == null ? 404 : 200), {
|
|
|
|
|
'Content-Type': 'application/json' })
|
|
|
|
|
response.writeHead (data == null ? 404 : 200)
|
|
|
|
|
response.end (JSON.stringify (data))
|
|
|
|
|
})
|
|
|
|
|
await new Promise (resolve => api.listen (0, '127.0.0.1', resolve))
|
|
|
|
|
apiURL = `http://127.0.0.1:${api.address ().port}`
|
|
|
|
|
node = spawn (process.execPath, ['server.mjs'], { env: {
|
|
|
|
|
...process.env, NODE_ENV: 'production', PORT: '0', HOST: '127.0.0.1',
|
|
|
|
|
PUBLIC_ORIGIN: origin, SSR_API_BASE_URL: apiURL,
|
|
|
|
|
SSR_COOKIE_SECRET: 'automated-check-only-not-a-production-secret' },
|
|
|
|
|
PUBLIC_ORIGIN: origin, SSR_API_BASE_URL: apiURL },
|
|
|
|
|
stdio: ['ignore', 'pipe', 'pipe'] })
|
|
|
|
|
httpOrigin = await new Promise ((resolve, reject) => {
|
|
|
|
|
node.stdout.on ('data', data => {
|
|
|
|
@@ -99,6 +192,22 @@ before (async () => {
|
|
|
|
|
routes = flatten (pageRoutes (null, () => {}))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const authenticatedCookie = async (code = recoveryCode) => {
|
|
|
|
|
const csrf = await fetch (`${httpOrigin}/api/users/csrf`)
|
|
|
|
|
assert.equal (csrf.status, 200)
|
|
|
|
|
const { token } = await csrf.json ()
|
|
|
|
|
const verified = await fetch (`${httpOrigin}/api/users/verify`, {
|
|
|
|
|
method: 'POST', headers: { Origin: origin, 'Content-Type': 'application/json',
|
|
|
|
|
Cookie: csrf.headers.get ('set-cookie').split (';')[0], 'X-CSRF-Token': token },
|
|
|
|
|
body: JSON.stringify ({ code, restore: true }) })
|
|
|
|
|
assert.equal (verified.status, 200)
|
|
|
|
|
assert.equal ((await verified.json ()).valid, true)
|
|
|
|
|
const cookie = verified.headers.get ('set-cookie')
|
|
|
|
|
assert.match (cookie, /HttpOnly; Secure; SameSite=Lax/)
|
|
|
|
|
assert.doesNotMatch (cookie, new RegExp (code))
|
|
|
|
|
return cookie.split (';')[0]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
after (async () => {
|
|
|
|
|
await vite?.close ()
|
|
|
|
|
node?.kill ()
|
|
|
|
@@ -107,13 +216,14 @@ after (async () => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test ('every canonical screen route returns page HTML without JavaScript', async t => {
|
|
|
|
|
const cookie = await authenticatedCookie ()
|
|
|
|
|
for (const route of routes) {
|
|
|
|
|
for (const locale of ['ja', 'ain-Kana']) {
|
|
|
|
|
const path = route.path.replace (':identifier', 'home').replace (/:id|:title/g, '1')
|
|
|
|
|
.replace ('*', 'unknown-screen')
|
|
|
|
|
const url = `/${locale}/${path}?from=2026-09-15&to=2026-09-15`
|
|
|
|
|
await t.test (`${locale} ${route.path}`, async () => {
|
|
|
|
|
const result = await render (url, origin, apiURL, { transferCode: 'test-secret' })
|
|
|
|
|
const result = await render (url, origin, apiURL, { cookie })
|
|
|
|
|
assert.ok (result != null)
|
|
|
|
|
if (route.handle?.redirectTo) {
|
|
|
|
|
assert.equal (result.status, 308)
|
|
|
|
@@ -124,7 +234,7 @@ test ('every canonical screen route returns page HTML without JavaScript', async
|
|
|
|
|
assert.match (result.head, /<title/)
|
|
|
|
|
assert.match (result.head, /rel="canonical"/)
|
|
|
|
|
assert.equal (result.locale, locale)
|
|
|
|
|
assert.doesNotMatch (result.html + result.bootstrap, /test-secret/)
|
|
|
|
|
assert.doesNotMatch (result.html + result.bootstrap, /test-recovery-secret|opaque-rails/)
|
|
|
|
|
if (route.path === 'wiki/:id/edit') assert.match (result.html, /<textarea/)
|
|
|
|
|
if (route.path === 'users/settings') assert.match (result.html, /SSR account/)
|
|
|
|
|
if (route.path === 'posts')
|
|
|
|
@@ -142,6 +252,7 @@ test ('every canonical screen route returns page HTML without JavaScript', async
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test ('parallel localized requests keep list data isolated', async () => {
|
|
|
|
|
const start = requests.length
|
|
|
|
|
const pages = await Promise.all (['ja', 'ain-Kana', 'ja', 'ain-Kana'].map (locale =>
|
|
|
|
|
render (`/${locale}/posts?limit=50&page=2`, origin, apiURL)))
|
|
|
|
|
for (const [index, page] of pages.entries ()) {
|
|
|
|
@@ -152,7 +263,7 @@ test ('parallel localized requests keep list data isolated', async () => {
|
|
|
|
|
assert.equal (state.locale, locale)
|
|
|
|
|
}
|
|
|
|
|
assert.ok (requests.some (request => request.url.includes ('page=2')))
|
|
|
|
|
assert.ok (requests.every (request => request.method === 'GET'))
|
|
|
|
|
assert.ok (requests.slice (start).every (request => request.method === 'GET'))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test ('unknown resources and legacy URLs have correct status', async () => {
|
|
|
|
@@ -164,26 +275,52 @@ test ('unknown resources and legacy URLs have correct status', async () => {
|
|
|
|
|
assert.equal (redirect.location, '/ja/help/home?edition=2026-09-15')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test ('HTTP cookie migration, CSRF protection, and API failure', async () => {
|
|
|
|
|
const rejected = await fetch (`${httpOrigin}/_ssr/session`, {
|
|
|
|
|
method: 'POST', headers: { Origin: 'https://other.invalid', 'X-Transfer-Code': 'test-secret' } })
|
|
|
|
|
assert.equal (rejected.status, 403)
|
|
|
|
|
const created = await fetch (`${httpOrigin}/_ssr/session`, {
|
|
|
|
|
method: 'POST', headers: { Origin: origin, 'X-Transfer-Code': 'test-secret' } })
|
|
|
|
|
assert.equal (created.status, 204)
|
|
|
|
|
const cookie = created.headers.get ('set-cookie')
|
|
|
|
|
assert.match (cookie, /HttpOnly; Secure; SameSite=Lax/)
|
|
|
|
|
assert.doesNotMatch (cookie, /test-secret/)
|
|
|
|
|
test ('HTTP forwards Rails cookies, CSRF, expiration and upstream errors', async () => {
|
|
|
|
|
const cookie = await authenticatedCookie ()
|
|
|
|
|
const settings = await fetch (`${httpOrigin}/ja/users/settings`, { headers: { Cookie: cookie } })
|
|
|
|
|
assert.equal (settings.status, 200)
|
|
|
|
|
const html = await settings.text ()
|
|
|
|
|
assert.match (html, /SSR account/)
|
|
|
|
|
assert.doesNotMatch (html, /test-secret/)
|
|
|
|
|
assert.doesNotMatch (html, /test-recovery-secret|opaque-rails-session/)
|
|
|
|
|
assert.match (settings.headers.get ('cache-control'), /private, no-store/)
|
|
|
|
|
assert.match (settings.headers.get ('set-cookie'), /Max-Age=630720000/)
|
|
|
|
|
assert.ok (requests.some (request => request.url === '/users/session'
|
|
|
|
|
&& request.cookie === cookie))
|
|
|
|
|
assert.ok (requests.every (request => request.transferCode == null))
|
|
|
|
|
assert.equal ((await fetch (`${httpOrigin}/ja/users/settings`)).status, 401)
|
|
|
|
|
const invalid = cookie.replace ('=', '=invalid')
|
|
|
|
|
assert.equal ((await fetch (`${httpOrigin}/ja/users/settings`, {
|
|
|
|
|
headers: { Cookie: invalid } })).status, 401)
|
|
|
|
|
const publicPage = await fetch (`${httpOrigin}/ain-Kana/posts`)
|
|
|
|
|
assert.equal (publicPage.status, 200)
|
|
|
|
|
assert.match (await publicPage.text (), /SSR post ain-Kana/)
|
|
|
|
|
assert.equal ((await fetch (`${httpOrigin}/_ssr/session`, { method: 'POST' })).status, 405)
|
|
|
|
|
|
|
|
|
|
const csrf = await fetch (`${httpOrigin}/api/users/csrf`, { headers: { Cookie: cookie } })
|
|
|
|
|
const { token } = await csrf.json ()
|
|
|
|
|
const write = (csrfToken, requestOrigin = origin) => fetch (
|
|
|
|
|
`${httpOrigin}/api/users/verify`, { method: 'POST',
|
|
|
|
|
headers: { Cookie: cookie, Origin: requestOrigin, 'Content-Type': 'application/json',
|
|
|
|
|
'X-CSRF-Token': csrfToken }, body: JSON.stringify ({ code: recoveryCode }) })
|
|
|
|
|
assert.equal ((await write ('invalid')).status, 403)
|
|
|
|
|
assert.equal ((await write (token, 'https://other.invalid')).status, 403)
|
|
|
|
|
assert.equal ((await fetch (`${httpOrigin}/ja/users/settings`, {
|
|
|
|
|
headers: { Cookie: cookie } })).status, 200)
|
|
|
|
|
const renewed = await write (token)
|
|
|
|
|
assert.equal (renewed.status, 200)
|
|
|
|
|
const renewedCookie = renewed.headers.get ('set-cookie').split (';')[0]
|
|
|
|
|
assert.notEqual (renewedCookie, cookie)
|
|
|
|
|
assert.equal ((await fetch (`${httpOrigin}/ja/users/settings`, {
|
|
|
|
|
headers: { Cookie: cookie } })).status, 401)
|
|
|
|
|
const renewedCSRF = await fetch (`${httpOrigin}/api/users/csrf`, {
|
|
|
|
|
headers: { Cookie: renewedCookie } })
|
|
|
|
|
const newToken = (await renewedCSRF.json ()).token
|
|
|
|
|
const logout = await fetch (`${httpOrigin}/api/users/session`, { method: 'DELETE',
|
|
|
|
|
headers: { Cookie: renewedCookie, Origin: origin, 'X-CSRF-Token': newToken } })
|
|
|
|
|
assert.equal (logout.status, 204)
|
|
|
|
|
assert.match (logout.headers.get ('set-cookie'), /Max-Age=0/)
|
|
|
|
|
assert.equal ((await fetch (`${httpOrigin}/ja/users/settings`, {
|
|
|
|
|
headers: { Cookie: renewedCookie } })).status, 401)
|
|
|
|
|
failAPI = true
|
|
|
|
|
try {
|
|
|
|
|
assert.equal ((await fetch (`${httpOrigin}/ja/posts`)).status, 503)
|
|
|
|
@@ -192,6 +329,43 @@ test ('HTTP cookie migration, CSRF protection, and API failure', async () => {
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test ('parallel user and Locale requests never share authentication or query state', async () => {
|
|
|
|
|
const cookies = await Promise.all ([authenticatedCookie (),
|
|
|
|
|
authenticatedCookie ('second-recovery-secret')])
|
|
|
|
|
const inputs = cookies.flatMap ((cookie, user) => ['ja', 'ain-Kana'].map (locale => ({
|
|
|
|
|
cookie, locale, name: user === 0 ? 'SSR account' : 'Second account',
|
|
|
|
|
other: user === 0 ? 'Second account' : 'SSR account' })))
|
|
|
|
|
const pages = await Promise.all (inputs.map (async input => {
|
|
|
|
|
const response = await fetch (`${httpOrigin}/${input.locale}/users/settings`, {
|
|
|
|
|
headers: { Cookie: input.cookie } })
|
|
|
|
|
assert.equal (response.status, 200)
|
|
|
|
|
return response.text ()
|
|
|
|
|
}))
|
|
|
|
|
for (const [index, html] of pages.entries ()) {
|
|
|
|
|
const input = inputs[index]
|
|
|
|
|
assert.match (html, new RegExp (input.name))
|
|
|
|
|
assert.doesNotMatch (html, new RegExp (input.other))
|
|
|
|
|
assert.match (html, new RegExp (`<html lang="${input.locale}"`))
|
|
|
|
|
assert.doesNotMatch (html, /recovery-secret|opaque-rails-session|csrf-/)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test ('renewed recovery codes invalidate old sessions and forward the new Rails cookie',
|
|
|
|
|
async () => {
|
|
|
|
|
const cookie = await authenticatedCookie ('second-recovery-secret')
|
|
|
|
|
const csrf = await fetch (`${httpOrigin}/api/users/csrf`, { headers: { Cookie: cookie } })
|
|
|
|
|
const { token } = await csrf.json ()
|
|
|
|
|
const renewed = await fetch (`${httpOrigin}/api/users/code/renew`, {
|
|
|
|
|
method: 'POST', headers: { Cookie: cookie, Origin: origin, 'X-CSRF-Token': token } })
|
|
|
|
|
assert.equal (renewed.status, 200)
|
|
|
|
|
const newCookie = renewed.headers.get ('set-cookie').split (';')[0]
|
|
|
|
|
assert.notEqual (newCookie, cookie)
|
|
|
|
|
assert.equal ((await fetch (`${httpOrigin}/ja/users/settings`, {
|
|
|
|
|
headers: { Cookie: cookie } })).status, 401)
|
|
|
|
|
assert.equal ((await fetch (`${httpOrigin}/ain-Kana/users/settings`, {
|
|
|
|
|
headers: { Cookie: newCookie } })).status, 200)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test ('hydrate every route with stored browser preferences', {
|
|
|
|
|
skip: !process.env.SSR_CHROME_PATH, timeout: 180_000,
|
|
|
|
|
}, async t => {
|
|
|
|
@@ -230,14 +404,11 @@ test ('hydrate every route with stored browser preferences', {
|
|
|
|
|
} else if (message.method === 'Fetch.requestPaused') {
|
|
|
|
|
const { requestId, request } = message.params
|
|
|
|
|
const url = new URL (request.url)
|
|
|
|
|
if (url.origin === httpOrigin && url.pathname !== '/_ssr/session') {
|
|
|
|
|
if (url.origin === httpOrigin) {
|
|
|
|
|
void send ('Fetch.continueRequest', { requestId }, message.sessionId)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const data = url.pathname === '/users/verify'
|
|
|
|
|
? { valid: true, user: { id: 1, name: 'SSR account', role: 'admin',
|
|
|
|
|
inheritance_code: 'test-secret' } }
|
|
|
|
|
: responseFor (url)
|
|
|
|
|
const data = responseFor (url)
|
|
|
|
|
void send ('Fetch.fulfillRequest', { requestId,
|
|
|
|
|
responseCode: request.method === 'OPTIONS' ? 204 : 200,
|
|
|
|
|
responseHeaders: [{ name: 'Content-Type', value: 'application/json' },
|
|
|
|
@@ -259,12 +430,11 @@ test ('hydrate every route with stored browser preferences', {
|
|
|
|
|
await send ('Page.enable', {}, sessionId)
|
|
|
|
|
await send ('Network.enable', {}, sessionId)
|
|
|
|
|
await send ('Fetch.enable', { patterns: [{ urlPattern: '*' }] }, sessionId)
|
|
|
|
|
const session = await fetch (`${httpOrigin}/_ssr/session`, {
|
|
|
|
|
method: 'POST', headers: { Origin: origin, 'X-Transfer-Code': 'test-secret' } })
|
|
|
|
|
const cookie = await authenticatedCookie ()
|
|
|
|
|
await send ('Network.setExtraHTTPHeaders', { headers: {
|
|
|
|
|
Cookie: session.headers.get ('set-cookie').split (';')[0] } }, sessionId)
|
|
|
|
|
Cookie: cookie } }, sessionId)
|
|
|
|
|
await send ('Page.addScriptToEvaluateOnNewDocument', { source: `
|
|
|
|
|
localStorage.setItem('user_code','test-secret');
|
|
|
|
|
localStorage.setItem('user_code','test-recovery-secret');
|
|
|
|
|
localStorage.setItem('btrc_hub.client_settings',JSON.stringify({
|
|
|
|
|
lists:{postList:{limit:50},tagList:{limit:50},postSearch:{limit:50}},
|
|
|
|
|
behavior:{animation:'off'},theatre:{layoutMode:'tagsBottom',tagFlow:'horizontal'}
|
|
|
|
|