mirror of
https://github.com/iflytek/skillhub.git
synced 2026-08-28 11:25:00 +00:00
Merge pull request #311 from iflytek/fix/public-skill-detail-anon
fix(web): allow anonymous access to public skill detail
This commit is contained in:
commit
89bc58d29e
3 changed files with 84 additions and 6 deletions
46
web/e2e/public-skill-detail-anonymous.spec.ts
Normal file
46
web/e2e/public-skill-detail-anonymous.spec.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { expect, test } from '@playwright/test'
|
||||
import { setEnglishLocale } from './helpers/auth-fixtures'
|
||||
import { getSearchCard, prepareSearchSeed, type PreparedSearchSeed } from './helpers/search-seed'
|
||||
|
||||
const SEARCH_URL = (q: string) => `/search?q=${encodeURIComponent(q)}&sort=relevance&page=0&starredOnly=false`
|
||||
|
||||
function latestSeed(seed: PreparedSearchSeed) {
|
||||
return {
|
||||
skill: seed.skills[seed.skills.length - 1],
|
||||
skillName: seed.skillNames[seed.skillNames.length - 1],
|
||||
}
|
||||
}
|
||||
|
||||
let seeded: PreparedSearchSeed | undefined
|
||||
|
||||
test.describe('Public Skill Detail Anonymous Access (Real API)', () => {
|
||||
test.beforeAll(async ({ browser }, testInfo) => {
|
||||
seeded = await prepareSearchSeed(browser, testInfo, { count: 1 })
|
||||
})
|
||||
|
||||
test.afterAll(async () => {
|
||||
await seeded?.dispose()
|
||||
seeded = undefined
|
||||
})
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await setEnglishLocale(page)
|
||||
})
|
||||
|
||||
test('allows anonymous users to open a public skill detail and view install content', async ({ page }) => {
|
||||
const current = latestSeed(seeded!)
|
||||
|
||||
await page.goto(SEARCH_URL(seeded!.keyword))
|
||||
const card = getSearchCard(page, current.skillName)
|
||||
await expect(card).toBeVisible({ timeout: 15_000 })
|
||||
|
||||
await card.click()
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`/space/${current.skill.namespace}/${current.skill.slug}$`))
|
||||
await expect(page).not.toHaveURL(/\/login\?returnTo=/)
|
||||
await expect(page.getByRole('heading', { name: current.skillName, exact: true })).toBeVisible()
|
||||
await expect(page.getByText('Install', { exact: true })).toBeVisible()
|
||||
await expect(page.getByText(new RegExp(`npx clawhub install ${current.skill.slug}`))).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: 'Copy' }).first()).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
|
@ -224,7 +224,6 @@ const namespaceRoute = createRoute({
|
|||
const skillDetailRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: '/space/$namespace/$slug',
|
||||
beforeLoad: requireAuth,
|
||||
validateSearch: (search: Record<string, unknown>): { returnTo?: string } => ({
|
||||
returnTo: typeof search.returnTo === 'string' && search.returnTo.startsWith('/') ? search.returnTo : undefined,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -2,10 +2,17 @@ import { renderToStaticMarkup } from 'react-dom/server'
|
|||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const navigateMock = vi.fn()
|
||||
const hasRoleMock = vi.fn((role: string) => role === 'USER')
|
||||
const hasRoleMock = vi.fn<(role: string) => boolean>((role: string) => role === 'USER')
|
||||
const useSkillDetailMock = vi.fn()
|
||||
const useSkillLabelsMock = vi.fn()
|
||||
const useSkillVersionsMock = vi.fn()
|
||||
let authState: {
|
||||
user: { userId: string; platformRoles: string[] } | null
|
||||
hasRole: (role: string) => boolean
|
||||
} = {
|
||||
user: { userId: 'owner-1', platformRoles: ['USER'] },
|
||||
hasRole: hasRoleMock,
|
||||
}
|
||||
|
||||
vi.mock('@tanstack/react-router', () => ({
|
||||
useNavigate: () => navigateMock,
|
||||
|
|
@ -32,10 +39,7 @@ vi.mock('@tanstack/react-query', () => ({
|
|||
}))
|
||||
|
||||
vi.mock('@/features/auth/use-auth', () => ({
|
||||
useAuth: () => ({
|
||||
user: { userId: 'owner-1', platformRoles: ['USER'] },
|
||||
hasRole: hasRoleMock,
|
||||
}),
|
||||
useAuth: () => authState,
|
||||
}))
|
||||
|
||||
vi.mock('@/features/report/use-skill-reports', () => ({
|
||||
|
|
@ -165,6 +169,10 @@ describe('SkillDetailPage', () => {
|
|||
beforeEach(() => {
|
||||
navigateMock.mockReset()
|
||||
hasRoleMock.mockImplementation((role: string) => role === 'USER')
|
||||
authState = {
|
||||
user: { userId: 'owner-1', platformRoles: ['USER'] },
|
||||
hasRole: hasRoleMock,
|
||||
}
|
||||
useSkillDetailMock.mockReturnValue({
|
||||
data: createSkill(),
|
||||
isLoading: false,
|
||||
|
|
@ -208,6 +216,31 @@ describe('SkillDetailPage', () => {
|
|||
expect(html).not.toContain('skillDetail.deleteSkill')
|
||||
})
|
||||
|
||||
it('renders public skill details for an anonymous viewer', () => {
|
||||
authState = {
|
||||
user: null,
|
||||
hasRole: vi.fn(() => false),
|
||||
}
|
||||
|
||||
useSkillDetailMock.mockReturnValue({
|
||||
data: createSkill({
|
||||
canManageLifecycle: false,
|
||||
canInteract: true,
|
||||
visibility: 'PUBLIC',
|
||||
}),
|
||||
isLoading: false,
|
||||
isFetching: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const html = renderToStaticMarkup(<SkillDetailPage />)
|
||||
|
||||
expect(html).toContain('Demo Skill')
|
||||
expect(html).toContain('install')
|
||||
expect(html).not.toContain('skillDetail.loginRequired')
|
||||
expect(html).not.toContain('skillDetail.deleteSkill')
|
||||
})
|
||||
|
||||
it('shows the label management panel for a user who can manage the skill lifecycle', () => {
|
||||
useSkillDetailMock.mockReturnValue({
|
||||
data: createSkill({
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue