mirror of https://github.com/langgenius/dify.git
319 lines
12 KiB
TypeScript
319 lines
12 KiB
TypeScript
/**
|
|
* Integration test: Education Verification Flow
|
|
*
|
|
* Tests the education plan verification flow in PlanComp:
|
|
* PlanComp → handleVerify → useEducationVerify → router.push → education-apply
|
|
* PlanComp → handleVerify → error → show VerifyStateModal
|
|
*
|
|
* Also covers education button visibility based on context flags.
|
|
*/
|
|
import type { UsagePlanInfo, UsageResetInfo } from '@/app/components/billing/type'
|
|
import { cleanup, render, screen, waitFor } from '@testing-library/react'
|
|
import userEvent from '@testing-library/user-event'
|
|
import * as React from 'react'
|
|
import { defaultPlan } from '@/app/components/billing/config'
|
|
import PlanComp from '@/app/components/billing/plan'
|
|
import { Plan } from '@/app/components/billing/type'
|
|
|
|
// ─── Mock state ──────────────────────────────────────────────────────────────
|
|
let mockProviderCtx: Record<string, unknown> = {}
|
|
let mockAppCtx: Record<string, unknown> = {}
|
|
const mockSetShowPricingModal = vi.fn()
|
|
const mockSetShowAccountSettingModal = vi.fn()
|
|
const mockRouterPush = vi.fn()
|
|
const mockMutateAsync = vi.fn()
|
|
|
|
// ─── Context mocks ───────────────────────────────────────────────────────────
|
|
vi.mock('@/context/provider-context', () => ({
|
|
useProviderContext: () => mockProviderCtx,
|
|
}))
|
|
|
|
vi.mock('@/context/app-context', () => ({
|
|
useAppContext: () => mockAppCtx,
|
|
}))
|
|
|
|
vi.mock('@/context/modal-context', () => ({
|
|
useModalContext: () => ({
|
|
setShowPricingModal: mockSetShowPricingModal,
|
|
}),
|
|
useModalContextSelector: (selector: (s: Record<string, unknown>) => unknown) =>
|
|
selector({
|
|
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
|
}),
|
|
}))
|
|
|
|
vi.mock('@/context/i18n', () => ({
|
|
useGetLanguage: () => 'en-US',
|
|
}))
|
|
|
|
// ─── Service mocks ───────────────────────────────────────────────────────────
|
|
vi.mock('@/service/use-education', () => ({
|
|
useEducationVerify: () => ({
|
|
mutateAsync: mockMutateAsync,
|
|
isPending: false,
|
|
}),
|
|
}))
|
|
|
|
vi.mock('@/service/use-billing', () => ({
|
|
useBillingUrl: () => ({
|
|
data: 'https://billing.example.com',
|
|
isFetching: false,
|
|
refetch: vi.fn(),
|
|
}),
|
|
}))
|
|
|
|
// ─── Navigation mocks ───────────────────────────────────────────────────────
|
|
vi.mock('next/navigation', () => ({
|
|
useRouter: () => ({ push: mockRouterPush }),
|
|
usePathname: () => '/billing',
|
|
useSearchParams: () => new URLSearchParams(),
|
|
}))
|
|
|
|
vi.mock('@/hooks/use-async-window-open', () => ({
|
|
useAsyncWindowOpen: () => vi.fn(),
|
|
}))
|
|
|
|
// ─── External component mocks ───────────────────────────────────────────────
|
|
vi.mock('@/app/education-apply/verify-state-modal', () => ({
|
|
default: ({ isShow, title, content, email, showLink }: {
|
|
isShow: boolean
|
|
title?: string
|
|
content?: string
|
|
email?: string
|
|
showLink?: boolean
|
|
}) =>
|
|
isShow
|
|
? (
|
|
<div data-testid="verify-state-modal">
|
|
{title && <span data-testid="modal-title">{title}</span>}
|
|
{content && <span data-testid="modal-content">{content}</span>}
|
|
{email && <span data-testid="modal-email">{email}</span>}
|
|
{showLink && <span data-testid="modal-show-link">link</span>}
|
|
</div>
|
|
)
|
|
: null,
|
|
}))
|
|
|
|
// ─── Test data factories ────────────────────────────────────────────────────
|
|
type PlanOverrides = {
|
|
type?: string
|
|
usage?: Partial<UsagePlanInfo>
|
|
total?: Partial<UsagePlanInfo>
|
|
reset?: Partial<UsageResetInfo>
|
|
}
|
|
|
|
const createPlanData = (overrides: PlanOverrides = {}) => ({
|
|
...defaultPlan,
|
|
...overrides,
|
|
type: overrides.type ?? defaultPlan.type,
|
|
usage: { ...defaultPlan.usage, ...overrides.usage },
|
|
total: { ...defaultPlan.total, ...overrides.total },
|
|
reset: { ...defaultPlan.reset, ...overrides.reset },
|
|
})
|
|
|
|
const setupContexts = (
|
|
planOverrides: PlanOverrides = {},
|
|
providerOverrides: Record<string, unknown> = {},
|
|
appOverrides: Record<string, unknown> = {},
|
|
) => {
|
|
mockProviderCtx = {
|
|
plan: createPlanData(planOverrides),
|
|
enableBilling: true,
|
|
isFetchedPlan: true,
|
|
enableEducationPlan: false,
|
|
isEducationAccount: false,
|
|
allowRefreshEducationVerify: false,
|
|
...providerOverrides,
|
|
}
|
|
mockAppCtx = {
|
|
isCurrentWorkspaceManager: true,
|
|
userProfile: { email: 'student@university.edu' },
|
|
langGeniusVersionInfo: { current_version: '1.0.0' },
|
|
...appOverrides,
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
describe('Education Verification Flow', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
cleanup()
|
|
setupContexts()
|
|
})
|
|
|
|
// ─── 1. Education Button Visibility ─────────────────────────────────────
|
|
describe('Education button visibility', () => {
|
|
it('should not show verify button when enableEducationPlan is false', () => {
|
|
setupContexts({}, { enableEducationPlan: false })
|
|
|
|
render(<PlanComp loc="test" />)
|
|
|
|
expect(screen.queryByText(/toVerified/i)).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should show verify button when enableEducationPlan is true and not yet verified', () => {
|
|
setupContexts({}, { enableEducationPlan: true, isEducationAccount: false })
|
|
|
|
render(<PlanComp loc="test" />)
|
|
|
|
expect(screen.getByText(/toVerified/i)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should not show verify button when already verified and not about to expire', () => {
|
|
setupContexts({}, {
|
|
enableEducationPlan: true,
|
|
isEducationAccount: true,
|
|
allowRefreshEducationVerify: false,
|
|
})
|
|
|
|
render(<PlanComp loc="test" />)
|
|
|
|
expect(screen.queryByText(/toVerified/i)).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should show verify button when about to expire (allowRefreshEducationVerify is true)', () => {
|
|
setupContexts({}, {
|
|
enableEducationPlan: true,
|
|
isEducationAccount: true,
|
|
allowRefreshEducationVerify: true,
|
|
})
|
|
|
|
render(<PlanComp loc="test" />)
|
|
|
|
// Shown because isAboutToExpire = allowRefreshEducationVerify = true
|
|
expect(screen.getByText(/toVerified/i)).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
// ─── 2. Successful Verification Flow ────────────────────────────────────
|
|
describe('Successful verification flow', () => {
|
|
it('should navigate to education-apply with token on successful verification', async () => {
|
|
mockMutateAsync.mockResolvedValue({ token: 'edu-token-123' })
|
|
setupContexts({}, { enableEducationPlan: true, isEducationAccount: false })
|
|
const user = userEvent.setup()
|
|
|
|
render(<PlanComp loc="test" />)
|
|
|
|
const verifyButton = screen.getByText(/toVerified/i)
|
|
await user.click(verifyButton)
|
|
|
|
await waitFor(() => {
|
|
expect(mockMutateAsync).toHaveBeenCalledTimes(1)
|
|
expect(mockRouterPush).toHaveBeenCalledWith('/education-apply?token=edu-token-123')
|
|
})
|
|
})
|
|
|
|
it('should remove education verifying flag from localStorage on success', async () => {
|
|
mockMutateAsync.mockResolvedValue({ token: 'token-xyz' })
|
|
setupContexts({}, { enableEducationPlan: true, isEducationAccount: false })
|
|
const user = userEvent.setup()
|
|
|
|
render(<PlanComp loc="test" />)
|
|
|
|
await user.click(screen.getByText(/toVerified/i))
|
|
|
|
await waitFor(() => {
|
|
expect(localStorage.removeItem).toHaveBeenCalledWith('educationVerifying')
|
|
})
|
|
})
|
|
})
|
|
|
|
// ─── 3. Failed Verification Flow ────────────────────────────────────────
|
|
describe('Failed verification flow', () => {
|
|
it('should show VerifyStateModal with rejection info on error', async () => {
|
|
mockMutateAsync.mockRejectedValue(new Error('Verification failed'))
|
|
setupContexts({}, { enableEducationPlan: true, isEducationAccount: false })
|
|
const user = userEvent.setup()
|
|
|
|
render(<PlanComp loc="test" />)
|
|
|
|
// Modal should not be visible initially
|
|
expect(screen.queryByTestId('verify-state-modal')).not.toBeInTheDocument()
|
|
|
|
const verifyButton = screen.getByText(/toVerified/i)
|
|
await user.click(verifyButton)
|
|
|
|
// Modal should appear after verification failure
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('verify-state-modal')).toBeInTheDocument()
|
|
})
|
|
|
|
// Modal should display rejection title and content
|
|
expect(screen.getByTestId('modal-title')).toHaveTextContent(/rejectTitle/i)
|
|
expect(screen.getByTestId('modal-content')).toHaveTextContent(/rejectContent/i)
|
|
})
|
|
|
|
it('should show email and link in VerifyStateModal', async () => {
|
|
mockMutateAsync.mockRejectedValue(new Error('fail'))
|
|
setupContexts({}, { enableEducationPlan: true, isEducationAccount: false })
|
|
const user = userEvent.setup()
|
|
|
|
render(<PlanComp loc="test" />)
|
|
|
|
await user.click(screen.getByText(/toVerified/i))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('modal-email')).toHaveTextContent('student@university.edu')
|
|
expect(screen.getByTestId('modal-show-link')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('should not redirect on verification failure', async () => {
|
|
mockMutateAsync.mockRejectedValue(new Error('fail'))
|
|
setupContexts({}, { enableEducationPlan: true, isEducationAccount: false })
|
|
const user = userEvent.setup()
|
|
|
|
render(<PlanComp loc="test" />)
|
|
|
|
await user.click(screen.getByText(/toVerified/i))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('verify-state-modal')).toBeInTheDocument()
|
|
})
|
|
|
|
// Should NOT navigate
|
|
expect(mockRouterPush).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
// ─── 4. Education + Upgrade Coexistence ─────────────────────────────────
|
|
describe('Education and upgrade button coexistence', () => {
|
|
it('should show both education verify and upgrade buttons for sandbox user', () => {
|
|
setupContexts(
|
|
{ type: Plan.sandbox },
|
|
{ enableEducationPlan: true, isEducationAccount: false },
|
|
)
|
|
|
|
render(<PlanComp loc="test" />)
|
|
|
|
expect(screen.getByText(/toVerified/i)).toBeInTheDocument()
|
|
expect(screen.getByText(/upgradeBtn\.encourageShort/i)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should not show upgrade button for enterprise plan', () => {
|
|
setupContexts(
|
|
{ type: Plan.enterprise },
|
|
{ enableEducationPlan: true, isEducationAccount: false },
|
|
)
|
|
|
|
render(<PlanComp loc="test" />)
|
|
|
|
expect(screen.getByText(/toVerified/i)).toBeInTheDocument()
|
|
expect(screen.queryByText(/upgradeBtn\.encourageShort/i)).not.toBeInTheDocument()
|
|
expect(screen.queryByText(/upgradeBtn\.plain/i)).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should show team plan with plain upgrade button and education button', () => {
|
|
setupContexts(
|
|
{ type: Plan.team },
|
|
{ enableEducationPlan: true, isEducationAccount: false },
|
|
)
|
|
|
|
render(<PlanComp loc="test" />)
|
|
|
|
expect(screen.getByText(/toVerified/i)).toBeInTheDocument()
|
|
expect(screen.getByText(/upgradeBtn\.plain/i)).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|