131 lines
4.2 KiB
TypeScript
131 lines
4.2 KiB
TypeScript
import {test, expect} from '../../support/fixtures'
|
|
import {UserFactory} from '../../factories/user'
|
|
import {setupApiUrl} from '../../support/authenticateUser'
|
|
import {TEST_PASSWORD} from '../../support/constants'
|
|
|
|
async function loginViaBrowser(page, username: string) {
|
|
await setupApiUrl(page)
|
|
|
|
await page.goto('/login')
|
|
await page.locator('input[id=username]').fill(username)
|
|
await page.locator('input[id=password]').fill(TEST_PASSWORD)
|
|
await page.locator('.button').filter({hasText: 'Login'}).click()
|
|
await expect(page).toHaveURL('/')
|
|
await expect(page.locator('main h1')).toContainText(username)
|
|
|
|
// Wait for the proactive refresh (from useRenewTokenOnFocus) to complete
|
|
// so it doesn't race with our test assertions.
|
|
await page.waitForTimeout(1500)
|
|
}
|
|
|
|
test.describe('Session refresh and retry interceptor', () => {
|
|
let username: string
|
|
|
|
test.beforeEach(async ({apiContext}) => {
|
|
const [user] = await UserFactory.create(1)
|
|
username = user.username
|
|
})
|
|
|
|
test('Transparently retries a request and rotates the JWT when a 401 with code 11 is returned', async ({page}) => {
|
|
await loginViaBrowser(page, username)
|
|
|
|
const tokenBefore = await page.evaluate(() => localStorage.getItem('token'))
|
|
expect(tokenBefore).not.toBeNull()
|
|
|
|
// Spy on the refresh endpoint
|
|
let refreshCalled = false
|
|
await page.route(/\/api\/v1\/user\/token\/refresh$/, async (route) => {
|
|
refreshCalled = true
|
|
await route.continue()
|
|
})
|
|
|
|
// Intercept the first GET to /user/sessions with 401 code 11.
|
|
// We navigate to the sessions page to trigger this call, avoiding
|
|
// a race with the proactive refresh that fires on page reload.
|
|
let intercepted = false
|
|
await page.route(/\/api\/v1\/user\/sessions/, async (route) => {
|
|
if (!intercepted && route.request().method() === 'GET') {
|
|
intercepted = true
|
|
await route.fulfill({
|
|
status: 401,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
code: 11,
|
|
message: 'missing, malformed, expired or otherwise invalid token provided',
|
|
}),
|
|
})
|
|
} else {
|
|
await route.continue()
|
|
}
|
|
})
|
|
|
|
await page.goto('/user/settings/sessions')
|
|
|
|
// The sessions page should load after transparent retry
|
|
await expect(page.locator('.tag.is-primary')).toBeVisible({timeout: 10000})
|
|
|
|
expect(intercepted).toBe(true)
|
|
expect(refreshCalled).toBe(true)
|
|
|
|
// The JWT in localStorage should have been rotated
|
|
const tokenAfter = await page.evaluate(() => localStorage.getItem('token'))
|
|
expect(tokenAfter).not.toBeNull()
|
|
expect(tokenAfter).not.toBe(tokenBefore)
|
|
})
|
|
|
|
test('Does not retry 401 with non-JWT error code', async ({page}) => {
|
|
await loginViaBrowser(page, username)
|
|
|
|
// Track refresh calls that happen AFTER the fake 401 is returned.
|
|
// The proactive refresh from useRenewTokenOnFocus fires on every page
|
|
// load, so we only care about refreshes triggered after our 401.
|
|
let projectsFailed = false
|
|
let refreshAfterFailure = false
|
|
|
|
await page.route(/\/api\/v1\/user\/token\/refresh$/, async (route) => {
|
|
if (projectsFailed) {
|
|
refreshAfterFailure = true
|
|
}
|
|
await route.continue()
|
|
})
|
|
|
|
// Return 401 with a non-JWT error code for all project GETs.
|
|
await page.route(/\/api\/v1\/projects(\?|$)/, async (route) => {
|
|
if (route.request().method() === 'GET') {
|
|
projectsFailed = true
|
|
await route.fulfill({
|
|
status: 401,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
code: 1002,
|
|
message: 'Wrong username or password.',
|
|
}),
|
|
})
|
|
} else {
|
|
await route.continue()
|
|
}
|
|
})
|
|
|
|
await page.reload()
|
|
// Wait for the proactive refresh (from useRenewTokenOnFocus) that fires
|
|
// on every page load to complete, then reset our flag so only
|
|
// interceptor-triggered refreshes are tracked.
|
|
await page.waitForTimeout(2000)
|
|
refreshAfterFailure = false
|
|
|
|
await page.waitForTimeout(1000)
|
|
|
|
// The interceptor should NOT have triggered a refresh for a non-JWT 401
|
|
expect(refreshAfterFailure).toBe(false)
|
|
})
|
|
|
|
test('Current session appears on the sessions settings page', async ({page}) => {
|
|
await loginViaBrowser(page, username)
|
|
|
|
await page.goto('/user/settings/sessions')
|
|
|
|
// The sessions table should have at least one row with the "Current" tag
|
|
await expect(page.locator('.tag.is-primary')).toBeVisible({timeout: 5000})
|
|
})
|
|
})
|