Test website auth error and oauth posture
This commit is contained in:
parent
220f7f54c5
commit
d11bf59a70
6 changed files with 116 additions and 56 deletions
|
|
@ -157,6 +157,7 @@ The frontend behavior coverage now also explicitly pins:
|
|||
- protected app-route loading/redirect behavior plus auth-aware marketing/app shell actions
|
||||
- real `AppRouteTree` smoke coverage for homepage, pricing, download, login, dashboard, and dashboard-download routes through the lazy public/protected router itself
|
||||
- top-level `App` bootstrap coverage for unknown-route redirect plus SuperTokens wrapper enabled-versus-fallback posture
|
||||
- login/register unhappy-path coverage for returned form errors, auth-runtime warning callouts, and OAuth-button visibility/invocation
|
||||
- dashboard launch-readiness visibility plus generated desktop-link verify URL behavior
|
||||
|
||||
The first-party auth server now also supports bounded same-origin public serving
|
||||
|
|
|
|||
|
|
@ -264,7 +264,7 @@ repo.
|
|||
| Feature | Status | Primary authority | Notes |
|
||||
|---|---|---|---|
|
||||
| Public `hypertwist.app` marketing shell | Implemented now | first-party `website/` app + feature registry/roadmap authority | HyperTwist now has a dedicated first-party public web surface for homepage, about, resources, pricing, download, support, and legal routes. This lane is separate from the embedded Unreal browser runtime under `Content/Browser/` and does not claim browser-simulator parity. The same package now also carries a first-party external runtime-readiness verifier so deploy-time env and live health posture can be checked outside the dashboard, plus separated local-versus-production env templates whose placeholder values are intentionally rejected until real launch config is in place, bootstrap CI now validates both the frontend and auth-server website commands directly, and the auth server can now auto-serve the built `website/dist` bundle with bounded SPA fallback for same-origin public deployment. Request-level server coverage now also proves that public/app shell delivery does not shadow `/api/*`, `/auth*`, `/health`, or missing asset paths. |
|
||||
| Browser-based operator/account dashboard | Implemented now | first-party `website/` app + shared auth/dashboard packet | A protected browser dashboard is now live for operator access, account state, download posture, browser-access boundary explanation, notices review, and bounded billing/entitlement status. It reuses the shared SuperTokens auth posture proven in FamiliarOS and ScriptoriumAI while remaining HyperTwist-specific in product content and boundary claims, the current auth-health surface now truthfully distinguishes configured versus reachable or ready shared-core posture while exposing fallback-active reason instead of hardcoding readiness, and the same dashboard now also surfaces launch-readiness truth for download URLs, checkout links, source/notices URLs, billing-secret/map configuration, and local-versus-public runtime deployment posture. Focused frontend coverage now also protects deep-link login redirect preservation, safe `next`-path normalization across auth entry points, fallback/email auth-bootstrap normalization, login/register continuation behavior, public download-gating behavior, protected-route/shell behavior, real lazy-route tree behavior for key public and protected paths, top-level app-bootstrap and SuperTokens-wrapper posture, and desktop-link verify-url/dashboard readiness behavior. |
|
||||
| Browser-based operator/account dashboard | Implemented now | first-party `website/` app + shared auth/dashboard packet | A protected browser dashboard is now live for operator access, account state, download posture, browser-access boundary explanation, notices review, and bounded billing/entitlement status. It reuses the shared SuperTokens auth posture proven in FamiliarOS and ScriptoriumAI while remaining HyperTwist-specific in product content and boundary claims, the current auth-health surface now truthfully distinguishes configured versus reachable or ready shared-core posture while exposing fallback-active reason instead of hardcoding readiness, and the same dashboard now also surfaces launch-readiness truth for download URLs, checkout links, source/notices URLs, billing-secret/map configuration, and local-versus-public runtime deployment posture. Focused frontend coverage now also protects deep-link login redirect preservation, safe `next`-path normalization across auth entry points, fallback/email auth-bootstrap normalization, login/register continuation behavior, public download-gating behavior, protected-route/shell behavior, real lazy-route tree behavior for key public and protected paths, top-level app-bootstrap and SuperTokens-wrapper posture, login/register unhappy-path and OAuth-button behavior, and desktop-link verify-url/dashboard readiness behavior. |
|
||||
| Desktop download posture and browser-to-desktop pairing | Implemented now | first-party `website/` app + `website/server` desktop-link endpoints | Public download targets, dashboard-side release posture, and short-lived desktop-link token generation/verification are now first-party owned. The current server posture now enforces exact website-origin matching, bounded per-user issuance, one-time token consumption, and billing-backed plan/download entitlement resolution with focused `website/server` tests green on `2026-06-22`, and the verify handshake now returns the same resolved download-entitlement posture the dashboard sees instead of only identity plus plan/role. The public `/download` page now keeps raw download URLs behind the protected dashboard instead of exposing them directly. Actual release URLs remain deployment configuration rather than hardcoded product truth. |
|
||||
| Paddle-ready pricing and billing webhook seam | Implemented now | first-party `website/` app + `website/server` billing endpoint | The public pricing surface now exists with plan structure, checkout-link configuration seams, and the same `/api/billing/paddle/webhook` endpoint family used by the broader product website lane. The current server now verifies `Paddle-Signature` against `PADDLE_WEBHOOK_SECRET` using the documented raw-body HMAC flow, persists a bounded first-party billing state file, and applies verified Paddle events into account/download entitlement state that the browser dashboard consumes, with focused `website/server` tests green on `2026-06-22`. Production checkout URLs, secret management, and broader operator/admin billing workflows remain deployment/application tasks, not shipped-code omissions. |
|
||||
| Public open-source notices and corresponding-source surface | Implemented now | first-party `website/` app + `HYPERTWIST_MPL_DISTRIBUTION_PLACEMENT_CHECKLIST_2026-05-25.md` | HyperTwist now has a stable public `Open Source Notices` route linked from pricing, download, and footer surfaces, satisfying the requirement that public distribution surfaces expose notice and corresponding-source guidance when shipped builds contain `MPL`-covered material. The exact public corresponding-source URL still must be configured before external launch. |
|
||||
|
|
|
|||
|
|
@ -226,7 +226,9 @@ Current consolidated milestone snapshot:
|
|||
plus desktop-link verify-url behavior, with real lazy-route smoke coverage now
|
||||
pinned for `/`, `/pricing`, `/download`, `/login`, `/app`, and `/app/downloads`,
|
||||
plus top-level `App` bootstrap proof for unknown-route redirect and
|
||||
SuperTokens-wrapper posture,
|
||||
SuperTokens-wrapper posture, plus login/register unhappy-path proof for
|
||||
returned form errors, auth-runtime warning posture, and OAuth-button
|
||||
behavior,
|
||||
and the auth server can now auto-serve the built
|
||||
`website/dist` bundle with bounded SPA fallback for same-origin `hypertwist.app`
|
||||
deployment when that build output is present, while the env templates and
|
||||
|
|
|
|||
|
|
@ -99,4 +99,5 @@ The focused frontend test coverage now also pins:
|
|||
- protected-route loading/redirect behavior plus auth-aware marketing/app shell actions
|
||||
- real `AppRouteTree` smoke coverage for `/`, `/pricing`, `/download`, `/login`, `/app`, and `/app/downloads`
|
||||
- top-level `App` bootstrap coverage for unknown-route redirect and SuperTokens wrapper on/off posture
|
||||
- login/register unhappy-path coverage for returned form errors, auth-runtime warning callouts, and OAuth-button visibility/invocation
|
||||
- dashboard launch-readiness plus desktop-link verify-url behavior
|
||||
|
|
|
|||
|
|
@ -7,20 +7,20 @@ import { ROUTER_FUTURE_FLAGS } from '../router/router-future'
|
|||
const mockUsePlatformAuth = vi.fn()
|
||||
const mockLogin = vi.fn()
|
||||
const mockRegister = vi.fn()
|
||||
const mockGetSuperTokensAuthRuntimeValidation = vi.fn()
|
||||
const mockIsGitHubOAuthEnabled = vi.fn(() => false)
|
||||
const mockIsGoogleOAuthEnabled = vi.fn(() => false)
|
||||
const mockIsOrcidOAuthEnabled = vi.fn(() => false)
|
||||
|
||||
vi.mock('../auth/platform-auth', () => ({
|
||||
usePlatformAuth: () => mockUsePlatformAuth(),
|
||||
}))
|
||||
|
||||
vi.mock('../auth/supertokens-client', () => ({
|
||||
getSuperTokensAuthRuntimeValidation: () => ({
|
||||
ready: true,
|
||||
missing: [],
|
||||
warnings: [],
|
||||
}),
|
||||
isGitHubOAuthEnabled: () => false,
|
||||
isGoogleOAuthEnabled: () => false,
|
||||
isOrcidOAuthEnabled: () => false,
|
||||
getSuperTokensAuthRuntimeValidation: () => mockGetSuperTokensAuthRuntimeValidation(),
|
||||
isGitHubOAuthEnabled: () => mockIsGitHubOAuthEnabled(),
|
||||
isGoogleOAuthEnabled: () => mockIsGoogleOAuthEnabled(),
|
||||
isOrcidOAuthEnabled: () => mockIsOrcidOAuthEnabled(),
|
||||
}))
|
||||
|
||||
vi.mock('../site-config', async () => {
|
||||
|
|
@ -77,6 +77,18 @@ describe('public auth and download pages', () => {
|
|||
mockLogin.mockReset()
|
||||
mockRegister.mockReset()
|
||||
mockUsePlatformAuth.mockReset()
|
||||
mockGetSuperTokensAuthRuntimeValidation.mockReset()
|
||||
mockIsGitHubOAuthEnabled.mockReset()
|
||||
mockIsGoogleOAuthEnabled.mockReset()
|
||||
mockIsOrcidOAuthEnabled.mockReset()
|
||||
mockGetSuperTokensAuthRuntimeValidation.mockReturnValue({
|
||||
ready: true,
|
||||
missing: [],
|
||||
warnings: [],
|
||||
})
|
||||
mockIsGitHubOAuthEnabled.mockReturnValue(false)
|
||||
mockIsGoogleOAuthEnabled.mockReturnValue(false)
|
||||
mockIsOrcidOAuthEnabled.mockReturnValue(false)
|
||||
mockUsePlatformAuth.mockReturnValue({
|
||||
isAuthenticated: false,
|
||||
login: (...args: unknown[]) => mockLogin(...args),
|
||||
|
|
@ -126,6 +138,64 @@ describe('public auth and download pages', () => {
|
|||
})
|
||||
})
|
||||
|
||||
it('shows login errors without navigating away from the auth page', async () => {
|
||||
mockLogin.mockResolvedValue({ ok: false, error: 'Incorrect email or password.' })
|
||||
|
||||
renderAuthRoutes('/login?next=%2Fapp')
|
||||
|
||||
await userEvent.type(screen.getByLabelText('Email'), 'operator@hypertwist.app')
|
||||
await userEvent.type(screen.getByLabelText('Password'), 'wrong-password')
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Log in' }))
|
||||
|
||||
expect(await screen.findByText('Incorrect email or password.')).toBeTruthy()
|
||||
expect(screen.queryByTestId('location')).toBeNull()
|
||||
})
|
||||
|
||||
it('shows register errors without navigating away from the auth page', async () => {
|
||||
mockRegister.mockResolvedValue({ ok: false, error: 'Sign-up is not allowed right now.' })
|
||||
|
||||
renderAuthRoutes('/register?next=%2Fapp%2Fdownloads')
|
||||
|
||||
await userEvent.type(screen.getByLabelText('Name'), 'Operator')
|
||||
await userEvent.type(screen.getByLabelText('Email'), 'operator@hypertwist.app')
|
||||
await userEvent.type(screen.getByLabelText('Password'), 'password123')
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Create account' }))
|
||||
|
||||
expect(await screen.findByText('Sign-up is not allowed right now.')).toBeTruthy()
|
||||
expect(screen.queryByTestId('location')).toBeNull()
|
||||
})
|
||||
|
||||
it('surfaces auth-runtime missing and warning posture on the login page', () => {
|
||||
mockGetSuperTokensAuthRuntimeValidation.mockReturnValue({
|
||||
ready: false,
|
||||
missing: ['VITE_SUPERTOKENS_API_DOMAIN'],
|
||||
warnings: ['VITE_AUTH_API_BASE_URL not configured; same-origin auth fallback remains active.'],
|
||||
})
|
||||
|
||||
renderAuthRoutes('/login')
|
||||
|
||||
expect(screen.getByText(/Shared browser auth is not fully in production posture yet./i)).toBeTruthy()
|
||||
expect(screen.getByText('Missing auth env: VITE_SUPERTOKENS_API_DOMAIN')).toBeTruthy()
|
||||
expect(screen.getByText('VITE_AUTH_API_BASE_URL not configured; same-origin auth fallback remains active.')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('shows enabled OAuth buttons and routes provider clicks through the login handler', async () => {
|
||||
mockIsGoogleOAuthEnabled.mockReturnValue(true)
|
||||
mockIsGitHubOAuthEnabled.mockReturnValue(true)
|
||||
mockIsOrcidOAuthEnabled.mockReturnValue(true)
|
||||
mockLogin.mockResolvedValue({ ok: true })
|
||||
|
||||
renderAuthRoutes('/login')
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Continue with Google' }))
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Continue with GitHub' }))
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Continue with ORCID' }))
|
||||
|
||||
expect(mockLogin).toHaveBeenCalledWith({ method: 'google', email: '' })
|
||||
expect(mockLogin).toHaveBeenCalledWith({ method: 'github', email: '' })
|
||||
expect(mockLogin).toHaveBeenCalledWith({ method: 'orcid', email: '' })
|
||||
})
|
||||
|
||||
it('keeps configured public download targets behind the protected dashboard instead of exposing raw URLs', () => {
|
||||
render(
|
||||
<MemoryRouter future={ROUTER_FUTURE_FLAGS}>
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@ import {
|
|||
isOrcidOAuthEnabled,
|
||||
} from '../auth/supertokens-client'
|
||||
|
||||
const authRuntimeValidation = getSuperTokensAuthRuntimeValidation()
|
||||
|
||||
function AuthShell({
|
||||
title,
|
||||
subtitle,
|
||||
|
|
@ -40,6 +38,36 @@ function useNextPath() {
|
|||
return normalizeNextPath(searchParams.get('next'))
|
||||
}
|
||||
|
||||
function AuthRuntimeNotice() {
|
||||
const authRuntimeValidation = getSuperTokensAuthRuntimeValidation()
|
||||
if (authRuntimeValidation.ready && authRuntimeValidation.warnings.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="callout">
|
||||
<p>
|
||||
Shared browser auth is not fully in production posture yet.
|
||||
{!authRuntimeValidation.ready ? ' Local fallback mode may activate until the required auth env vars are configured.' : ''}
|
||||
</p>
|
||||
{authRuntimeValidation.missing.length > 0 ? (
|
||||
<ul className="list">
|
||||
{authRuntimeValidation.missing.map((item) => (
|
||||
<li key={item}>Missing auth env: {item}</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
{authRuntimeValidation.warnings.length > 0 ? (
|
||||
<ul className="list">
|
||||
{authRuntimeValidation.warnings.map((item) => (
|
||||
<li key={item}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function LoginPage() {
|
||||
const navigate = useNavigate()
|
||||
const { login, isAuthenticated } = usePlatformAuth()
|
||||
|
|
@ -74,28 +102,7 @@ export function LoginPage() {
|
|||
subtitle="Use the same browser auth posture as FamiliarOS and ScriptoriumAI, then hand off to the desktop runtime when needed."
|
||||
footer={<p>Need access? <Link to={`/register?next=${encodeURIComponent(nextPath)}`}>Create an account</Link>.</p>}
|
||||
>
|
||||
{!authRuntimeValidation.ready || authRuntimeValidation.warnings.length > 0 ? (
|
||||
<div className="callout">
|
||||
<p>
|
||||
Shared browser auth is not fully in production posture yet.
|
||||
{!authRuntimeValidation.ready ? ' Local fallback mode may activate until the required auth env vars are configured.' : ''}
|
||||
</p>
|
||||
{authRuntimeValidation.missing.length > 0 ? (
|
||||
<ul className="list">
|
||||
{authRuntimeValidation.missing.map((item) => (
|
||||
<li key={item}>Missing auth env: {item}</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
{authRuntimeValidation.warnings.length > 0 ? (
|
||||
<ul className="list">
|
||||
{authRuntimeValidation.warnings.map((item) => (
|
||||
<li key={item}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<AuthRuntimeNotice />
|
||||
<form className="auth-form" onSubmit={handleSubmit}>
|
||||
<label className="input-label" htmlFor="login-email">Email</label>
|
||||
<input id="login-email" className="input" value={email} onChange={(event) => setEmail(event.target.value)} />
|
||||
|
|
@ -162,28 +169,7 @@ export function RegisterPage() {
|
|||
subtitle="This unlocks the browser dashboard, release posture, and desktop-link pairing for the simulator lane."
|
||||
footer={<p>Already have access? <Link to={`/login?next=${encodeURIComponent(nextPath)}`}>Log in</Link>.</p>}
|
||||
>
|
||||
{!authRuntimeValidation.ready || authRuntimeValidation.warnings.length > 0 ? (
|
||||
<div className="callout">
|
||||
<p>
|
||||
Shared browser auth is not fully in production posture yet.
|
||||
{!authRuntimeValidation.ready ? ' Local fallback mode may activate until the required auth env vars are configured.' : ''}
|
||||
</p>
|
||||
{authRuntimeValidation.missing.length > 0 ? (
|
||||
<ul className="list">
|
||||
{authRuntimeValidation.missing.map((item) => (
|
||||
<li key={item}>Missing auth env: {item}</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
{authRuntimeValidation.warnings.length > 0 ? (
|
||||
<ul className="list">
|
||||
{authRuntimeValidation.warnings.map((item) => (
|
||||
<li key={item}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<AuthRuntimeNotice />
|
||||
<form className="auth-form" onSubmit={handleSubmit}>
|
||||
<label className="input-label" htmlFor="register-name">Name</label>
|
||||
<input id="register-name" className="input" value={name} onChange={(event) => setName(event.target.value)} />
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue