mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-02 21:32:10 +03:00
fix(oauth): show GitLab Duo setup before authorize error (#8710)
* fix(oauth): show GitLab Duo setup before authorize error Surface the OAuth app registration and env-var recipe in the Add Connection modal before auto-starting authorize, and keep the same shared copy for catalog authHint and the authorize fallback (#8688). * fix(oauth): keep OAuthModal under file-size baseline for #8688 Extract waiting/error panels so the GitLab Duo setup step does not trip the Fast Quality Gates file-size ratchet, and update the retry Button regression guard for the extracted error step.
This commit is contained in:
committed by
GitHub
parent
85128984f9
commit
ba28e497fe
1
changelog.d/fixes/8688-gitlab-duo-oauth-setup.md
Normal file
1
changelog.d/fixes/8688-gitlab-duo-oauth-setup.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(oauth):** show GitLab Duo OAuth app / env setup instructions in the Add Connection modal _before_ authorize, using a shared recipe with catalog `authHint` and the authorize-route error fallback (#8688)
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
import { isAuthRequired, isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
|
||||
import { GITLAB_DUO_OAUTH_SETUP_MESSAGE } from "@/shared/constants/gitlabDuoSetupMessage";
|
||||
import { keychainImportOnlyGuard } from "./keychainImportOnly";
|
||||
import { buildRemoteOAuthHint } from "./remoteOAuthHint";
|
||||
|
||||
@@ -174,17 +175,14 @@ export async function GET(
|
||||
"Qoder browser OAuth is experimental and disabled by default. Configure QODER_OAUTH_* environment variables or use a Personal Access Token.",
|
||||
});
|
||||
}
|
||||
// #3861: GitLab Duo needs a self-registered OAuth app. Without a client_id,
|
||||
// #3861 / #8688: GitLab Duo needs a self-registered OAuth app. Without a client_id,
|
||||
// buildAuthUrl returns null — surface a clear setup message instead of a 500.
|
||||
// Same copy is shown in the OAuthModal setup step *before* authorize (#8688).
|
||||
if (provider === "gitlab-duo" && !authData.authUrl) {
|
||||
return NextResponse.json({
|
||||
...authData,
|
||||
supported: false,
|
||||
error:
|
||||
"GitLab Duo OAuth is not configured. Register an OAuth application at " +
|
||||
"https://gitlab.com/-/profile/applications with redirect URI " +
|
||||
'http://localhost:20128/callback and scopes "ai_features read_user", then set ' +
|
||||
"GITLAB_DUO_OAUTH_CLIENT_ID (and optionally GITLAB_DUO_OAUTH_CLIENT_SECRET) and restart.",
|
||||
error: GITLAB_DUO_OAUTH_SETUP_MESSAGE,
|
||||
});
|
||||
}
|
||||
return NextResponse.json(authData);
|
||||
|
||||
@@ -5958,7 +5958,7 @@
|
||||
"cline": "Connect Cline with the existing OAuth flow.",
|
||||
"cursor": "Connect Cursor IDE with the existing OAuth flow.",
|
||||
"github": "Connect GitHub Copilot with the existing OAuth flow.",
|
||||
"gitlab-duo": "OAuth application with ai_features + read_user scopes. Configure GITLAB_DUO_OAUTH_CLIENT_ID and optionally GITLAB_DUO_OAUTH_CLIENT_SECRET on this OmniRoute instance.",
|
||||
"gitlab-duo": "GitLab Duo OAuth is not configured. Register an OAuth application at https://gitlab.com/-/profile/applications with redirect URI http://localhost:20128/callback and scopes \"ai_features read_user\", then set GITLAB_DUO_OAUTH_CLIENT_ID (and optionally GITLAB_DUO_OAUTH_CLIENT_SECRET) and restart.",
|
||||
"kilocode": "Connect Kilo Code with the existing OAuth flow.",
|
||||
"kimi-coding": "Connect Kimi Coding with the existing OAuth flow.",
|
||||
"kiro": "Free tier: 50 credits/month (~25K–100K tokens). ⚠️ Kiro ToS prohibits third-party proxy/harness use.",
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useTranslations } from "next-intl";
|
||||
import Modal from "./Modal";
|
||||
import Button from "./Button";
|
||||
import Input from "./Input";
|
||||
import LinkifiedText from "./LinkifiedText";
|
||||
import {
|
||||
OAuthDeviceCodePanel,
|
||||
OAuthLoopbackMismatchPanel,
|
||||
@@ -18,6 +17,9 @@ import {
|
||||
parseCodexSessionJson,
|
||||
} from "@/lib/oauth/utils/codexSessionImport";
|
||||
import GheConfigStep from "@/shared/components/oauthModal/GheConfigStep";
|
||||
import GitlabDuoSetupStep from "@/shared/components/oauthModal/GitlabDuoSetupStep";
|
||||
import OAuthErrorStep from "@/shared/components/oauthModal/OAuthErrorStep";
|
||||
import OAuthWaitingStep from "@/shared/components/oauthModal/OAuthWaitingStep";
|
||||
import { parseGrokCliPasteToken } from "@/lib/oauth/utils/grokCliAuthJson";
|
||||
import { buildGoogleLoopbackHint } from "@/lib/oauth/utils/googleLoopbackHint";
|
||||
import {
|
||||
@@ -667,6 +669,8 @@ export default function OAuthModal({
|
||||
if (!isOpen || !provider || flowStartedRef.current) return;
|
||||
flowStartedRef.current = true;
|
||||
const startsInPasteMode = IMPORT_TOKEN_ONLY_PROVIDERS.has(provider);
|
||||
// #8688: show GitLab Duo OAuth app / env setup before authorize error.
|
||||
const startsInGitlabDuoSetup = provider === "gitlab-duo";
|
||||
setShowPasteToken(startsInPasteMode);
|
||||
setGrokBrowserMode(false);
|
||||
setAuthData(null);
|
||||
@@ -675,6 +679,10 @@ export default function OAuthModal({
|
||||
setIsDeviceCode(false);
|
||||
setDeviceData(null);
|
||||
setPolling(false);
|
||||
if (startsInGitlabDuoSetup) {
|
||||
setStep("gitlab-duo-setup");
|
||||
return;
|
||||
}
|
||||
if (!startsInPasteMode) startOAuthFlow();
|
||||
}, [isOpen, provider, startOAuthFlow]);
|
||||
|
||||
@@ -1028,21 +1036,18 @@ export default function OAuthModal({
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Waiting Step (Localhost - popup mode) */}
|
||||
{provider === "gitlab-duo" && step === "gitlab-duo-setup" && (
|
||||
<GitlabDuoSetupStep onContinue={() => void startOAuthFlow()} onClose={handleClose} />
|
||||
)}
|
||||
|
||||
{step === "waiting" && !isDeviceCode && (
|
||||
<div className="text-center py-6">
|
||||
<div className="size-16 mx-auto mb-4 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<span className="material-symbols-outlined text-3xl text-primary animate-spin">
|
||||
progress_activity
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold mb-2">{t("waiting")}</h3>
|
||||
<p className="text-sm text-text-muted mb-2">{t("completeAuthInPopup")}</p>
|
||||
<p className="text-xs text-text-muted mb-4 opacity-70">{t("popupClosedHint")}</p>
|
||||
<Button variant="ghost" onClick={() => setStep("input")}>
|
||||
{t("popupBlocked")}
|
||||
</Button>
|
||||
</div>
|
||||
<OAuthWaitingStep
|
||||
waitingLabel={t("waiting")}
|
||||
completeAuthLabel={t("completeAuthInPopup")}
|
||||
popupClosedHint={t("popupClosedHint")}
|
||||
popupBlockedLabel={t("popupBlocked")}
|
||||
onManualInput={() => setStep("input")}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Device Code Flow - Waiting */}
|
||||
@@ -1096,9 +1101,7 @@ export default function OAuthModal({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* LAN-IP loopback mismatch (#8046) — a dedicated, actionable panel rather
|
||||
than the generic red error step: retrying this origin cannot succeed, so
|
||||
the space goes to the diagnosis + the copy-pasteable tunnel command. */}
|
||||
{/* LAN-IP loopback mismatch (#8046) — dedicated panel; retrying this origin cannot succeed. */}
|
||||
{step === "loopback-mismatch" && loopbackHint && !showPasteToken && (
|
||||
<OAuthLoopbackMismatchPanel
|
||||
providerName={providerInfo.name}
|
||||
@@ -1107,25 +1110,20 @@ export default function OAuthModal({
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Error Step — OAuth errors only; paste-token errors shown inline */}
|
||||
{step === "error" && !showPasteToken && (
|
||||
<div className="text-center py-6">
|
||||
<div className="size-16 mx-auto mb-4 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center">
|
||||
<span className="material-symbols-outlined text-3xl text-red-600">error</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold mb-2">{t("error")}</h3>
|
||||
<p className="text-sm text-red-600 mb-4">
|
||||
<LinkifiedText text={error} />
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={() => startOAuthFlow()} variant="secondary" fullWidth>
|
||||
{t("tryAgain")}
|
||||
</Button>
|
||||
<Button onClick={handleClose} variant="ghost" fullWidth>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<OAuthErrorStep
|
||||
error={error}
|
||||
errorTitle={t("error")}
|
||||
tryAgainLabel={t("tryAgain")}
|
||||
cancelLabel={t("cancel")}
|
||||
returnToGitlabDuoSetup={provider === "gitlab-duo"}
|
||||
onReturnToGitlabDuoSetup={() => {
|
||||
setError(null);
|
||||
setStep("gitlab-duo-setup");
|
||||
}}
|
||||
onTryAgain={() => void startOAuthFlow()}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
40
src/shared/components/oauthModal/GitlabDuoSetupStep.tsx
Normal file
40
src/shared/components/oauthModal/GitlabDuoSetupStep.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import Button from "@/shared/components/Button";
|
||||
import LinkifiedText from "@/shared/components/LinkifiedText";
|
||||
import { GITLAB_DUO_OAUTH_SETUP_MESSAGE } from "@/shared/constants/gitlabDuoSetupMessage";
|
||||
|
||||
type GitlabDuoSetupStepProps = {
|
||||
onContinue: () => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* #8688 — Show GitLab Duo OAuth registration / env-var instructions *before*
|
||||
* the authorize call, so operators are not dumped onto a red error step with
|
||||
* the only copy of the setup recipe.
|
||||
*/
|
||||
export default function GitlabDuoSetupStep({ onContinue, onClose }: GitlabDuoSetupStepProps) {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="rounded-md border border-border bg-muted/40 px-3 py-3 text-left">
|
||||
<p className="text-sm font-medium mb-2">GitLab Duo OAuth setup</p>
|
||||
<p className="text-sm text-text-muted leading-relaxed">
|
||||
<LinkifiedText text={GITLAB_DUO_OAUTH_SETUP_MESSAGE} />
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs text-text-muted">
|
||||
After the application is registered and the env vars are set on this OmniRoute instance,
|
||||
click Continue to start the OAuth login.
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={onContinue} fullWidth>
|
||||
Continue
|
||||
</Button>
|
||||
<Button onClick={onClose} variant="ghost" fullWidth>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
60
src/shared/components/oauthModal/OAuthErrorStep.tsx
Normal file
60
src/shared/components/oauthModal/OAuthErrorStep.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
"use client";
|
||||
|
||||
import Button from "@/shared/components/Button";
|
||||
import LinkifiedText from "@/shared/components/LinkifiedText";
|
||||
|
||||
type OAuthErrorStepProps = {
|
||||
error: string | null;
|
||||
errorTitle: string;
|
||||
tryAgainLabel: string;
|
||||
cancelLabel: string;
|
||||
/** When true, Try Again returns to the GitLab Duo setup recipe (#8688). */
|
||||
returnToGitlabDuoSetup: boolean;
|
||||
onReturnToGitlabDuoSetup: () => void;
|
||||
onTryAgain: () => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
/** Shared OAuth error panel — paste-token errors stay inline in OAuthModal. */
|
||||
export default function OAuthErrorStep({
|
||||
error,
|
||||
errorTitle,
|
||||
tryAgainLabel,
|
||||
cancelLabel,
|
||||
returnToGitlabDuoSetup,
|
||||
onReturnToGitlabDuoSetup,
|
||||
onTryAgain,
|
||||
onClose,
|
||||
}: OAuthErrorStepProps) {
|
||||
return (
|
||||
<div className="text-center py-6">
|
||||
<div className="size-16 mx-auto mb-4 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center">
|
||||
<span className="material-symbols-outlined text-3xl text-red-600">error</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold mb-2">{errorTitle}</h3>
|
||||
<p className="text-sm text-red-600 mb-4">
|
||||
<LinkifiedText text={error} />
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={() => {
|
||||
// #8688: return to the setup recipe when still unconfigured,
|
||||
// instead of immediately re-hitting authorize → same red error.
|
||||
if (returnToGitlabDuoSetup) {
|
||||
onReturnToGitlabDuoSetup();
|
||||
return;
|
||||
}
|
||||
onTryAgain();
|
||||
}}
|
||||
variant="secondary"
|
||||
fullWidth
|
||||
>
|
||||
{tryAgainLabel}
|
||||
</Button>
|
||||
<Button onClick={onClose} variant="ghost" fullWidth>
|
||||
{cancelLabel}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
36
src/shared/components/oauthModal/OAuthWaitingStep.tsx
Normal file
36
src/shared/components/oauthModal/OAuthWaitingStep.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
"use client";
|
||||
|
||||
import Button from "@/shared/components/Button";
|
||||
|
||||
type OAuthWaitingStepProps = {
|
||||
waitingLabel: string;
|
||||
completeAuthLabel: string;
|
||||
popupClosedHint: string;
|
||||
popupBlockedLabel: string;
|
||||
onManualInput: () => void;
|
||||
};
|
||||
|
||||
/** Localhost popup-mode waiting panel while the OAuth popup completes. */
|
||||
export default function OAuthWaitingStep({
|
||||
waitingLabel,
|
||||
completeAuthLabel,
|
||||
popupClosedHint,
|
||||
popupBlockedLabel,
|
||||
onManualInput,
|
||||
}: OAuthWaitingStepProps) {
|
||||
return (
|
||||
<div className="text-center py-6">
|
||||
<div className="size-16 mx-auto mb-4 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<span className="material-symbols-outlined text-3xl text-primary animate-spin">
|
||||
progress_activity
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold mb-2">{waitingLabel}</h3>
|
||||
<p className="text-sm text-text-muted mb-2">{completeAuthLabel}</p>
|
||||
<p className="text-xs text-text-muted mb-4 opacity-70">{popupClosedHint}</p>
|
||||
<Button variant="ghost" onClick={onManualInput}>
|
||||
{popupBlockedLabel}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
18
src/shared/constants/gitlabDuoSetupMessage.ts
Normal file
18
src/shared/constants/gitlabDuoSetupMessage.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* #3861 / #8688 — GitLab Duo needs an operator-registered OAuth application.
|
||||
* Same copy is used for:
|
||||
* - catalog `authHint` (pre-connect guidance)
|
||||
* - OAuthModal setup step (before auto-start)
|
||||
* - authorize-route error when `GITLAB_DUO_OAUTH_CLIENT_ID` is unset
|
||||
*/
|
||||
export const GITLAB_DUO_OAUTH_APPLICATIONS_URL = "https://gitlab.com/-/profile/applications";
|
||||
|
||||
export const GITLAB_DUO_OAUTH_DEFAULT_REDIRECT_URI = "http://localhost:20128/callback";
|
||||
|
||||
export const GITLAB_DUO_OAUTH_SCOPES = "ai_features read_user";
|
||||
|
||||
export const GITLAB_DUO_OAUTH_SETUP_MESSAGE =
|
||||
"GitLab Duo OAuth is not configured. Register an OAuth application at " +
|
||||
`${GITLAB_DUO_OAUTH_APPLICATIONS_URL} with redirect URI ` +
|
||||
`${GITLAB_DUO_OAUTH_DEFAULT_REDIRECT_URI} and scopes "${GITLAB_DUO_OAUTH_SCOPES}", then set ` +
|
||||
"GITLAB_DUO_OAUTH_CLIENT_ID (and optionally GITLAB_DUO_OAUTH_CLIENT_SECRET) and restart.";
|
||||
@@ -2,6 +2,8 @@
|
||||
* Provider catalog data — extracted from providers.ts (god-file decomposition).
|
||||
* Pure data literal; re-exported by the providers.ts barrel. No behavior change.
|
||||
*/
|
||||
import { GITLAB_DUO_OAUTH_SETUP_MESSAGE } from "@/shared/constants/gitlabDuoSetupMessage";
|
||||
|
||||
export const OAUTH_PROVIDERS = {
|
||||
"ghe-copilot": {
|
||||
id: "ghe-copilot",
|
||||
@@ -122,8 +124,8 @@ export const OAUTH_PROVIDERS = {
|
||||
color: "#FC6D26",
|
||||
textIcon: "GL",
|
||||
website: "https://docs.gitlab.com/user/duo_agent_platform/code_suggestions/",
|
||||
authHint:
|
||||
"OAuth application with ai_features + read_user scopes. Configure GITLAB_DUO_OAUTH_CLIENT_ID and optionally GITLAB_DUO_OAUTH_CLIENT_SECRET on this OmniRoute instance.",
|
||||
// #8688: full actionable recipe (same string as authorize error + OAuthModal setup step)
|
||||
authHint: GITLAB_DUO_OAUTH_SETUP_MESSAGE,
|
||||
},
|
||||
cursor: {
|
||||
id: "cursor",
|
||||
|
||||
89
tests/unit/gitlab-duo-oauth-setup-8688.test.ts
Normal file
89
tests/unit/gitlab-duo-oauth-setup-8688.test.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* #8688 — GitLab Duo OAuth setup must be visible *before* the authorize error step.
|
||||
*
|
||||
* Previously Add Connection auto-started OAuth and the only copy of the registration
|
||||
* recipe lived in the red error body. The setup step + shared message keep that
|
||||
* recipe actionable up front; authorize still returns the same string as fallback.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import {
|
||||
GITLAB_DUO_OAUTH_APPLICATIONS_URL,
|
||||
GITLAB_DUO_OAUTH_DEFAULT_REDIRECT_URI,
|
||||
GITLAB_DUO_OAUTH_SCOPES,
|
||||
GITLAB_DUO_OAUTH_SETUP_MESSAGE,
|
||||
} from "../../src/shared/constants/gitlabDuoSetupMessage";
|
||||
import { OAUTH_PROVIDERS } from "../../src/shared/constants/providers/oauth";
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const read = (rel: string) => readFileSync(resolve(here, rel), "utf8");
|
||||
|
||||
test("#8688 setup message embeds registration URL, redirect, scopes, and env vars", () => {
|
||||
assert.match(GITLAB_DUO_OAUTH_SETUP_MESSAGE, /GitLab Duo OAuth is not configured/);
|
||||
assert.equal(GITLAB_DUO_OAUTH_APPLICATIONS_URL, "https://gitlab.com/-/profile/applications");
|
||||
assert.ok(GITLAB_DUO_OAUTH_SETUP_MESSAGE.includes(GITLAB_DUO_OAUTH_APPLICATIONS_URL));
|
||||
assert.ok(GITLAB_DUO_OAUTH_SETUP_MESSAGE.includes(GITLAB_DUO_OAUTH_DEFAULT_REDIRECT_URI));
|
||||
assert.ok(GITLAB_DUO_OAUTH_SETUP_MESSAGE.includes(`"${GITLAB_DUO_OAUTH_SCOPES}"`));
|
||||
assert.match(GITLAB_DUO_OAUTH_SETUP_MESSAGE, /GITLAB_DUO_OAUTH_CLIENT_ID/);
|
||||
assert.match(GITLAB_DUO_OAUTH_SETUP_MESSAGE, /GITLAB_DUO_OAUTH_CLIENT_SECRET/);
|
||||
});
|
||||
|
||||
test("#8688 catalog authHint shares the authorize / modal setup message", () => {
|
||||
assert.equal(
|
||||
OAUTH_PROVIDERS["gitlab-duo"].authHint,
|
||||
GITLAB_DUO_OAUTH_SETUP_MESSAGE,
|
||||
"authHint must stay byte-identical to GITLAB_DUO_OAUTH_SETUP_MESSAGE"
|
||||
);
|
||||
});
|
||||
|
||||
test("#8688 authorize route returns the shared setup message on missing client_id", () => {
|
||||
const route = read("../../src/app/api/oauth/[provider]/[action]/route.ts");
|
||||
assert.match(route, /GITLAB_DUO_OAUTH_SETUP_MESSAGE/);
|
||||
assert.match(route, /provider === "gitlab-duo" && !authData\.authUrl/);
|
||||
// No inline duplicate of the long recipe — single source of truth.
|
||||
assert.doesNotMatch(route, /Register an OAuth application at " \+\s*"https:\/\/gitlab\.com/);
|
||||
});
|
||||
|
||||
test("#8688 OAuthModal skips auto-start and renders GitlabDuoSetupStep (#8688)", () => {
|
||||
const modal = read("../../src/shared/components/OAuthModal.tsx");
|
||||
assert.match(modal, /GitlabDuoSetupStep/);
|
||||
assert.match(modal, /startsInGitlabDuoSetup/);
|
||||
assert.match(modal, /setStep\("gitlab-duo-setup"\)/);
|
||||
assert.match(modal, /step === "gitlab-duo-setup"/);
|
||||
|
||||
// Auto-start must be skipped when the setup step is active.
|
||||
const openEffect = modal.match(
|
||||
/\/\/ Reset state and start OAuth when modal opens[\s\S]*?}, \[isOpen, provider, startOAuthFlow\]\);/
|
||||
);
|
||||
assert.ok(openEffect, "expected the modal-open useEffect");
|
||||
assert.match(openEffect![0], /startsInGitlabDuoSetup/);
|
||||
assert.match(openEffect![0], /setStep\("gitlab-duo-setup"\)/);
|
||||
assert.match(openEffect![0], /return;/);
|
||||
assert.match(
|
||||
openEffect![0],
|
||||
/if \(!startsInPasteMode\) startOAuthFlow\(\)/,
|
||||
"other providers still auto-start"
|
||||
);
|
||||
|
||||
const setup = read("../../src/shared/components/oauthModal/GitlabDuoSetupStep.tsx");
|
||||
assert.match(setup, /GITLAB_DUO_OAUTH_SETUP_MESSAGE/);
|
||||
assert.match(setup, /LinkifiedText/);
|
||||
assert.match(setup, /Continue/);
|
||||
});
|
||||
|
||||
test("#8688 error Try Again returns gitlab-duo to the setup step", () => {
|
||||
const modal = read("../../src/shared/components/OAuthModal.tsx");
|
||||
assert.match(modal, /returnToGitlabDuoSetup=\{provider === "gitlab-duo"\}/);
|
||||
assert.match(modal, /setStep\("gitlab-duo-setup"\)/);
|
||||
|
||||
const errorStep = read("../../src/shared/components/oauthModal/OAuthErrorStep.tsx");
|
||||
assert.match(errorStep, /returnToGitlabDuoSetup/);
|
||||
assert.match(errorStep, /onReturnToGitlabDuoSetup/);
|
||||
assert.match(
|
||||
errorStep,
|
||||
/if \(returnToGitlabDuoSetup\)[\s\S]{0,80}?onReturnToGitlabDuoSetup\(\)/
|
||||
);
|
||||
});
|
||||
@@ -3,14 +3,21 @@ import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
const modalSource = readFileSync("src/shared/components/OAuthModal.tsx", "utf8");
|
||||
const errorStepSource = readFileSync(
|
||||
"src/shared/components/oauthModal/OAuthErrorStep.tsx",
|
||||
"utf8"
|
||||
);
|
||||
|
||||
test("OAuthModal narrows failed Grok paste results before reading the error", () => {
|
||||
assert.match(modalSource, /if \(parsed\.ok === false\) \{\s*setError\(parsed\.error\);/);
|
||||
});
|
||||
|
||||
test("OAuthModal adapts the retry action to the button click handler", () => {
|
||||
// #8688 extracted the error panel; retry stays a Button onClick in OAuthErrorStep,
|
||||
// wired from OAuthModal via onTryAgain → startOAuthFlow.
|
||||
assert.match(modalSource, /onTryAgain=\{\(\) => void startOAuthFlow\(\)\}/);
|
||||
assert.match(
|
||||
modalSource,
|
||||
/<Button onClick=\{\(\) => startOAuthFlow\(\)\} variant="secondary" fullWidth>/
|
||||
errorStepSource,
|
||||
/<Button\s+onClick=\{\(\) => \{[\s\S]*?onTryAgain\(\);[\s\S]*?\}\}\s+variant="secondary"\s+fullWidth\s*>/
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user