docs: 交付文档、维护指引与端到端回归
补齐 README、开发计划、维护交接与 E2E 用例,执行最终验收。
This commit is contained in:
1 parent
5be5ca3376
commit
308332eb94
216 files changed
+37038
No files matched your search
@@ -0,0 +1,38 @@
|
||||
import { test, expect, storageStatePath } from '@e2e'
|
||||
|
||||
test.describe('signed out', () => {
|
||||
test.use({ storageState: { cookies: [], origins: [] } })
|
||||
|
||||
test('admin signs in and lands on the dashboard @smoke @auth', async ({ loginPage, org, page }) => {
|
||||
await loginPage.goto()
|
||||
await loginPage.signIn(org.admin.email, org.admin.password)
|
||||
await expect(page).toHaveURL(/\/ai-agents/)
|
||||
})
|
||||
|
||||
test('a wrong password keeps you on the sign-in page with an error @auth', async ({ loginPage, org, page }) => {
|
||||
await loginPage.goto()
|
||||
await loginPage.signIn(org.admin.email, 'not-the-password')
|
||||
await expect(loginPage.error).toHaveText('Incorrect email or password')
|
||||
await expect(page).toHaveURL(/\/login/)
|
||||
})
|
||||
|
||||
test('signing out returns to the sign-in page @auth', async ({ loginPage, dashboard, org, page }) => {
|
||||
// A fresh session, not the shared admin state — logging out must not affect other tests.
|
||||
await loginPage.goto()
|
||||
await loginPage.signIn(org.admin.email, org.admin.password)
|
||||
await expect(page).toHaveURL(/\/ai-agents/)
|
||||
await dashboard.logout()
|
||||
await page.goto('/conversations')
|
||||
await expect(page).toHaveURL(/\/login/)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('as a human agent', () => {
|
||||
// A path, not a read: spec files load before the setup project writes the state.
|
||||
test.use({ storageState: storageStatePath('human-agent') })
|
||||
|
||||
test('the Agent role cannot open AI settings @auth', async ({ dashboard, page }) => {
|
||||
await page.goto('/settings/ai-config')
|
||||
await expect(dashboard.accessDenied).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,88 @@
|
||||
import { test, expect, env, ApiClient, createUser, login, unique, type APIRequestContext } from '@e2e'
|
||||
|
||||
// Sessions end on the server, not just in the browser: a copied token stops
|
||||
// working the moment its session is signed out, replaced or deactivated.
|
||||
|
||||
const PASSWORD = 'E2e-Session-Passw0rd!'
|
||||
|
||||
interface Device { api: ApiClient; accessToken: string; refreshToken: string }
|
||||
|
||||
test.describe('sessions', () => {
|
||||
let contexts: APIRequestContext[] = []
|
||||
let newContext: () => Promise<APIRequestContext>
|
||||
|
||||
test.beforeEach(({ playwright }) => {
|
||||
newContext = async () => {
|
||||
const context = await playwright.request.newContext()
|
||||
contexts.push(context)
|
||||
return context
|
||||
}
|
||||
})
|
||||
|
||||
test.afterEach(async () => {
|
||||
await Promise.all(contexts.map((c) => c.dispose()))
|
||||
contexts = []
|
||||
})
|
||||
|
||||
/** Signs `email` in on a browser-like context of its own, as one device would. */
|
||||
async function signIn(email: string, password = PASSWORD): Promise<Device> {
|
||||
const context = await newContext()
|
||||
const { token } = await login(new ApiClient(context), email, password)
|
||||
const refreshToken = (await context.storageState()).cookies.find((c) => c.name === 'refresh_token')!.value
|
||||
return { api: new ApiClient(context, token), accessToken: token, refreshToken }
|
||||
}
|
||||
|
||||
/** What a stolen copy of the access token gets: a bare Bearer request, no cookies, no renewal. */
|
||||
async function withStolen(accessToken: string): Promise<number> {
|
||||
return (await new ApiClient(await newContext(), accessToken).raw('GET', '/chats/recent')).status()
|
||||
}
|
||||
|
||||
/** What a stolen copy of the refresh cookie gets from /users/refresh. */
|
||||
async function refreshWithStolen(refreshToken: string): Promise<number> {
|
||||
const response = await (await newContext()).post(`${env.apiUrl}/users/refresh`, { headers: { Cookie: `refresh_token=${refreshToken}` } })
|
||||
return response.status()
|
||||
}
|
||||
|
||||
async function newTeammate(adminApi: ApiClient): Promise<{ id: string; email: string }> {
|
||||
const email = `${unique('session')}@e2e.example.com`
|
||||
const user = await createUser(adminApi, { name: 'Session Teammate', email, password: PASSWORD, role: 'Agent' })
|
||||
return { id: user.id, email }
|
||||
}
|
||||
|
||||
test('signing out ends the session: copies of both tokens are refused @auth @security', async ({ adminApi }) => {
|
||||
const device = await signIn((await newTeammate(adminApi)).email)
|
||||
expect(await withStolen(device.accessToken)).toBe(200)
|
||||
|
||||
await device.api.post('/users/logout')
|
||||
|
||||
expect(await withStolen(device.accessToken)).toBe(401)
|
||||
expect(await refreshWithStolen(device.refreshToken)).toBe(401)
|
||||
})
|
||||
|
||||
test('a refresh token is not accepted as an access token @auth @security', async ({ adminApi }) => {
|
||||
const device = await signIn((await newTeammate(adminApi)).email)
|
||||
expect(await withStolen(device.refreshToken)).toBe(401)
|
||||
})
|
||||
|
||||
test('deactivating a teammate cuts them off at once @auth @security', async ({ adminApi }) => {
|
||||
const teammate = await newTeammate(adminApi)
|
||||
const device = await signIn(teammate.email)
|
||||
|
||||
await adminApi.put(`/users/${teammate.id}`, { data: { is_active: false } })
|
||||
|
||||
expect(await withStolen(device.accessToken)).toBe(401)
|
||||
expect(await refreshWithStolen(device.refreshToken)).toBe(401)
|
||||
})
|
||||
|
||||
test('changing your password signs out your other devices, not this one @auth @security', async ({ adminApi }) => {
|
||||
const { email } = await newTeammate(adminApi)
|
||||
const laptop = await signIn(email)
|
||||
const phone = await signIn(email)
|
||||
|
||||
await laptop.api.patch('/users/me', { data: { current_password: PASSWORD, password: 'E2e-Session-Passw0rd-2!' } })
|
||||
|
||||
expect(await withStolen(laptop.accessToken)).toBe(200)
|
||||
expect(await withStolen(phone.accessToken)).toBe(401)
|
||||
expect(await refreshWithStolen(phone.refreshToken)).toBe(401)
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user