Files
Containr/app/frontend/src/features/setup/SetupWizardPage.test.tsx
T
Tomáš DvořákandDevin c4307034ec feat(setup): first-run wizard — admin, GitHub App, tunnel, done (#71)
* feat(setup): first-run wizard — admin, GitHub App, tunnel, done

- GET /api/v1/setup/status (public): has_users, needs_setup,
  has_github_app, has_tunnel. app_settings.setup_completed flag; installs
  with existing projects auto-complete so upgrades never see the wizard.
- POST /api/v1/setup/complete records the flag.
- /setup page: progress rail + four steps — create admin (links into the
  bootstrap sign-up with redirect back), GitHub App manifest flow,
  optional Cloudflare tunnel token, finish. Every optional step is
  skippable; nothing blocks.
- PlatformShell redirects signed-in users to /setup while needs_setup.
- The GitHub App callback honors a sessionStorage return marker set by
  the wizard so provisioning resumes at /setup instead of /settings.
- OpenAPI documents both endpoints.

Verified live: status reports auto-completed on this install,
complete persists the flag, response shape matches the client.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(setup): wizard step-state coverage

Five cases across the state machine: fresh-install account prompt,
sign-in resume, git-provider step, tunnel step, and finish.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-10 02:17:41 +02:00

93 lines
3.0 KiB
TypeScript

import { describe, expect, it, vi, beforeEach } from 'vitest';
import { render, screen } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { MemoryRouter } from 'react-router-dom';
import { SetupWizardPage } from './SetupWizardPage';
import type { SetupStatus } from '@/lib/api-client';
const mockStatus = vi.fn<() => Promise<SetupStatus>>();
const mockSession = vi.fn();
vi.mock('@/lib/api-client', async (importOriginal) => {
const mod = await importOriginal<typeof import('@/lib/api-client')>();
return {
...mod,
getSetupStatus: () => mockStatus(),
completeSetup: vi.fn().mockResolvedValue(undefined),
};
});
vi.mock('@/lib/use-auth-session', () => ({
useAuthSession: () => mockSession(),
}));
function renderWizard() {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
return render(
<QueryClientProvider client={queryClient}>
<MemoryRouter initialEntries={['/setup']}>
<SetupWizardPage />
</MemoryRouter>
</QueryClientProvider>,
);
}
const base: SetupStatus = {
has_users: false,
setup_completed: false,
needs_setup: true,
has_github_app: false,
has_tunnel: false,
signup_enabled: false,
};
describe('SetupWizardPage', () => {
beforeEach(() => {
mockSession.mockReturnValue({ data: null });
});
it('prompts for the admin account on a fresh install', async () => {
mockStatus.mockResolvedValue({ ...base });
renderWizard();
expect(await screen.findByText(/welcome to containr/i)).toBeInTheDocument();
expect(screen.getByText(/create admin account/i)).toBeInTheDocument();
});
it('offers sign-in when users exist but nobody is signed in', async () => {
mockStatus.mockResolvedValue({ ...base, has_users: true });
renderWizard();
expect(await screen.findByText(/sign in to continue setup/i)).toBeInTheDocument();
});
it('shows the git provider step for a signed-in admin without GitHub App', async () => {
mockStatus.mockResolvedValue({ ...base, has_users: true });
mockSession.mockReturnValue({ data: { user: { id: 'u1' } } });
renderWizard();
expect(await screen.findByText(/connect a git provider/i)).toBeInTheDocument();
expect(screen.getByText(/set up github app/i)).toBeInTheDocument();
});
it('shows the tunnel step when git is already wired', async () => {
mockStatus.mockResolvedValue({ ...base, has_users: true, has_github_app: true });
mockSession.mockReturnValue({ data: { user: { id: 'u1' } } });
renderWizard();
expect(
await screen.findByRole('heading', { name: /public access/i }),
).toBeInTheDocument();
});
it('lands on the finish step when everything is configured', async () => {
mockStatus.mockResolvedValue({
...base,
has_users: true,
has_github_app: true,
has_tunnel: true,
});
mockSession.mockReturnValue({ data: { user: { id: 'u1' } } });
renderWizard();
expect(await screen.findByText(/enter containr/i)).toBeInTheDocument();
});
});