mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 15:51:26 +00:00
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>
This commit is contained in:
1 parent
985d22798c
commit
c4307034ec
11 files changed
+622
-4
No files matched your search
@@ -0,0 +1,92 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user