mirror of
https://github.com/langgenius/dify.git
synced 2026-07-18 07:44:36 -04:00
test(web): align auth e2e with console home (#38538)
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
@auth @core @authenticated
|
||||
Feature: Console session refresh
|
||||
|
||||
Scenario: Refresh the console session during server-side navigation
|
||||
Given I am signed in as the default E2E admin
|
||||
And my console session requires token refresh
|
||||
When I open the default console entry after the access token expires
|
||||
Then I should be on the console home
|
||||
And I should not see the "Sign in" button
|
||||
@@ -1,8 +1,8 @@
|
||||
@auth @smoke @core @unauthenticated
|
||||
Feature: Sign in
|
||||
|
||||
Scenario: Sign in with valid credentials and reach the apps console
|
||||
Scenario: Sign in with valid credentials and reach the console home
|
||||
Given I am not signed in
|
||||
When I open the sign-in page
|
||||
And I sign in as the default E2E admin
|
||||
Then I should be on the apps console
|
||||
Then I should be on the console home
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@smoke @authenticated
|
||||
Feature: Authenticated app console
|
||||
Scenario: Open the apps console with the shared authenticated state
|
||||
Feature: Authenticated console home
|
||||
Scenario: Open the default console entry with the shared authenticated state
|
||||
Given I am signed in as the default E2E admin
|
||||
When I open the apps console
|
||||
Then I should stay on the apps console
|
||||
When I open the default console entry
|
||||
Then I should be on the console home
|
||||
And I should not see the "Sign in" button
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@smoke @unauthenticated
|
||||
Feature: Unauthenticated app console entry
|
||||
Scenario: Redirect to the sign-in page when opening the apps console without logging in
|
||||
Feature: Unauthenticated console home entry
|
||||
Scenario: Redirect to the sign-in page when opening the default console entry without logging in
|
||||
Given I am not signed in
|
||||
When I open the apps console
|
||||
When I open the default console entry
|
||||
Then I should be redirected to the signin page
|
||||
And I should see the "Sign in" button
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { DifyWorld } from '../../support/world'
|
||||
import { Given, When } from '@cucumber/cucumber'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
const consoleAccessTokenCookieName = /^(?:__Host-)?access_token$/
|
||||
const consoleRefreshTokenCookieName = /^(?:__Host-)?refresh_token$/
|
||||
|
||||
Given('my console session requires token refresh', async function (this: DifyWorld) {
|
||||
if (!this.context)
|
||||
throw new Error('Playwright browser context has not been initialized for this scenario.')
|
||||
|
||||
const cookies = await this.context.cookies()
|
||||
const hasAccessToken = cookies.some(cookie => consoleAccessTokenCookieName.test(cookie.name))
|
||||
const hasRefreshToken = cookies.some(cookie => consoleRefreshTokenCookieName.test(cookie.name))
|
||||
|
||||
expect(hasAccessToken, 'Expected the authenticated E2E session to include a console access token.').toBe(true)
|
||||
expect(hasRefreshToken, 'Expected the authenticated E2E session to include a console refresh token.').toBe(true)
|
||||
|
||||
await this.context.clearCookies({ name: consoleAccessTokenCookieName })
|
||||
|
||||
const remainingCookies = await this.context.cookies()
|
||||
expect(
|
||||
remainingCookies.some(cookie => consoleAccessTokenCookieName.test(cookie.name)),
|
||||
'Expected the console access token to be removed before opening the default console entry.',
|
||||
).toBe(false)
|
||||
expect(
|
||||
remainingCookies.some(cookie => consoleRefreshTokenCookieName.test(cookie.name)),
|
||||
'Expected the console refresh token to remain available for server-side refresh.',
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
When('I open the default console entry after the access token expires', async function (this: DifyWorld) {
|
||||
const page = this.getPage()
|
||||
const refreshRequestPromise = page.waitForRequest((request) => {
|
||||
const url = new URL(request.url())
|
||||
return url.pathname.endsWith('/auth/refresh') && url.searchParams.get('redirect_url') === '/'
|
||||
})
|
||||
|
||||
await page.goto('/')
|
||||
|
||||
const refreshRequest = await refreshRequestPromise
|
||||
this.attach(`Session refresh request: ${refreshRequest.url()}`, 'text/plain')
|
||||
})
|
||||
@@ -1,10 +1,9 @@
|
||||
import type { DifyWorld } from '../../support/world'
|
||||
import { Then, When } from '@cucumber/cucumber'
|
||||
import { expect } from '@playwright/test'
|
||||
import { When } from '@cucumber/cucumber'
|
||||
import { adminCredentials } from '../../../fixtures/auth'
|
||||
|
||||
When('I open the sign-in page', async function (this: DifyWorld) {
|
||||
await this.getPage().goto('/signin?redirect_url=%2Fapps')
|
||||
await this.getPage().goto('/signin')
|
||||
})
|
||||
|
||||
When('I sign in as the default E2E admin', async function (this: DifyWorld) {
|
||||
@@ -14,7 +13,3 @@ When('I sign in as the default E2E admin', async function (this: DifyWorld) {
|
||||
await page.getByLabel('Password', { exact: true }).fill(adminCredentials.password)
|
||||
await page.getByRole('button', { name: 'Sign in' }).click()
|
||||
})
|
||||
|
||||
Then('I should be on the apps console', async function (this: DifyWorld) {
|
||||
await expect(this.getPage()).toHaveURL(/\/apps(?:\?.*)?$/, { timeout: 30_000 })
|
||||
})
|
||||
|
||||
@@ -2,6 +2,11 @@ import type { DifyWorld } from '../../support/world'
|
||||
import { Then, When } from '@cucumber/cucumber'
|
||||
import { expect } from '@playwright/test'
|
||||
import { waitForAppsConsole } from '../../../support/apps'
|
||||
import { waitForConsoleHome } from '../../../support/home'
|
||||
|
||||
When('I open the default console entry', async function (this: DifyWorld) {
|
||||
await this.getPage().goto('/')
|
||||
})
|
||||
|
||||
When('I open the apps console', async function (this: DifyWorld) {
|
||||
await this.getPage().goto('/apps')
|
||||
@@ -15,6 +20,10 @@ Then('I should stay on the apps console', async function (this: DifyWorld) {
|
||||
await waitForAppsConsole(this.getPage())
|
||||
})
|
||||
|
||||
Then('I should be on the console home', async function (this: DifyWorld) {
|
||||
await waitForConsoleHome(this.getPage())
|
||||
})
|
||||
|
||||
Then('I should be redirected to the signin page', async function (this: DifyWorld) {
|
||||
await expect(this.getPage()).toHaveURL(/\/signin(?:\?.*)?$/)
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Buffer } from 'node:buffer'
|
||||
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { waitForAppsConsole } from '../support/apps'
|
||||
import { waitForConsoleHome } from '../support/home'
|
||||
import { apiURL, defaultBaseURL, defaultLocale } from '../test-env'
|
||||
|
||||
export type AuthSessionMetadata = {
|
||||
@@ -150,12 +150,12 @@ export const ensureAuthenticatedState = async (browser: Browser, configuredBaseU
|
||||
const { mode, usedInitPassword } = await ensureAdminAccount(context, deadline)
|
||||
await loginAdmin(context, deadline)
|
||||
|
||||
console.warn('[e2e] auth bootstrap: verifying apps console')
|
||||
await page.goto(appURL(baseURL, '/apps'), {
|
||||
console.warn('[e2e] auth bootstrap: verifying console home')
|
||||
await page.goto(appURL(baseURL, '/'), {
|
||||
timeout: getRemainingTimeout(deadline),
|
||||
waitUntil: 'domcontentloaded',
|
||||
})
|
||||
await waitForAppsConsole(page, getRemainingTimeout(deadline))
|
||||
await waitForConsoleHome(page, getRemainingTimeout(deadline))
|
||||
|
||||
await context.storageState({ path: authStatePath })
|
||||
|
||||
|
||||
+7
-2
@@ -1,10 +1,15 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
const getExpectOptions = (timeout?: number) =>
|
||||
timeout === undefined ? undefined : { timeout }
|
||||
|
||||
export const waitForAppsConsole = async (page: Page, timeout?: number) => {
|
||||
await expect(page).toHaveURL(/\/apps(?:\?.*)?$/, timeout === undefined ? undefined : { timeout })
|
||||
const options = getExpectOptions(timeout)
|
||||
|
||||
await expect(page).toHaveURL(/\/apps(?:\?.*)?$/, options)
|
||||
await expect(page.getByRole('heading', { name: 'Studio' })).toBeVisible(
|
||||
timeout === undefined ? undefined : { timeout },
|
||||
options,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
const getExpectOptions = (timeout?: number) =>
|
||||
timeout === undefined ? undefined : { timeout }
|
||||
|
||||
export const waitForConsoleHome = async (page: Page, timeout?: number) => {
|
||||
const options = getExpectOptions(timeout)
|
||||
|
||||
await expect.poll(() => new URL(page.url()).pathname, options).toBe('/')
|
||||
await expect(page.getByRole('link', { name: 'Home' })).toHaveAttribute('aria-current', 'page', options)
|
||||
}
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
basePath: '',
|
||||
}))
|
||||
|
||||
vi.mock('@/config', () => ({
|
||||
API_PREFIX: 'http://localhost:5001/console/api',
|
||||
CSRF_COOKIE_NAME: () => 'csrf_token',
|
||||
@@ -15,7 +19,9 @@ vi.mock('@/config/server', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/var', () => ({
|
||||
basePath: '',
|
||||
get basePath() {
|
||||
return mocks.basePath
|
||||
},
|
||||
}))
|
||||
|
||||
const getSetCookieHeaders = (headers: Headers) => {
|
||||
@@ -38,7 +44,9 @@ const createRequest = (url: string, cookie?: string) => ({
|
||||
describe('auth refresh route', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.resetModules()
|
||||
vi.unstubAllGlobals()
|
||||
mocks.basePath = ''
|
||||
})
|
||||
|
||||
it('should refresh cookies and redirect back to the requested path', async () => {
|
||||
@@ -131,4 +139,47 @@ describe('auth refresh route', () => {
|
||||
expect(response.status).toBe(303)
|
||||
expect(response.headers.get('location')).toBe('/signin?redirect_url=%2F')
|
||||
})
|
||||
|
||||
it('should preserve base path when refreshing and redirecting back', async () => {
|
||||
mocks.basePath = '/console'
|
||||
const headers = new Headers()
|
||||
Object.defineProperty(headers, 'getSetCookie', {
|
||||
value: () => [
|
||||
'access_token=new-access; Path=/console; HttpOnly',
|
||||
'refresh_token=new-refresh; Path=/console; HttpOnly',
|
||||
],
|
||||
})
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
headers,
|
||||
} as Response)
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
const { GET } = await import('../route')
|
||||
|
||||
const response = await GET(createRequest(
|
||||
'http://localhost:3000/console/auth/refresh?redirect_url=%2Fconsole%2Fapps%3Fcategory%3Dworkflow',
|
||||
'refresh_token=old-refresh',
|
||||
))
|
||||
|
||||
expect(response.status).toBe(303)
|
||||
expect(response.headers.get('location')).toBe('/console/apps?category=workflow')
|
||||
expect(getSetCookieHeaders(response.headers)).toEqual([
|
||||
'access_token=new-access; Path=/console; HttpOnly',
|
||||
'refresh_token=new-refresh; Path=/console; HttpOnly',
|
||||
])
|
||||
})
|
||||
|
||||
it('should fall back to the base path home when base path refresh redirects to itself', async () => {
|
||||
mocks.basePath = '/console'
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(null, { status: 401 })))
|
||||
const { GET } = await import('../route')
|
||||
|
||||
const response = await GET(createRequest(
|
||||
'http://localhost:3000/console/auth/refresh?redirect_url=%2Fconsole%2Fauth%2Frefresh',
|
||||
'refresh_token=expired',
|
||||
))
|
||||
|
||||
expect(response.status).toBe(303)
|
||||
expect(response.headers.get('location')).toBe('/console/signin?redirect_url=%2Fconsole%2F')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -41,10 +41,6 @@ vi.mock('@/service/common', async () => {
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('./utils/post-login-redirect', () => ({
|
||||
resolvePostLoginRedirect: vi.fn(() => null),
|
||||
}))
|
||||
|
||||
const mockReplace = vi.fn()
|
||||
const mockUseQuery = vi.mocked(useQuery)
|
||||
const mockUseSuspenseQuery = vi.mocked(useSuspenseQuery)
|
||||
@@ -74,11 +70,17 @@ const invitationQueryResult = {
|
||||
},
|
||||
}
|
||||
|
||||
const nonInviteQueryResult = {
|
||||
isPending: false,
|
||||
isError: false,
|
||||
data: undefined,
|
||||
}
|
||||
|
||||
describe('NormalForm', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockUseRouter.mockReturnValue({ replace: mockReplace })
|
||||
mockUseSearchParams.mockReturnValue(new URLSearchParams('invite_token=invite-token'))
|
||||
mockUseSearchParams.mockReturnValue(new URLSearchParams())
|
||||
mockUseSuspenseQuery.mockReturnValue({
|
||||
data: {
|
||||
enable_social_oauth_login: false,
|
||||
@@ -97,8 +99,25 @@ describe('NormalForm', () => {
|
||||
} as unknown as ReturnType<typeof useSuspenseQuery>)
|
||||
})
|
||||
|
||||
describe('Default Redirects', () => {
|
||||
it('should send logged-in visitors without a redirect target to the console home', async () => {
|
||||
const searchParams = new URLSearchParams()
|
||||
mockUseSearchParams.mockReturnValue(searchParams)
|
||||
mockUseQuery
|
||||
.mockReturnValueOnce(loggedInQueryResult as unknown as ReturnType<typeof useQuery>)
|
||||
.mockReturnValueOnce(nonInviteQueryResult as unknown as ReturnType<typeof useQuery>)
|
||||
|
||||
render(<NormalForm />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockReplace).toHaveBeenCalledWith('/')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Invite Redirects', () => {
|
||||
it('should send logged-in invite visitors to the invite confirmation page', async () => {
|
||||
mockUseSearchParams.mockReturnValue(new URLSearchParams('invite_token=invite-token'))
|
||||
mockUseQuery
|
||||
.mockReturnValueOnce(loggedInQueryResult as unknown as ReturnType<typeof useQuery>)
|
||||
.mockReturnValueOnce(invitationQueryResult as unknown as ReturnType<typeof useQuery>)
|
||||
|
||||
Reference in New Issue
Block a user