mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(settings): redirect unavailable tabs to general (#6710)
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const {
|
||||
mockCanOpenOrganizationSettingsSection,
|
||||
mockGetSession,
|
||||
mockGetWorkspaceHostContext,
|
||||
mockHasWorkspaceInboxAccess,
|
||||
mockHasWorkspaceSandboxAccess,
|
||||
mockIsForkingAvailable,
|
||||
mockIsOrganizationOnEnterprisePlan,
|
||||
mockIsOrganizationSettingsSectionAvailable,
|
||||
mockNotFound,
|
||||
mockRedirect,
|
||||
mockResolveWorkspaceGroup,
|
||||
mockResolveWorkspaceNavigation,
|
||||
} = vi.hoisted(() => ({
|
||||
mockCanOpenOrganizationSettingsSection: vi.fn(),
|
||||
mockGetSession: vi.fn(),
|
||||
mockGetWorkspaceHostContext: vi.fn(),
|
||||
mockHasWorkspaceInboxAccess: vi.fn(),
|
||||
mockHasWorkspaceSandboxAccess: vi.fn(),
|
||||
mockIsForkingAvailable: vi.fn(),
|
||||
mockIsOrganizationOnEnterprisePlan: vi.fn(),
|
||||
mockIsOrganizationSettingsSectionAvailable: vi.fn(),
|
||||
mockNotFound: vi.fn(() => {
|
||||
throw new Error('NEXT_NOT_FOUND')
|
||||
}),
|
||||
mockRedirect: vi.fn((href: string) => {
|
||||
throw new Error(`NEXT_REDIRECT:${href}`)
|
||||
}),
|
||||
mockResolveWorkspaceGroup: vi.fn(),
|
||||
mockResolveWorkspaceNavigation: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('next/navigation', () => ({
|
||||
notFound: mockNotFound,
|
||||
redirect: mockRedirect,
|
||||
}))
|
||||
|
||||
vi.mock('@/components/settings/navigation', () => ({
|
||||
getOrganizationSettingsFeatures: vi.fn(() => ({})),
|
||||
isOrganizationSettingsSectionAvailable: mockIsOrganizationSettingsSectionAvailable,
|
||||
resolveWorkspaceNavigation: mockResolveWorkspaceNavigation,
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/auth', () => ({
|
||||
getSession: mockGetSession,
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/billing', () => ({
|
||||
isOrganizationOnEnterprisePlan: mockIsOrganizationOnEnterprisePlan,
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/billing/core/subscription', () => ({
|
||||
hasWorkspaceInboxAccess: mockHasWorkspaceInboxAccess,
|
||||
hasWorkspaceSandboxAccess: mockHasWorkspaceSandboxAccess,
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/core/config/env', () => ({
|
||||
getEnv: vi.fn(),
|
||||
isTruthy: vi.fn(() => false),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/core/config/env-flags', () => ({
|
||||
isBillingEnabled: true,
|
||||
isHosted: true,
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/organizations/settings-access', () => ({
|
||||
canOpenOrganizationSettingsSection: mockCanOpenOrganizationSettingsSection,
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/permissions/super-user', () => ({
|
||||
isPlatformAdmin: vi.fn(() => false),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/workspaces/host-context', () => ({
|
||||
getWorkspaceHostContextForViewer: mockGetWorkspaceHostContext,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/_shell/providers/get-query-client', () => ({
|
||||
getQueryClient: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/workspace/[workspaceId]/settings/navigation', () => ({
|
||||
allNavigationItems: [{ id: 'general' }, { id: 'billing' }, { id: 'secrets' }, { id: 'sessions' }],
|
||||
getSettingsSectionMeta: vi.fn(() => null),
|
||||
}))
|
||||
|
||||
vi.mock('@/ee/access-control/utils/permission-check', () => ({
|
||||
resolveWorkspaceGroup: mockResolveWorkspaceGroup,
|
||||
}))
|
||||
|
||||
vi.mock('@/ee/workspace-forking/lib/lineage/authz', () => ({
|
||||
isForkingAvailableForWorkspace: mockIsForkingAvailable,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/workspace/[workspaceId]/settings/[section]/prefetch', () => ({
|
||||
prefetchGeneralSettings: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/workspace/[workspaceId]/settings/[section]/settings', () => ({
|
||||
SettingsPage: vi.fn(() => null),
|
||||
}))
|
||||
|
||||
import WorkspaceSettingsSectionPage from '@/app/workspace/[workspaceId]/settings/[section]/page'
|
||||
|
||||
const PERSONAL_HOST_CONTEXT = {
|
||||
workspace: {
|
||||
id: 'workspace-b',
|
||||
billedAccountUserId: 'owner-b',
|
||||
},
|
||||
hostOrganizationId: null,
|
||||
ownerBilling: {
|
||||
isEnterprise: false,
|
||||
},
|
||||
viewer: {
|
||||
permission: 'admin',
|
||||
isHostOrganizationAdmin: false,
|
||||
},
|
||||
}
|
||||
|
||||
function pageProps(section: string) {
|
||||
return {
|
||||
params: Promise.resolve({ workspaceId: 'workspace-b', section }),
|
||||
}
|
||||
}
|
||||
|
||||
describe('WorkspaceSettingsSectionPage unavailable sections', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockGetSession.mockResolvedValue({ user: { id: 'viewer-a' } })
|
||||
mockGetWorkspaceHostContext.mockResolvedValue(PERSONAL_HOST_CONTEXT)
|
||||
mockResolveWorkspaceNavigation.mockReturnValue([])
|
||||
mockResolveWorkspaceGroup.mockResolvedValue(null)
|
||||
mockIsForkingAvailable.mockResolvedValue(false)
|
||||
mockHasWorkspaceInboxAccess.mockResolvedValue(false)
|
||||
mockHasWorkspaceSandboxAccess.mockResolvedValue(false)
|
||||
mockCanOpenOrganizationSettingsSection.mockResolvedValue(false)
|
||||
mockIsOrganizationOnEnterprisePlan.mockResolvedValue(false)
|
||||
mockIsOrganizationSettingsSectionAvailable.mockReturnValue(true)
|
||||
})
|
||||
|
||||
it('redirects an unavailable subscription section to General', async () => {
|
||||
await expect(WorkspaceSettingsSectionPage(pageProps('billing'))).rejects.toThrow(
|
||||
'NEXT_REDIRECT:/workspace/workspace-b/settings/general'
|
||||
)
|
||||
})
|
||||
|
||||
it('redirects a workspace section hidden in the destination workspace to General', async () => {
|
||||
await expect(WorkspaceSettingsSectionPage(pageProps('secrets'))).rejects.toThrow(
|
||||
'NEXT_REDIRECT:/workspace/workspace-b/settings/general'
|
||||
)
|
||||
})
|
||||
|
||||
it('redirects an organization section when the destination has no organization', async () => {
|
||||
await expect(WorkspaceSettingsSectionPage(pageProps('sessions'))).rejects.toThrow(
|
||||
'NEXT_REDIRECT:/workspace/workspace-b/settings/general'
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps unknown settings sections fail-fast', async () => {
|
||||
await expect(WorkspaceSettingsSectionPage(pageProps('unknown'))).rejects.toThrow(
|
||||
'NEXT_NOT_FOUND'
|
||||
)
|
||||
expect(mockGetWorkspaceHostContext).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps inaccessible workspaces fail-fast', async () => {
|
||||
mockGetWorkspaceHostContext.mockResolvedValue(null)
|
||||
|
||||
await expect(WorkspaceSettingsSectionPage(pageProps('general'))).rejects.toThrow(
|
||||
'NEXT_NOT_FOUND'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -67,6 +67,7 @@ const ORGANIZATION_SECTION_MAP: Partial<Record<SettingsSection, OrganizationSett
|
||||
'access-control': 'access-control',
|
||||
'audit-logs': 'audit-logs',
|
||||
sso: 'sso',
|
||||
sessions: 'sessions',
|
||||
'data-retention': 'data-retention',
|
||||
'data-drains': 'data-drains',
|
||||
whitelabeling: 'whitelabeling',
|
||||
@@ -79,6 +80,14 @@ function parseSection(section: string): SettingsSection | null {
|
||||
: null
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings availability varies across workspaces, so a preserved section may
|
||||
* need to land on the destination workspace's universally available page.
|
||||
*/
|
||||
function redirectToGeneralSettings(workspaceId: string): never {
|
||||
redirect(`/workspace/${workspaceId}/settings/general`)
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: WorkspaceSettingsSectionPageProps): Promise<Metadata> {
|
||||
@@ -131,20 +140,24 @@ export default async function WorkspaceSettingsSectionPage({
|
||||
sandboxes,
|
||||
},
|
||||
})
|
||||
if (!navigation.some((item) => item.id === workspaceSection)) notFound()
|
||||
if (!navigation.some((item) => item.id === workspaceSection)) {
|
||||
redirectToGeneralSettings(workspaceId)
|
||||
}
|
||||
}
|
||||
|
||||
const organizationSection = ORGANIZATION_SECTION_MAP[parsed]
|
||||
if (organizationSection) {
|
||||
if (!isBillingEnabled && (parsed === 'billing' || parsed === 'organization')) {
|
||||
redirect(`/workspace/${workspaceId}/settings/general`)
|
||||
redirectToGeneralSettings(workspaceId)
|
||||
}
|
||||
if (!hostContext.hostOrganizationId) {
|
||||
if (parsed !== 'billing' || hostContext.workspace.billedAccountUserId !== session.user.id) {
|
||||
notFound()
|
||||
redirectToGeneralSettings(workspaceId)
|
||||
}
|
||||
} else {
|
||||
if (!hostContext.viewer.isHostOrganizationAdmin) notFound()
|
||||
if (!hostContext.viewer.isHostOrganizationAdmin) {
|
||||
redirectToGeneralSettings(workspaceId)
|
||||
}
|
||||
if (
|
||||
!(await canOpenOrganizationSettingsSection(
|
||||
hostContext.hostOrganizationId,
|
||||
@@ -152,7 +165,7 @@ export default async function WorkspaceSettingsSectionPage({
|
||||
organizationSection
|
||||
))
|
||||
) {
|
||||
notFound()
|
||||
redirectToGeneralSettings(workspaceId)
|
||||
}
|
||||
const hasEnterprisePlan =
|
||||
organizationSection !== 'members' &&
|
||||
@@ -164,7 +177,7 @@ export default async function WorkspaceSettingsSectionPage({
|
||||
getOrganizationSettingsFeatures(hasEnterprisePlan)
|
||||
)
|
||||
) {
|
||||
notFound()
|
||||
redirectToGeneralSettings(workspaceId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user