mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-01-09 14:57:59 -05:00
[Feat]: Support self hosted gitlab + enterprise github (#8274)
Co-authored-by: openhands <openhands@all-hands.dev> Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
This commit is contained in:
@@ -138,8 +138,8 @@ describe("Content", () => {
|
|||||||
getSettingsSpy.mockResolvedValue({
|
getSettingsSpy.mockResolvedValue({
|
||||||
...MOCK_DEFAULT_USER_SETTINGS,
|
...MOCK_DEFAULT_USER_SETTINGS,
|
||||||
provider_tokens_set: {
|
provider_tokens_set: {
|
||||||
github: "some-token",
|
github: null,
|
||||||
gitlab: "some-token",
|
gitlab: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
queryClient.invalidateQueries();
|
queryClient.invalidateQueries();
|
||||||
@@ -163,7 +163,7 @@ describe("Content", () => {
|
|||||||
getSettingsSpy.mockResolvedValue({
|
getSettingsSpy.mockResolvedValue({
|
||||||
...MOCK_DEFAULT_USER_SETTINGS,
|
...MOCK_DEFAULT_USER_SETTINGS,
|
||||||
provider_tokens_set: {
|
provider_tokens_set: {
|
||||||
gitlab: "some-token",
|
gitlab: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
queryClient.invalidateQueries();
|
queryClient.invalidateQueries();
|
||||||
@@ -241,8 +241,8 @@ describe("Form submission", () => {
|
|||||||
await userEvent.click(submit);
|
await userEvent.click(submit);
|
||||||
|
|
||||||
expect(saveProvidersSpy).toHaveBeenCalledWith({
|
expect(saveProvidersSpy).toHaveBeenCalledWith({
|
||||||
github: { token: "test-token" },
|
github: { token: "test-token", host: "" },
|
||||||
gitlab: { token: "" },
|
gitlab: { token: "", host: "" },
|
||||||
});
|
});
|
||||||
|
|
||||||
const gitlabInput = await screen.findByTestId("gitlab-token-input");
|
const gitlabInput = await screen.findByTestId("gitlab-token-input");
|
||||||
@@ -250,8 +250,8 @@ describe("Form submission", () => {
|
|||||||
await userEvent.click(submit);
|
await userEvent.click(submit);
|
||||||
|
|
||||||
expect(saveProvidersSpy).toHaveBeenCalledWith({
|
expect(saveProvidersSpy).toHaveBeenCalledWith({
|
||||||
github: { token: "test-token" },
|
github: { token: "test-token", host: "" },
|
||||||
gitlab: { token: "" },
|
gitlab: { token: "", host: "" },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -290,7 +290,7 @@ describe("Form submission", () => {
|
|||||||
...MOCK_DEFAULT_USER_SETTINGS,
|
...MOCK_DEFAULT_USER_SETTINGS,
|
||||||
provider_tokens_set: {
|
provider_tokens_set: {
|
||||||
github: null,
|
github: null,
|
||||||
gitlab: "some-token",
|
gitlab: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -321,7 +321,7 @@ describe("Form submission", () => {
|
|||||||
...MOCK_DEFAULT_USER_SETTINGS,
|
...MOCK_DEFAULT_USER_SETTINGS,
|
||||||
provider_tokens_set: {
|
provider_tokens_set: {
|
||||||
github: null,
|
github: null,
|
||||||
gitlab: "some-token",
|
gitlab: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ describe("HomeScreen", () => {
|
|||||||
getSettingsSpy.mockResolvedValue({
|
getSettingsSpy.mockResolvedValue({
|
||||||
...MOCK_DEFAULT_USER_SETTINGS,
|
...MOCK_DEFAULT_USER_SETTINGS,
|
||||||
provider_tokens_set: {
|
provider_tokens_set: {
|
||||||
github: "some-token",
|
github: null,
|
||||||
gitlab: null,
|
gitlab: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export function ConfigureGitHubRepositoriesAnchor({
|
|||||||
href={`https://github.com/apps/${slug}/installations/new`}
|
href={`https://github.com/apps/${slug}/installations/new`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
className="px-11 py-9"
|
className="py-9"
|
||||||
>
|
>
|
||||||
<BrandButton type="button" variant="secondary">
|
<BrandButton type="button" variant="secondary">
|
||||||
{t(I18nKey.GITHUB$CONFIGURE_REPOS)}
|
{t(I18nKey.GITHUB$CONFIGURE_REPOS)}
|
||||||
|
|||||||
@@ -6,14 +6,18 @@ import { KeyStatusIcon } from "../key-status-icon";
|
|||||||
|
|
||||||
interface GitHubTokenInputProps {
|
interface GitHubTokenInputProps {
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
|
onGitHubHostChange: (value: string) => void;
|
||||||
isGitHubTokenSet: boolean;
|
isGitHubTokenSet: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
|
githubHostSet: string | null | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GitHubTokenInput({
|
export function GitHubTokenInput({
|
||||||
onChange,
|
onChange,
|
||||||
|
onGitHubHostChange,
|
||||||
isGitHubTokenSet,
|
isGitHubTokenSet,
|
||||||
name,
|
name,
|
||||||
|
githubHostSet,
|
||||||
}: GitHubTokenInputProps) {
|
}: GitHubTokenInputProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -37,6 +41,24 @@ export function GitHubTokenInput({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SettingsInput
|
||||||
|
onChange={onGitHubHostChange || (() => {})}
|
||||||
|
name="github-host-input"
|
||||||
|
testId="github-host-input"
|
||||||
|
label={t(I18nKey.GITHUB$HOST_LABEL)}
|
||||||
|
type="text"
|
||||||
|
className="w-[680px]"
|
||||||
|
placeholder="github.com"
|
||||||
|
defaultValue={githubHostSet || undefined}
|
||||||
|
startContent={
|
||||||
|
githubHostSet && githubHostSet.trim() !== "" ? (
|
||||||
|
<KeyStatusIcon testId="gh-set-host-indicator" isSet />
|
||||||
|
) : (
|
||||||
|
<KeyStatusIcon testId="gh-set-host-indicator" isSet={false} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<GitHubTokenHelpAnchor />
|
<GitHubTokenHelpAnchor />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,14 +6,18 @@ import { KeyStatusIcon } from "../key-status-icon";
|
|||||||
|
|
||||||
interface GitLabTokenInputProps {
|
interface GitLabTokenInputProps {
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
|
onGitLabHostChange: (value: string) => void;
|
||||||
isGitLabTokenSet: boolean;
|
isGitLabTokenSet: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
|
gitlabHostSet: string | null | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GitLabTokenInput({
|
export function GitLabTokenInput({
|
||||||
onChange,
|
onChange,
|
||||||
|
onGitLabHostChange,
|
||||||
isGitLabTokenSet,
|
isGitLabTokenSet,
|
||||||
name,
|
name,
|
||||||
|
gitlabHostSet,
|
||||||
}: GitLabTokenInputProps) {
|
}: GitLabTokenInputProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -37,6 +41,24 @@ export function GitLabTokenInput({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SettingsInput
|
||||||
|
onChange={onGitLabHostChange || (() => {})}
|
||||||
|
name="gitlab-host-input"
|
||||||
|
testId="gitlab-host-input"
|
||||||
|
label={t(I18nKey.GITLAB$HOST_LABEL)}
|
||||||
|
type="text"
|
||||||
|
className="w-[680px]"
|
||||||
|
placeholder="gitlab.com"
|
||||||
|
defaultValue={gitlabHostSet || undefined}
|
||||||
|
startContent={
|
||||||
|
gitlabHostSet && gitlabHostSet.trim() !== "" ? (
|
||||||
|
<KeyStatusIcon testId="gl-set-host-indicator" isSet />
|
||||||
|
) : (
|
||||||
|
<KeyStatusIcon testId="gl-set-host-indicator" isSet={false} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<GitLabTokenHelpAnchor />
|
<GitLabTokenHelpAnchor />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ export enum I18nKey {
|
|||||||
EXIT_PROJECT$TITLE = "EXIT_PROJECT$TITLE",
|
EXIT_PROJECT$TITLE = "EXIT_PROJECT$TITLE",
|
||||||
LANGUAGE$LABEL = "LANGUAGE$LABEL",
|
LANGUAGE$LABEL = "LANGUAGE$LABEL",
|
||||||
GITHUB$TOKEN_LABEL = "GITHUB$TOKEN_LABEL",
|
GITHUB$TOKEN_LABEL = "GITHUB$TOKEN_LABEL",
|
||||||
|
GITHUB$HOST_LABEL = "GITHUB$HOST_LABEL",
|
||||||
GITHUB$TOKEN_OPTIONAL = "GITHUB$TOKEN_OPTIONAL",
|
GITHUB$TOKEN_OPTIONAL = "GITHUB$TOKEN_OPTIONAL",
|
||||||
GITHUB$GET_TOKEN = "GITHUB$GET_TOKEN",
|
GITHUB$GET_TOKEN = "GITHUB$GET_TOKEN",
|
||||||
GITHUB$TOKEN_HELP_TEXT = "GITHUB$TOKEN_HELP_TEXT",
|
GITHUB$TOKEN_HELP_TEXT = "GITHUB$TOKEN_HELP_TEXT",
|
||||||
@@ -483,6 +484,7 @@ export enum I18nKey {
|
|||||||
MODEL_SELECTOR$VERIFIED = "MODEL_SELECTOR$VERIFIED",
|
MODEL_SELECTOR$VERIFIED = "MODEL_SELECTOR$VERIFIED",
|
||||||
MODEL_SELECTOR$OTHERS = "MODEL_SELECTOR$OTHERS",
|
MODEL_SELECTOR$OTHERS = "MODEL_SELECTOR$OTHERS",
|
||||||
GITLAB$TOKEN_LABEL = "GITLAB$TOKEN_LABEL",
|
GITLAB$TOKEN_LABEL = "GITLAB$TOKEN_LABEL",
|
||||||
|
GITLAB$HOST_LABEL = "GITLAB$HOST_LABEL",
|
||||||
GITLAB$GET_TOKEN = "GITLAB$GET_TOKEN",
|
GITLAB$GET_TOKEN = "GITLAB$GET_TOKEN",
|
||||||
GITLAB$TOKEN_HELP_TEXT = "GITLAB$TOKEN_HELP_TEXT",
|
GITLAB$TOKEN_HELP_TEXT = "GITLAB$TOKEN_HELP_TEXT",
|
||||||
GITLAB$TOKEN_LINK_TEXT = "GITLAB$TOKEN_LINK_TEXT",
|
GITLAB$TOKEN_LINK_TEXT = "GITLAB$TOKEN_LINK_TEXT",
|
||||||
|
|||||||
@@ -2145,6 +2145,21 @@
|
|||||||
"de": "GitHub-Token",
|
"de": "GitHub-Token",
|
||||||
"uk": "GitHub Токен"
|
"uk": "GitHub Токен"
|
||||||
},
|
},
|
||||||
|
"GITHUB$HOST_LABEL": {
|
||||||
|
"en": "GitHub Host (optional)",
|
||||||
|
"ja": "GitHubホスト (オプション)",
|
||||||
|
"zh-CN": "GitHub主机 (可选)",
|
||||||
|
"zh-TW": "GitHub主機 (選填)",
|
||||||
|
"ko-KR": "GitHub 호스트 (선택사항)",
|
||||||
|
"no": "GitHub-vert (valgfritt)",
|
||||||
|
"it": "Host GitHub (opzionale)",
|
||||||
|
"pt": "Host do GitHub (opcional)",
|
||||||
|
"es": "Host de GitHub (opcional)",
|
||||||
|
"ar": "مضيف GitHub (اختياري)",
|
||||||
|
"fr": "Hôte GitHub (optionnel)",
|
||||||
|
"tr": "GitHub Sunucusu (isteğe bağlı)",
|
||||||
|
"de": "GitHub-Host (optional)"
|
||||||
|
},
|
||||||
"GITHUB$TOKEN_OPTIONAL": {
|
"GITHUB$TOKEN_OPTIONAL": {
|
||||||
"en": "GitHub Token (Optional)",
|
"en": "GitHub Token (Optional)",
|
||||||
"ja": "GitHubトークン(任意)",
|
"ja": "GitHubトークン(任意)",
|
||||||
@@ -7432,6 +7447,21 @@
|
|||||||
"de": "GitLab-Token",
|
"de": "GitLab-Token",
|
||||||
"uk": "GitLab токен"
|
"uk": "GitLab токен"
|
||||||
},
|
},
|
||||||
|
"GITLAB$HOST_LABEL": {
|
||||||
|
"en": "GitLab Host (optional)",
|
||||||
|
"ja": "GitLabホスト (オプション)",
|
||||||
|
"zh-CN": "GitLab主机 (可选)",
|
||||||
|
"zh-TW": "GitLab主機 (選填)",
|
||||||
|
"ko-KR": "GitLab 호스트 (선택사항)",
|
||||||
|
"no": "GitLab-vert (valgfritt)",
|
||||||
|
"it": "Host GitLab (opzionale)",
|
||||||
|
"pt": "Host do GitLab (opcional)",
|
||||||
|
"es": "Host de GitLab (opcional)",
|
||||||
|
"ar": "مضيف GitLab (اختياري)",
|
||||||
|
"fr": "Hôte GitLab (optionnel)",
|
||||||
|
"tr": "GitLab Sunucusu (isteğe bağlı)",
|
||||||
|
"de": "GitLab-Host (optional)"
|
||||||
|
},
|
||||||
"GITLAB$GET_TOKEN": {
|
"GITLAB$GET_TOKEN": {
|
||||||
"en": "Generate a token on",
|
"en": "Generate a token on",
|
||||||
"ja": "トークンを生成する",
|
"ja": "トークンを生成する",
|
||||||
|
|||||||
@@ -23,8 +23,9 @@ function GitSettingsScreen() {
|
|||||||
const { mutate: saveGitProviders, isPending } = useAddGitProviders();
|
const { mutate: saveGitProviders, isPending } = useAddGitProviders();
|
||||||
const { mutate: disconnectGitTokens } = useLogout();
|
const { mutate: disconnectGitTokens } = useLogout();
|
||||||
|
|
||||||
|
const { data: settings, isLoading } = useSettings();
|
||||||
const { providers } = useUserProviders();
|
const { providers } = useUserProviders();
|
||||||
const { isLoading } = useSettings();
|
|
||||||
const { data: config } = useConfig();
|
const { data: config } = useConfig();
|
||||||
|
|
||||||
const [githubTokenInputHasValue, setGithubTokenInputHasValue] =
|
const [githubTokenInputHasValue, setGithubTokenInputHasValue] =
|
||||||
@@ -32,6 +33,14 @@ function GitSettingsScreen() {
|
|||||||
const [gitlabTokenInputHasValue, setGitlabTokenInputHasValue] =
|
const [gitlabTokenInputHasValue, setGitlabTokenInputHasValue] =
|
||||||
React.useState(false);
|
React.useState(false);
|
||||||
|
|
||||||
|
const [githubHostInputHasValue, setGithubHostInputHasValue] =
|
||||||
|
React.useState(false);
|
||||||
|
const [gitlabHostInputHasValue, setGitlabHostInputHasValue] =
|
||||||
|
React.useState(false);
|
||||||
|
|
||||||
|
const existingGithubHost = settings?.PROVIDER_TOKENS_SET.github;
|
||||||
|
const existingGitlabHost = settings?.PROVIDER_TOKENS_SET.gitlab;
|
||||||
|
|
||||||
const isSaas = config?.APP_MODE === "saas";
|
const isSaas = config?.APP_MODE === "saas";
|
||||||
const isGitHubTokenSet = providers.includes("github");
|
const isGitHubTokenSet = providers.includes("github");
|
||||||
const isGitLabTokenSet = providers.includes("gitlab");
|
const isGitLabTokenSet = providers.includes("gitlab");
|
||||||
@@ -47,12 +56,14 @@ function GitSettingsScreen() {
|
|||||||
|
|
||||||
const githubToken = formData.get("github-token-input")?.toString() || "";
|
const githubToken = formData.get("github-token-input")?.toString() || "";
|
||||||
const gitlabToken = formData.get("gitlab-token-input")?.toString() || "";
|
const gitlabToken = formData.get("gitlab-token-input")?.toString() || "";
|
||||||
|
const githubHost = formData.get("github-host-input")?.toString() || "";
|
||||||
|
const gitlabHost = formData.get("gitlab-host-input")?.toString() || "";
|
||||||
|
|
||||||
saveGitProviders(
|
saveGitProviders(
|
||||||
{
|
{
|
||||||
providers: {
|
providers: {
|
||||||
github: { token: githubToken },
|
github: { token: githubToken, host: githubHost },
|
||||||
gitlab: { token: gitlabToken },
|
gitlab: { token: gitlabToken, host: gitlabHost },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -66,12 +77,18 @@ function GitSettingsScreen() {
|
|||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
setGithubTokenInputHasValue(false);
|
setGithubTokenInputHasValue(false);
|
||||||
setGitlabTokenInputHasValue(false);
|
setGitlabTokenInputHasValue(false);
|
||||||
|
setGithubHostInputHasValue(false);
|
||||||
|
setGitlabHostInputHasValue(false);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const formIsClean = !githubTokenInputHasValue && !gitlabTokenInputHasValue;
|
const formIsClean =
|
||||||
|
!githubTokenInputHasValue &&
|
||||||
|
!gitlabTokenInputHasValue &&
|
||||||
|
!githubHostInputHasValue &&
|
||||||
|
!gitlabHostInputHasValue;
|
||||||
const shouldRenderExternalConfigureButtons = isSaas && config.APP_SLUG;
|
const shouldRenderExternalConfigureButtons = isSaas && config.APP_SLUG;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -80,55 +97,68 @@ function GitSettingsScreen() {
|
|||||||
action={formAction}
|
action={formAction}
|
||||||
className="flex flex-col h-full justify-between"
|
className="flex flex-col h-full justify-between"
|
||||||
>
|
>
|
||||||
|
{!isLoading && (
|
||||||
|
<div className="p-9 flex flex-col gap-12">
|
||||||
|
{shouldRenderExternalConfigureButtons && !isLoading && (
|
||||||
|
<ConfigureGitHubRepositoriesAnchor slug={config.APP_SLUG!} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isSaas && (
|
||||||
|
<GitHubTokenInput
|
||||||
|
name="github-token-input"
|
||||||
|
isGitHubTokenSet={isGitHubTokenSet}
|
||||||
|
onChange={(value) => {
|
||||||
|
setGithubTokenInputHasValue(!!value);
|
||||||
|
}}
|
||||||
|
onGitHubHostChange={(value) => {
|
||||||
|
setGitlabHostInputHasValue(!!value);
|
||||||
|
}}
|
||||||
|
githubHostSet={existingGithubHost}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isSaas && (
|
||||||
|
<GitLabTokenInput
|
||||||
|
name="gitlab-token-input"
|
||||||
|
isGitLabTokenSet={isGitLabTokenSet}
|
||||||
|
onChange={(value) => {
|
||||||
|
setGitlabTokenInputHasValue(!!value);
|
||||||
|
}}
|
||||||
|
onGitLabHostChange={(value) => {
|
||||||
|
setGitlabHostInputHasValue(!!value);
|
||||||
|
}}
|
||||||
|
gitlabHostSet={existingGitlabHost}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{isLoading && <GitSettingInputsSkeleton />}
|
{isLoading && <GitSettingInputsSkeleton />}
|
||||||
|
|
||||||
{shouldRenderExternalConfigureButtons && !isLoading && (
|
<div className="flex gap-6 p-6 justify-end border-t border-t-tertiary">
|
||||||
<ConfigureGitHubRepositoriesAnchor slug={config.APP_SLUG!} />
|
{!shouldRenderExternalConfigureButtons && (
|
||||||
)}
|
<>
|
||||||
|
<BrandButton
|
||||||
{!isSaas && !isLoading && (
|
testId="disconnect-tokens-button"
|
||||||
<div className="p-9 flex flex-col gap-12">
|
name="disconnect-tokens-button"
|
||||||
<GitHubTokenInput
|
type="submit"
|
||||||
name="github-token-input"
|
variant="secondary"
|
||||||
isGitHubTokenSet={isGitHubTokenSet}
|
isDisabled={!isGitHubTokenSet && !isGitLabTokenSet}
|
||||||
onChange={(value) => {
|
>
|
||||||
setGithubTokenInputHasValue(!!value);
|
Disconnect Tokens
|
||||||
}}
|
</BrandButton>
|
||||||
/>
|
<BrandButton
|
||||||
|
testId="submit-button"
|
||||||
<GitLabTokenInput
|
type="submit"
|
||||||
name="gitlab-token-input"
|
variant="primary"
|
||||||
isGitLabTokenSet={isGitLabTokenSet}
|
isDisabled={isPending || formIsClean}
|
||||||
onChange={(value) => {
|
>
|
||||||
setGitlabTokenInputHasValue(!!value);
|
{!isPending && t("SETTINGS$SAVE_CHANGES")}
|
||||||
}}
|
{isPending && t("SETTINGS$SAVING")}
|
||||||
/>
|
</BrandButton>
|
||||||
</div>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
{!shouldRenderExternalConfigureButtons && (
|
|
||||||
<div className="flex gap-6 p-6 justify-end border-t border-t-tertiary">
|
|
||||||
<BrandButton
|
|
||||||
testId="disconnect-tokens-button"
|
|
||||||
name="disconnect-tokens-button"
|
|
||||||
type="submit"
|
|
||||||
variant="secondary"
|
|
||||||
isDisabled={!isGitHubTokenSet && !isGitLabTokenSet}
|
|
||||||
>
|
|
||||||
Disconnect Tokens
|
|
||||||
</BrandButton>
|
|
||||||
|
|
||||||
<BrandButton
|
|
||||||
testId="submit-button"
|
|
||||||
type="submit"
|
|
||||||
variant="primary"
|
|
||||||
isDisabled={isPending || formIsClean}
|
|
||||||
>
|
|
||||||
{!isPending && t("SETTINGS$SAVE_CHANGES")}
|
|
||||||
{isPending && t("SETTINGS$SAVING")}
|
|
||||||
</BrandButton>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export type Provider = keyof typeof ProviderOptions;
|
|||||||
|
|
||||||
export type ProviderToken = {
|
export type ProviderToken = {
|
||||||
token: string;
|
token: string;
|
||||||
|
host: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MCPSSEServer = {
|
export type MCPSSEServer = {
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class GitHubService(BaseGitService, GitService):
|
|||||||
if token:
|
if token:
|
||||||
self.token = token
|
self.token = token
|
||||||
|
|
||||||
if base_domain:
|
if base_domain and base_domain != 'github.com':
|
||||||
self.BASE_URL = f'https://{base_domain}/api/v3'
|
self.BASE_URL = f'https://{base_domain}/api/v3'
|
||||||
|
|
||||||
self.external_auth_id = external_auth_id
|
self.external_auth_id = external_auth_id
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ from openhands.server.types import AppMode
|
|||||||
class ProviderToken(BaseModel):
|
class ProviderToken(BaseModel):
|
||||||
token: SecretStr | None = Field(default=None)
|
token: SecretStr | None = Field(default=None)
|
||||||
user_id: str | None = Field(default=None)
|
user_id: str | None = Field(default=None)
|
||||||
|
host: str | None = Field(default=None)
|
||||||
|
|
||||||
model_config = {
|
model_config = {
|
||||||
'frozen': True, # Makes the entire model immutable
|
'frozen': True, # Makes the entire model immutable
|
||||||
@@ -40,7 +41,7 @@ class ProviderToken(BaseModel):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def from_value(cls, token_value: ProviderToken | dict[str, str]) -> ProviderToken:
|
def from_value(cls, token_value: ProviderToken | dict[str, str]) -> ProviderToken:
|
||||||
"""Factory method to create a ProviderToken from various input types"""
|
"""Factory method to create a ProviderToken from various input types"""
|
||||||
if isinstance(token_value, ProviderToken):
|
if isinstance(token_value, cls):
|
||||||
return token_value
|
return token_value
|
||||||
elif isinstance(token_value, dict):
|
elif isinstance(token_value, dict):
|
||||||
token_str = token_value.get('token', '')
|
token_str = token_value.get('token', '')
|
||||||
@@ -49,10 +50,11 @@ class ProviderToken(BaseModel):
|
|||||||
if token_str is None:
|
if token_str is None:
|
||||||
token_str = ''
|
token_str = ''
|
||||||
user_id = token_value.get('user_id')
|
user_id = token_value.get('user_id')
|
||||||
return cls(token=SecretStr(token_str), user_id=user_id)
|
host = token_value.get('host')
|
||||||
|
return cls(token=SecretStr(token_str), user_id=user_id, host=host)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise ValueError('Unsupport Provider token type')
|
raise ValueError('Unsupported Provider token type')
|
||||||
|
|
||||||
|
|
||||||
PROVIDER_TOKEN_TYPE = MappingProxyType[ProviderType, ProviderToken]
|
PROVIDER_TOKEN_TYPE = MappingProxyType[ProviderType, ProviderToken]
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ from fastapi import APIRouter, Depends, status
|
|||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
from openhands.core.logger import openhands_logger as logger
|
from openhands.core.logger import openhands_logger as logger
|
||||||
|
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE, ProviderToken
|
||||||
|
from openhands.integrations.service_types import ProviderType
|
||||||
from openhands.integrations.utils import validate_provider_token
|
from openhands.integrations.utils import validate_provider_token
|
||||||
from openhands.server.settings import (
|
from openhands.server.settings import (
|
||||||
GETCustomSecrets,
|
GETCustomSecrets,
|
||||||
@@ -9,6 +11,7 @@ from openhands.server.settings import (
|
|||||||
POSTProviderModel,
|
POSTProviderModel,
|
||||||
)
|
)
|
||||||
from openhands.server.user_auth import (
|
from openhands.server.user_auth import (
|
||||||
|
get_provider_tokens,
|
||||||
get_secrets_store,
|
get_secrets_store,
|
||||||
get_user_secrets,
|
get_user_secrets,
|
||||||
)
|
)
|
||||||
@@ -51,24 +54,44 @@ async def invalidate_legacy_secrets_store(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def check_provider_tokens(provider_info: POSTProviderModel) -> str:
|
def process_token_validation_result(
|
||||||
if provider_info.provider_tokens:
|
confirmed_token_type: ProviderType | None,
|
||||||
# Determine whether tokens are valid
|
token_type: ProviderType):
|
||||||
for token_type, token_value in provider_info.provider_tokens.items():
|
|
||||||
if token_value.token:
|
if not confirmed_token_type or confirmed_token_type != token_type:
|
||||||
confirmed_token_type = await validate_provider_token(token_value.token)
|
return f'Invalid token. Please make sure it is a valid {token_type.value} token.'
|
||||||
if not confirmed_token_type or confirmed_token_type != token_type:
|
|
||||||
return f'Invalid token. Please make sure it is a valid {token_type.value} token.'
|
|
||||||
|
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
async def check_provider_tokens(
|
||||||
|
incoming_provider_tokens: POSTProviderModel,
|
||||||
|
existing_provider_tokens: PROVIDER_TOKEN_TYPE) -> str:
|
||||||
|
|
||||||
|
msg = ''
|
||||||
|
if incoming_provider_tokens.provider_tokens:
|
||||||
|
# Determine whether tokens are valid
|
||||||
|
for token_type, token_value in incoming_provider_tokens.provider_tokens.items():
|
||||||
|
if token_value.token:
|
||||||
|
confirmed_token_type = await validate_provider_token(token_value.token, token_value.host) # FE always sends latest host
|
||||||
|
msg = process_token_validation_result(confirmed_token_type, token_type)
|
||||||
|
|
||||||
|
existing_token = existing_provider_tokens.get(token_type, None)
|
||||||
|
if existing_token and (existing_token.host != token_value.host) and existing_token.token:
|
||||||
|
confirmed_token_type = await validate_provider_token(existing_token.token, token_value.host) # Host has changed, check it against existing token
|
||||||
|
if not confirmed_token_type or confirmed_token_type != token_type:
|
||||||
|
msg = process_token_validation_result(confirmed_token_type, token_type)
|
||||||
|
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
@app.post('/add-git-providers')
|
@app.post('/add-git-providers')
|
||||||
async def store_provider_tokens(
|
async def store_provider_tokens(
|
||||||
provider_info: POSTProviderModel,
|
provider_info: POSTProviderModel,
|
||||||
secrets_store: SecretsStore = Depends(get_secrets_store),
|
secrets_store: SecretsStore = Depends(get_secrets_store),
|
||||||
|
provider_tokens: PROVIDER_TOKEN_TYPE = Depends(get_provider_tokens)
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
provider_err_msg = await check_provider_tokens(provider_info)
|
|
||||||
|
provider_err_msg = await check_provider_tokens(provider_info, provider_tokens)
|
||||||
if provider_err_msg:
|
if provider_err_msg:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
@@ -90,8 +113,7 @@ async def store_provider_tokens(
|
|||||||
if existing_token and existing_token.token:
|
if existing_token and existing_token.token:
|
||||||
provider_info.provider_tokens[provider] = existing_token
|
provider_info.provider_tokens[provider] = existing_token
|
||||||
|
|
||||||
else: # nothing passed in means keep current settings
|
provider_info.provider_tokens[provider] = provider_info.provider_tokens[provider].model_copy(update={'host': token_value.host})
|
||||||
provider_info.provider_tokens = dict(user_secrets.provider_tokens)
|
|
||||||
|
|
||||||
updated_secrets = user_secrets.model_copy(
|
updated_secrets = user_secrets.model_copy(
|
||||||
update={'provider_tokens': provider_info.provider_tokens}
|
update={'provider_tokens': provider_info.provider_tokens}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from openhands.server.settings import (
|
|||||||
GETSettingsModel,
|
GETSettingsModel,
|
||||||
)
|
)
|
||||||
from openhands.server.shared import config
|
from openhands.server.shared import config
|
||||||
|
from openhands.server.types import AppMode
|
||||||
from openhands.server.user_auth import (
|
from openhands.server.user_auth import (
|
||||||
get_provider_tokens,
|
get_provider_tokens,
|
||||||
get_secrets_store,
|
get_secrets_store,
|
||||||
@@ -19,6 +20,7 @@ from openhands.server.user_auth import (
|
|||||||
from openhands.storage.data_models.settings import Settings
|
from openhands.storage.data_models.settings import Settings
|
||||||
from openhands.storage.secrets.secrets_store import SecretsStore
|
from openhands.storage.secrets.secrets_store import SecretsStore
|
||||||
from openhands.storage.settings.settings_store import SettingsStore
|
from openhands.storage.settings.settings_store import SettingsStore
|
||||||
|
from openhands.server.shared import server_config
|
||||||
|
|
||||||
app = APIRouter(prefix='/api')
|
app = APIRouter(prefix='/api')
|
||||||
|
|
||||||
@@ -38,10 +40,13 @@ async def load_settings(
|
|||||||
content={'error': 'Settings not found'},
|
content={'error': 'Settings not found'},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# On initial load, user secrets may not be populated with values migrated from settings store
|
# On initial load, user secrets may not be populated with values migrated from settings store
|
||||||
user_secrets = await invalidate_legacy_secrets_store(
|
user_secrets = await invalidate_legacy_secrets_store(
|
||||||
settings, settings_store, secrets_store
|
settings, settings_store, secrets_store
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# If invalidation is successful, then the returned user secrets holds the most recent values
|
# If invalidation is successful, then the returned user secrets holds the most recent values
|
||||||
git_providers = (
|
git_providers = (
|
||||||
user_secrets.provider_tokens if user_secrets else provider_tokens
|
user_secrets.provider_tokens if user_secrets else provider_tokens
|
||||||
@@ -51,7 +56,8 @@ async def load_settings(
|
|||||||
if git_providers:
|
if git_providers:
|
||||||
for provider_type, provider_token in git_providers.items():
|
for provider_type, provider_token in git_providers.items():
|
||||||
if provider_token.token or provider_token.user_id:
|
if provider_token.token or provider_token.user_id:
|
||||||
provider_tokens_set[provider_type] = None
|
provider_tokens_set[provider_type] = provider_token.host
|
||||||
|
|
||||||
|
|
||||||
settings_with_token_data = GETSettingsModel(
|
settings_with_token_data = GETSettingsModel(
|
||||||
**settings.model_dump(exclude='secrets_store'),
|
**settings.model_dump(exclude='secrets_store'),
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ class GETSettingsModel(Settings):
|
|||||||
)
|
)
|
||||||
llm_api_key_set: bool
|
llm_api_key_set: bool
|
||||||
|
|
||||||
|
model_config = {'use_enum_values': True}
|
||||||
|
|
||||||
|
|
||||||
class GETCustomSecrets(BaseModel):
|
class GETCustomSecrets(BaseModel):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class UserSecrets(BaseModel):
|
|||||||
expose_secrets = info.context and info.context.get('expose_secrets', False)
|
expose_secrets = info.context and info.context.get('expose_secrets', False)
|
||||||
|
|
||||||
for token_type, provider_token in provider_tokens.items():
|
for token_type, provider_token in provider_tokens.items():
|
||||||
if not provider_token or not provider_token.token:
|
if not provider_token:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
token_type_str = (
|
token_type_str = (
|
||||||
@@ -53,11 +53,16 @@ class UserSecrets(BaseModel):
|
|||||||
if isinstance(token_type, ProviderType)
|
if isinstance(token_type, ProviderType)
|
||||||
else str(token_type)
|
else str(token_type)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
token = None
|
||||||
|
if provider_token.token:
|
||||||
|
token = provider_token.token.get_secret_value() if expose_secrets else pydantic_encoder(provider_token.token)
|
||||||
|
|
||||||
tokens[token_type_str] = {
|
tokens[token_type_str] = {
|
||||||
'token': provider_token.token.get_secret_value()
|
'token': token,
|
||||||
if expose_secrets
|
'host': provider_token.host,
|
||||||
else pydantic_encoder(provider_token.token),
|
|
||||||
'user_id': provider_token.user_id,
|
'user_id': provider_token.user_id,
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return tokens
|
return tokens
|
||||||
|
|||||||
@@ -283,3 +283,152 @@ async def test_delete_nonexistent_custom_secret(test_client, file_secrets_store)
|
|||||||
|
|
||||||
# Check that other settings were preserved
|
# Check that other settings were preserved
|
||||||
assert ProviderType.GITHUB in stored_settings.provider_tokens
|
assert ProviderType.GITHUB in stored_settings.provider_tokens
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_git_providers_with_host(test_client, file_secrets_store):
|
||||||
|
"""Test adding git providers with host parameter."""
|
||||||
|
# Create initial user secrets
|
||||||
|
provider_tokens = {
|
||||||
|
ProviderType.GITHUB: ProviderToken(token=SecretStr('github-token'))
|
||||||
|
}
|
||||||
|
user_secrets = UserSecrets(provider_tokens=provider_tokens)
|
||||||
|
await file_secrets_store.store(user_secrets)
|
||||||
|
|
||||||
|
# Mock check_provider_tokens to return empty string (no error)
|
||||||
|
with patch(
|
||||||
|
'openhands.server.routes.secrets.check_provider_tokens',
|
||||||
|
AsyncMock(return_value=''),
|
||||||
|
):
|
||||||
|
# Add a GitHub provider with a host
|
||||||
|
add_provider_data = {
|
||||||
|
'provider_tokens': {
|
||||||
|
'github': {'token': 'new-github-token', 'host': 'github.enterprise.com'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response = test_client.post('/api/add-git-providers', json=add_provider_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
# Verify that the settings were stored with the new provider token and host
|
||||||
|
stored_secrets = await file_secrets_store.load()
|
||||||
|
assert ProviderType.GITHUB in stored_secrets.provider_tokens
|
||||||
|
assert (
|
||||||
|
stored_secrets.provider_tokens[ProviderType.GITHUB].token.get_secret_value()
|
||||||
|
== 'new-github-token'
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
stored_secrets.provider_tokens[ProviderType.GITHUB].host
|
||||||
|
== 'github.enterprise.com'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_git_providers_update_host_only(test_client, file_secrets_store):
|
||||||
|
"""Test updating only the host for an existing provider token."""
|
||||||
|
# Create initial user secrets with a token
|
||||||
|
provider_tokens = {
|
||||||
|
ProviderType.GITHUB: ProviderToken(
|
||||||
|
token=SecretStr('github-token'), host='github.com'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
user_secrets = UserSecrets(provider_tokens=provider_tokens)
|
||||||
|
await file_secrets_store.store(user_secrets)
|
||||||
|
|
||||||
|
# Mock check_provider_tokens to return empty string (no error)
|
||||||
|
with patch(
|
||||||
|
'openhands.server.routes.secrets.check_provider_tokens',
|
||||||
|
AsyncMock(return_value=''),
|
||||||
|
):
|
||||||
|
# Update only the host
|
||||||
|
update_host_data = {
|
||||||
|
'provider_tokens': {
|
||||||
|
'github': {
|
||||||
|
'token': '', # Empty token means keep existing token
|
||||||
|
'host': 'github.enterprise.com',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response = test_client.post('/api/add-git-providers', json=update_host_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
# Verify that the host was updated but the token remains the same
|
||||||
|
stored_secrets = await file_secrets_store.load()
|
||||||
|
assert ProviderType.GITHUB in stored_secrets.provider_tokens
|
||||||
|
assert (
|
||||||
|
stored_secrets.provider_tokens[ProviderType.GITHUB].token.get_secret_value()
|
||||||
|
== 'github-token'
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
stored_secrets.provider_tokens[ProviderType.GITHUB].host
|
||||||
|
== 'github.enterprise.com'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_git_providers_invalid_token_with_host(
|
||||||
|
test_client, file_secrets_store
|
||||||
|
):
|
||||||
|
"""Test adding an invalid token with a host."""
|
||||||
|
# Create initial user secrets
|
||||||
|
user_secrets = UserSecrets()
|
||||||
|
await file_secrets_store.store(user_secrets)
|
||||||
|
|
||||||
|
# Mock validate_provider_token to return None (invalid token)
|
||||||
|
with patch(
|
||||||
|
'openhands.integrations.utils.validate_provider_token',
|
||||||
|
AsyncMock(return_value=None),
|
||||||
|
):
|
||||||
|
# Try to add an invalid GitHub provider with a host
|
||||||
|
add_provider_data = {
|
||||||
|
'provider_tokens': {
|
||||||
|
'github': {'token': 'invalid-token', 'host': 'github.enterprise.com'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response = test_client.post('/api/add-git-providers', json=add_provider_data)
|
||||||
|
assert response.status_code == 401
|
||||||
|
assert 'Invalid token' in response.json()['error']
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_multiple_git_providers_with_hosts(test_client, file_secrets_store):
|
||||||
|
"""Test adding multiple git providers with different hosts."""
|
||||||
|
# Create initial user secrets
|
||||||
|
user_secrets = UserSecrets()
|
||||||
|
await file_secrets_store.store(user_secrets)
|
||||||
|
|
||||||
|
# Mock check_provider_tokens to return empty string (no error)
|
||||||
|
with patch(
|
||||||
|
'openhands.server.routes.secrets.check_provider_tokens',
|
||||||
|
AsyncMock(return_value=''),
|
||||||
|
):
|
||||||
|
# Add multiple providers with hosts
|
||||||
|
add_providers_data = {
|
||||||
|
'provider_tokens': {
|
||||||
|
'github': {'token': 'github-token', 'host': 'github.enterprise.com'},
|
||||||
|
'gitlab': {'token': 'gitlab-token', 'host': 'gitlab.enterprise.com'},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response = test_client.post('/api/add-git-providers', json=add_providers_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
# Verify that both providers were stored with their respective hosts
|
||||||
|
stored_secrets = await file_secrets_store.load()
|
||||||
|
assert ProviderType.GITHUB in stored_secrets.provider_tokens
|
||||||
|
assert (
|
||||||
|
stored_secrets.provider_tokens[ProviderType.GITHUB].token.get_secret_value()
|
||||||
|
== 'github-token'
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
stored_secrets.provider_tokens[ProviderType.GITHUB].host
|
||||||
|
== 'github.enterprise.com'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert ProviderType.GITLAB in stored_secrets.provider_tokens
|
||||||
|
assert (
|
||||||
|
stored_secrets.provider_tokens[ProviderType.GITLAB].token.get_secret_value()
|
||||||
|
== 'gitlab-token'
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
stored_secrets.provider_tokens[ProviderType.GITLAB].host
|
||||||
|
== 'gitlab.enterprise.com'
|
||||||
|
)
|
||||||
|
|||||||
@@ -60,13 +60,16 @@ async def test_check_provider_tokens_valid():
|
|||||||
provider_token = ProviderToken(token=SecretStr('valid-token'))
|
provider_token = ProviderToken(token=SecretStr('valid-token'))
|
||||||
providers = POSTProviderModel(provider_tokens={ProviderType.GITHUB: provider_token})
|
providers = POSTProviderModel(provider_tokens={ProviderType.GITHUB: provider_token})
|
||||||
|
|
||||||
|
# Empty existing provider tokens
|
||||||
|
existing_provider_tokens = {}
|
||||||
|
|
||||||
# Mock the validate_provider_token function to return GITHUB for valid tokens
|
# Mock the validate_provider_token function to return GITHUB for valid tokens
|
||||||
with patch(
|
with patch(
|
||||||
'openhands.server.routes.secrets.validate_provider_token'
|
'openhands.server.routes.secrets.validate_provider_token'
|
||||||
) as mock_validate:
|
) as mock_validate:
|
||||||
mock_validate.return_value = ProviderType.GITHUB
|
mock_validate.return_value = ProviderType.GITHUB
|
||||||
|
|
||||||
result = await check_provider_tokens(providers)
|
result = await check_provider_tokens(providers, existing_provider_tokens)
|
||||||
|
|
||||||
# Should return empty string for valid token
|
# Should return empty string for valid token
|
||||||
assert result == ''
|
assert result == ''
|
||||||
@@ -79,13 +82,16 @@ async def test_check_provider_tokens_invalid():
|
|||||||
provider_token = ProviderToken(token=SecretStr('invalid-token'))
|
provider_token = ProviderToken(token=SecretStr('invalid-token'))
|
||||||
providers = POSTProviderModel(provider_tokens={ProviderType.GITHUB: provider_token})
|
providers = POSTProviderModel(provider_tokens={ProviderType.GITHUB: provider_token})
|
||||||
|
|
||||||
|
# Empty existing provider tokens
|
||||||
|
existing_provider_tokens = {}
|
||||||
|
|
||||||
# Mock the validate_provider_token function to return None for invalid tokens
|
# Mock the validate_provider_token function to return None for invalid tokens
|
||||||
with patch(
|
with patch(
|
||||||
'openhands.server.routes.secrets.validate_provider_token'
|
'openhands.server.routes.secrets.validate_provider_token'
|
||||||
) as mock_validate:
|
) as mock_validate:
|
||||||
mock_validate.return_value = None
|
mock_validate.return_value = None
|
||||||
|
|
||||||
result = await check_provider_tokens(providers)
|
result = await check_provider_tokens(providers, existing_provider_tokens)
|
||||||
|
|
||||||
# Should return error message for invalid token
|
# Should return error message for invalid token
|
||||||
assert 'Invalid token' in result
|
assert 'Invalid token' in result
|
||||||
@@ -98,7 +104,11 @@ async def test_check_provider_tokens_wrong_type():
|
|||||||
# We can't test with an unsupported provider type directly since the model enforces valid types
|
# We can't test with an unsupported provider type directly since the model enforces valid types
|
||||||
# Instead, we'll test with an empty provider_tokens dictionary
|
# Instead, we'll test with an empty provider_tokens dictionary
|
||||||
providers = POSTProviderModel(provider_tokens={})
|
providers = POSTProviderModel(provider_tokens={})
|
||||||
result = await check_provider_tokens(providers)
|
|
||||||
|
# Empty existing provider tokens
|
||||||
|
existing_provider_tokens = {}
|
||||||
|
|
||||||
|
result = await check_provider_tokens(providers, existing_provider_tokens)
|
||||||
|
|
||||||
# Should return empty string for no providers
|
# Should return empty string for no providers
|
||||||
assert result == ''
|
assert result == ''
|
||||||
@@ -109,7 +119,10 @@ async def test_check_provider_tokens_no_tokens():
|
|||||||
"""Test check_provider_tokens with no tokens."""
|
"""Test check_provider_tokens with no tokens."""
|
||||||
providers = POSTProviderModel(provider_tokens={})
|
providers = POSTProviderModel(provider_tokens={})
|
||||||
|
|
||||||
result = await check_provider_tokens(providers)
|
# Empty existing provider tokens
|
||||||
|
existing_provider_tokens = {}
|
||||||
|
|
||||||
|
result = await check_provider_tokens(providers, existing_provider_tokens)
|
||||||
|
|
||||||
# Should return empty string when no tokens provided
|
# Should return empty string when no tokens provided
|
||||||
assert result == ''
|
assert result == ''
|
||||||
|
|||||||
Reference in New Issue
Block a user