fix(inference): cargo fmt + coverage for openai_oauth (#1953)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ghost Scripter
2026-05-20 14:14:39 +05:30
co-authored by Cursor
parent e5398e7e0f
commit 9aa390d6ec
7 changed files with 622 additions and 19 deletions
@@ -1,7 +1,11 @@
import { fireEvent, screen, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { setCloudProviderKey } from '../../../../services/api/aiSettingsApi';
import { callCoreRpc } from '../../../../services/coreRpcClient';
import { renderWithProviders } from '../../../../test/test-utils';
import { openUrl } from '../../../../utils/openUrl';
import { isTauri } from '../../../../utils/tauriCommands/common';
import ApiKeysStep from '../ApiKeysStep';
vi.mock('../../../../services/coreRpcClient', () => ({ callCoreRpc: vi.fn() }));
@@ -17,10 +21,12 @@ vi.mock('../../../../services/api/aiSettingsApi', () => ({
describe('ApiKeysStep OpenAI OAuth', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(isTauri).mockReturnValue(true);
vi.mocked(openUrl).mockResolvedValue(undefined);
vi.mocked(setCloudProviderKey).mockResolvedValue(undefined);
});
it('shows connected badge when oauth status reports connected', async () => {
const { callCoreRpc } = await import('../../../../services/coreRpcClient');
vi.mocked(callCoreRpc).mockResolvedValueOnce({ result: { connected: true } });
renderWithProviders(<ApiKeysStep onNext={vi.fn()} onSkip={vi.fn()} />);
@@ -30,7 +36,6 @@ describe('ApiKeysStep OpenAI OAuth', () => {
});
it('starts oauth and accepts pasted callback URL', async () => {
const { callCoreRpc } = await import('../../../../services/coreRpcClient');
vi.mocked(callCoreRpc)
.mockResolvedValueOnce({ result: { connected: false } })
.mockResolvedValueOnce({
@@ -42,8 +47,6 @@ describe('ApiKeysStep OpenAI OAuth', () => {
})
.mockResolvedValueOnce({ result: { connected: true } });
const { openUrl } = await import('../../../../utils/openUrl');
renderWithProviders(<ApiKeysStep onNext={vi.fn()} onSkip={vi.fn()} />);
fireEvent.click(await screen.findByTestId('onboarding-openai-oauth-connect'));
@@ -71,4 +74,94 @@ describe('ApiKeysStep OpenAI OAuth', () => {
expect(await screen.findByTestId('onboarding-openai-oauth-connected')).toBeInTheDocument();
});
it('shows a desktop-only error without calling core outside Tauri', async () => {
vi.mocked(isTauri).mockReturnValue(false);
renderWithProviders(<ApiKeysStep onNext={vi.fn()} onSkip={vi.fn()} />);
fireEvent.click(screen.getByTestId('onboarding-openai-oauth-connect'));
expect(
await screen.findByText('ChatGPT sign-in is only available in the desktop app.')
).toBeInTheDocument();
expect(callCoreRpc).not.toHaveBeenCalled();
expect(openUrl).not.toHaveBeenCalled();
});
it('reports an oauth start failure when core omits authUrl', async () => {
vi.mocked(callCoreRpc)
.mockResolvedValueOnce({ result: { connected: false } })
.mockResolvedValueOnce({ result: { authUrl: ' ' } });
renderWithProviders(<ApiKeysStep onNext={vi.fn()} onSkip={vi.fn()} />);
fireEvent.click(await screen.findByTestId('onboarding-openai-oauth-connect'));
expect(
await screen.findByText('Could not start ChatGPT sign-in. Try again or use an API key.')
).toBeInTheDocument();
expect(openUrl).not.toHaveBeenCalled();
});
it('requires a pasted callback before completing oauth', async () => {
vi.mocked(callCoreRpc)
.mockResolvedValueOnce({ result: { connected: false } })
.mockResolvedValueOnce({
result: { authUrl: 'https://auth.openai.com/oauth/authorize?client_id=test' },
});
renderWithProviders(<ApiKeysStep onNext={vi.fn()} onSkip={vi.fn()} />);
fireEvent.click(await screen.findByTestId('onboarding-openai-oauth-connect'));
await screen.findByTestId('onboarding-openai-oauth-callback-input');
fireEvent.click(screen.getByTestId('onboarding-openai-oauth-complete'));
expect(
await screen.findByText('Paste the redirect URL from your browser after signing in.')
).toBeInTheDocument();
expect(callCoreRpc).not.toHaveBeenCalledWith(
expect.objectContaining({ method: 'openhuman.inference_openai_oauth_complete' })
);
});
it('reports an oauth completion failure and keeps the callback form visible', async () => {
vi.mocked(callCoreRpc)
.mockResolvedValueOnce({ result: { connected: false } })
.mockResolvedValueOnce({
result: { authUrl: 'https://auth.openai.com/oauth/authorize?client_id=test' },
})
.mockRejectedValueOnce(new Error('state mismatch'));
renderWithProviders(<ApiKeysStep onNext={vi.fn()} onSkip={vi.fn()} />);
fireEvent.click(await screen.findByTestId('onboarding-openai-oauth-connect'));
const input = await screen.findByTestId('onboarding-openai-oauth-callback-input');
fireEvent.change(input, {
target: { value: 'http://127.0.0.1:1455/auth/callback?code=abc&state=wrong' },
});
fireEvent.click(screen.getByTestId('onboarding-openai-oauth-complete'));
expect(
await screen.findByText(
'ChatGPT sign-in did not complete. Check the redirect URL and try again.'
)
).toBeInTheDocument();
expect(screen.getByTestId('onboarding-openai-oauth-callback-input')).toBeInTheDocument();
});
it('continues without saving API keys when oauth is already connected', async () => {
const onNext = vi.fn();
vi.mocked(callCoreRpc).mockResolvedValueOnce({ result: { connected: true } });
renderWithProviders(<ApiKeysStep onNext={onNext} onSkip={vi.fn()} />);
await screen.findByTestId('onboarding-openai-oauth-connected');
fireEvent.click(screen.getByTestId('onboarding-next-button'));
await waitFor(() => {
expect(onNext).toHaveBeenCalledTimes(1);
});
expect(setCloudProviderKey).not.toHaveBeenCalled();
});
});
+2 -2
View File
@@ -233,7 +233,7 @@ pub fn disconnect_openai_oauth(config: &Config) -> Result<serde_json::Value, Str
Ok(serde_json::json!({ "disconnected": removed }))
}
fn build_authorize_url(
pub(super) fn build_authorize_url(
config: &motosan_ai_oauth::OAuthConfig,
challenge: &str,
state: &str,
@@ -256,7 +256,7 @@ fn build_authorize_url(
url.to_string()
}
async fn exchange_authorization_code(
pub(super) async fn exchange_authorization_code(
config: &motosan_ai_oauth::OAuthConfig,
code: &str,
state: &str,
@@ -1,20 +1,55 @@
use super::flow::parse_callback_input;
use super::flow::{build_authorize_url, exchange_authorization_code, parse_callback_input};
use super::store::persist_openai_oauth_token;
use super::{
complete_openai_oauth, disconnect_openai_oauth, openai_oauth_status, start_openai_oauth,
};
use crate::openhuman::config::Config;
use crate::openhuman::credentials::profiles::{AuthProfile, AuthProfilesStore, TokenSet};
use crate::openhuman::credentials::profiles::{
AuthProfile, AuthProfileKind, AuthProfilesStore, TokenSet,
};
use crate::openhuman::inference::openai_oauth::lookup_openai_bearer_token;
use crate::openhuman::inference::openai_oauth::store::{
OPENAI_OAUTH_PROFILE_NAME, OPENAI_PROVIDER_KEY,
};
use chrono::{Duration, Utc};
use motosan_ai_oauth::{OAuthConfig, StateStrategy, TokenBodyFormat};
use tempfile::tempdir;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
fn test_config(tmp: &tempfile::TempDir) -> Config {
let mut config = Config::default();
config.config_path = tmp.path().join("config.toml");
config
Config {
config_path: tmp.path().join("config.toml"),
..Config::default()
}
}
fn runtime() -> tokio::runtime::Runtime {
tokio::runtime::Runtime::new().unwrap()
}
fn unsigned_jwt(payload: serde_json::Value) -> String {
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
let header = URL_SAFE_NO_PAD.encode(r#"{"alg":"none"}"#);
let payload = URL_SAFE_NO_PAD.encode(payload.to_string());
format!("{header}.{payload}.")
}
fn test_oauth_config(token_url: &'static str) -> OAuthConfig {
OAuthConfig {
client_id: "client-id",
client_secret: Some("client-secret"),
auth_url: "https://auth.example.test/oauth/authorize",
token_url,
scopes: &["scope-a", "scope-b"],
redirect_port: Some(1455),
callback_path: "/auth/callback",
redirect_uri_host: "127.0.0.1",
token_body: TokenBodyFormat::Form,
extra_auth_params: &[("prompt", "consent")],
state_strategy: StateStrategy::Random,
}
}
#[test]
@@ -30,6 +65,41 @@ fn start_openai_oauth_returns_authorize_url() {
assert!(!openai_oauth_status(&config).unwrap().connected);
}
#[test]
fn build_authorize_url_includes_codex_pkce_and_extra_params() {
let url = build_authorize_url(
&test_oauth_config("https://token.example.test/oauth/token"),
"challenge-123",
"state-123",
"http://127.0.0.1:1455/auth/callback",
);
let parsed = reqwest::Url::parse(&url).unwrap();
let pairs = parsed
.query_pairs()
.into_owned()
.collect::<std::collections::BTreeMap<_, _>>();
assert_eq!(
pairs.get("client_id").map(String::as_str),
Some("client-id")
);
assert_eq!(pairs.get("response_type").map(String::as_str), Some("code"));
assert_eq!(
pairs.get("scope").map(String::as_str),
Some("scope-a scope-b")
);
assert_eq!(pairs.get("state").map(String::as_str), Some("state-123"));
assert_eq!(
pairs.get("code_challenge").map(String::as_str),
Some("challenge-123")
);
assert_eq!(
pairs.get("code_challenge_method").map(String::as_str),
Some("S256")
);
assert_eq!(pairs.get("prompt").map(String::as_str), Some("consent"));
}
#[test]
fn parse_callback_input_accepts_full_redirect_url() {
let url = "http://127.0.0.1:1455/auth/callback?code=abc&state=xyz";
@@ -38,12 +108,71 @@ fn parse_callback_input_accepts_full_redirect_url() {
assert_eq!(state, "xyz");
}
#[test]
fn parse_callback_input_accepts_raw_query_string() {
let (code, state) = parse_callback_input("code=abc%20123&state=xyz").unwrap();
assert_eq!(code, "abc 123");
assert_eq!(state, "xyz");
}
#[test]
fn parse_callback_input_rejects_missing_code() {
let err = parse_callback_input("http://127.0.0.1:1455/auth/callback?state=xyz").unwrap_err();
assert!(err.contains("code"));
}
#[test]
fn parse_callback_input_rejects_blank_invalid_and_missing_state() {
let blank = parse_callback_input(" ").unwrap_err();
assert!(blank.contains("required"));
let invalid = parse_callback_input("not-a-callback").unwrap_err();
assert!(invalid.contains("invalid"));
let missing_state =
parse_callback_input("http://127.0.0.1:1455/auth/callback?code=abc").unwrap_err();
assert!(missing_state.contains("state"));
}
#[test]
fn complete_openai_oauth_rejects_missing_pending_session() {
let tmp = tempdir().unwrap();
let config = test_config(&tmp);
let err = runtime()
.block_on(complete_openai_oauth(
&config,
"http://127.0.0.1:1455/auth/callback?code=fake&state=state",
))
.unwrap_err();
assert!(err.contains("no pending OAuth session"));
}
#[test]
fn complete_openai_oauth_rejects_expired_pending_session() {
let tmp = tempdir().unwrap();
let config = test_config(&tmp);
std::fs::write(
tmp.path().join("openai-oauth-pending.json"),
serde_json::json!({
"state": "state",
"verifier": "verifier",
"redirect_uri": "http://127.0.0.1:1455/auth/callback",
"created_at": 1_u64,
})
.to_string(),
)
.unwrap();
let err = runtime()
.block_on(complete_openai_oauth(
&config,
"http://127.0.0.1:1455/auth/callback?code=fake&state=state",
))
.unwrap_err();
assert!(err.contains("no pending OAuth session"));
assert!(!tmp.path().join("openai-oauth-pending.json").exists());
}
#[test]
fn complete_openai_oauth_rejects_state_mismatch() {
let tmp = tempdir().unwrap();
@@ -53,13 +182,134 @@ fn complete_openai_oauth_rejects_state_mismatch() {
"http://127.0.0.1:1455/auth/callback?code=fake&state=not-{}",
start.state
);
let rt = tokio::runtime::Runtime::new().unwrap();
let err = rt
let err = runtime()
.block_on(complete_openai_oauth(&config, &callback))
.unwrap_err();
assert!(err.contains("state mismatch"));
}
#[tokio::test]
async fn exchange_authorization_code_parses_successful_token_response() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/token"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"access_token": "access-token",
"refresh_token": "refresh-token",
"id_token": "id-token",
"expires_in": 3600,
})))
.mount(&server)
.await;
let token_url: &'static str = Box::leak(format!("{}/token", server.uri()).into_boxed_str());
let token = exchange_authorization_code(
&test_oauth_config(token_url),
"code-123",
"state-123",
"verifier-123",
"http://127.0.0.1:1455/auth/callback",
)
.await
.unwrap();
assert_eq!(token.access_token, "access-token");
assert_eq!(token.refresh_token, "refresh-token");
assert_eq!(token.id_token.as_deref(), Some("id-token"));
assert_eq!(token.expires_in, 3600);
assert!(token.issued_at > 0);
}
#[tokio::test]
async fn exchange_authorization_code_reports_http_errors() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/token"))
.respond_with(ResponseTemplate::new(400).set_body_string("bad auth code"))
.mount(&server)
.await;
let token_url: &'static str = Box::leak(format!("{}/token", server.uri()).into_boxed_str());
let err = exchange_authorization_code(
&test_oauth_config(token_url),
"code-123",
"state-123",
"verifier-123",
"http://127.0.0.1:1455/auth/callback",
)
.await
.unwrap_err();
assert!(err.contains("HTTP 400"));
assert!(err.contains("bad auth code"));
}
#[test]
fn persist_openai_oauth_token_stores_oauth_profile_with_metadata() {
let tmp = tempdir().unwrap();
let config = test_config(&tmp);
let access_token = unsigned_jwt(serde_json::json!({ "sub": "acct_123" }));
let token = motosan_ai_oauth::Token {
access_token: access_token.clone(),
refresh_token: "refresh-token".into(),
id_token: Some("id-token".into()),
expires_in: 3600,
issued_at: 123,
};
let profile = persist_openai_oauth_token(&config, &token).unwrap();
assert_eq!(profile.kind, AuthProfileKind::OAuth);
assert_eq!(
profile.metadata.get("account_id").map(String::as_str),
Some("acct_123")
);
assert_eq!(
profile
.token_set
.as_ref()
.map(|set| set.access_token.as_str()),
Some(access_token.as_str())
);
assert_eq!(
profile
.token_set
.as_ref()
.and_then(|set| set.refresh_token.as_deref()),
Some("refresh-token")
);
assert!(profile
.token_set
.as_ref()
.and_then(|set| set.expires_at)
.is_some());
let data = AuthProfilesStore::new(tmp.path(), false).load().unwrap();
let stored = data.profiles.get(&profile.id).unwrap();
assert_eq!(stored.id, profile.id);
}
#[test]
fn openai_oauth_status_reports_token_profile_as_disconnected() {
let tmp = tempdir().unwrap();
let config = test_config(&tmp);
let store = AuthProfilesStore::new(tmp.path(), false);
store
.upsert_profile(
AuthProfile::new_token(
OPENAI_PROVIDER_KEY,
OPENAI_OAUTH_PROFILE_NAME,
"sk-token-profile".to_string(),
),
true,
)
.unwrap();
let status = openai_oauth_status(&config).unwrap();
assert!(!status.connected);
assert_eq!(status.auth_method.as_deref(), Some("token"));
assert!(status.profile_id.is_some());
}
#[test]
fn lookup_openai_bearer_token_prefers_api_key_over_oauth() {
let tmp = tempdir().unwrap();
@@ -88,6 +338,105 @@ fn lookup_openai_bearer_token_prefers_api_key_over_oauth() {
assert_eq!(token.as_deref(), Some("sk-api-key"));
}
#[test]
fn lookup_openai_bearer_token_uses_oauth_when_api_key_missing() {
let tmp = tempdir().unwrap();
let config = test_config(&tmp);
let store = AuthProfilesStore::new(tmp.path(), false);
let oauth_profile = AuthProfile::new_oauth(
OPENAI_PROVIDER_KEY,
OPENAI_OAUTH_PROFILE_NAME,
TokenSet {
access_token: "oauth-access".into(),
refresh_token: Some("refresh".into()),
id_token: None,
expires_at: Some(Utc::now() + Duration::hours(1)),
token_type: Some("Bearer".into()),
scope: None,
},
);
store.upsert_profile(oauth_profile, true).unwrap();
let token = lookup_openai_bearer_token(&config).unwrap();
assert_eq!(token.as_deref(), Some("oauth-access"));
}
#[test]
fn lookup_openai_bearer_token_uses_legacy_api_key_when_new_style_is_empty() {
let tmp = tempdir().unwrap();
let config = test_config(&tmp);
let store = AuthProfilesStore::new(tmp.path(), false);
let oauth_profile = AuthProfile::new_oauth(
OPENAI_PROVIDER_KEY,
OPENAI_OAUTH_PROFILE_NAME,
TokenSet {
access_token: " ".into(),
refresh_token: None,
id_token: None,
expires_at: Some(Utc::now() + Duration::hours(1)),
token_type: Some("Bearer".into()),
scope: None,
},
);
store.upsert_profile(oauth_profile, true).unwrap();
store
.upsert_profile(
AuthProfile::new_token("openai", "default", "sk-legacy-key".to_string()),
true,
)
.unwrap();
let token = lookup_openai_bearer_token(&config).unwrap();
assert_eq!(token.as_deref(), Some("sk-legacy-key"));
}
#[test]
fn lookup_openai_bearer_token_keeps_expired_token_when_refresh_fails_without_runtime() {
let tmp = tempdir().unwrap();
let config = test_config(&tmp);
let store = AuthProfilesStore::new(tmp.path(), false);
let oauth_profile = AuthProfile::new_oauth(
OPENAI_PROVIDER_KEY,
OPENAI_OAUTH_PROFILE_NAME,
TokenSet {
access_token: "expired-access".into(),
refresh_token: Some("refresh".into()),
id_token: None,
expires_at: Some(Utc::now() - Duration::minutes(5)),
token_type: Some("Bearer".into()),
scope: None,
},
);
store.upsert_profile(oauth_profile, true).unwrap();
let token = lookup_openai_bearer_token(&config).unwrap();
assert_eq!(token.as_deref(), Some("expired-access"));
}
#[test]
fn lookup_openai_bearer_token_returns_none_without_profiles_or_access_token() {
let tmp = tempdir().unwrap();
let config = test_config(&tmp);
assert_eq!(lookup_openai_bearer_token(&config).unwrap(), None);
let store = AuthProfilesStore::new(tmp.path(), false);
let empty_oauth_profile = AuthProfile::new_oauth(
OPENAI_PROVIDER_KEY,
OPENAI_OAUTH_PROFILE_NAME,
TokenSet {
access_token: " ".into(),
refresh_token: None,
id_token: None,
expires_at: Some(Utc::now() - Duration::hours(1)),
token_type: Some("Bearer".into()),
scope: None,
},
);
store.upsert_profile(empty_oauth_profile, true).unwrap();
assert_eq!(lookup_openai_bearer_token(&config).unwrap(), None);
}
#[test]
fn disconnect_openai_oauth_clears_profile() {
let tmp = tempdir().unwrap();
+104 -3
View File
@@ -1,11 +1,16 @@
use super::*;
use crate::openhuman::credentials::profiles::{AuthProfile, AuthProfilesStore, TokenSet};
use crate::openhuman::inference::openai_oauth::{OPENAI_OAUTH_PROFILE_NAME, OPENAI_PROVIDER_KEY};
use chrono::{Duration, Utc};
use tempfile::tempdir;
fn disabled_config() -> (Config, tempfile::TempDir) {
let tmp = tempdir().expect("tempdir");
let mut config = Config::default();
config.workspace_dir = tmp.path().join("workspace");
config.config_path = tmp.path().join("config.toml");
let mut config = Config {
workspace_dir: tmp.path().join("workspace"),
config_path: tmp.path().join("config.toml"),
..Config::default()
};
config.local_ai.runtime_enabled = false;
config.local_ai.opt_in_confirmed = false;
(config, tmp)
@@ -109,3 +114,99 @@ async fn inference_presets_returns_recommended_tier() {
assert!(outcome.value.get("recommended_tier").is_some());
assert!(outcome.value.get("presets").is_some());
}
#[tokio::test]
async fn inference_openai_oauth_start_returns_authorize_payload() {
let (config, _tmp) = disabled_config();
let outcome = inference_openai_oauth_start(&config)
.await
.expect("oauth start");
assert!(outcome.value["authUrl"]
.as_str()
.unwrap()
.contains("auth.openai.com"));
assert_eq!(
outcome.value["redirectUri"].as_str(),
Some("http://127.0.0.1:1455/auth/callback")
);
assert_eq!(outcome.logs, vec!["openai oauth authorize url ready"]);
}
#[tokio::test]
async fn inference_openai_oauth_complete_surfaces_state_errors() {
let (config, _tmp) = disabled_config();
let start = inference_openai_oauth_start(&config)
.await
.expect("oauth start");
let state = start.value["state"].as_str().unwrap();
let callback = format!("http://127.0.0.1:1455/auth/callback?code=fake&state=wrong-{state}");
let err = inference_openai_oauth_complete(&config, &callback)
.await
.expect_err("state mismatch should fail");
assert!(err.contains("state mismatch"));
}
#[tokio::test]
async fn inference_openai_oauth_status_returns_connected_payload() {
let (config, tmp) = disabled_config();
let store = AuthProfilesStore::new(tmp.path(), false);
store
.upsert_profile(
AuthProfile::new_oauth(
OPENAI_PROVIDER_KEY,
OPENAI_OAUTH_PROFILE_NAME,
TokenSet {
access_token: "oauth-access".into(),
refresh_token: None,
id_token: None,
expires_at: Some(Utc::now() + Duration::hours(1)),
token_type: Some("Bearer".into()),
scope: None,
},
),
true,
)
.unwrap();
let outcome = inference_openai_oauth_status(&config)
.await
.expect("oauth status");
assert_eq!(outcome.value["connected"], true);
assert_eq!(outcome.value["authMethod"], "oauth");
assert_eq!(outcome.logs, vec!["openai oauth status"]);
}
#[tokio::test]
async fn inference_openai_oauth_disconnect_returns_removed_flag() {
let (config, tmp) = disabled_config();
let store = AuthProfilesStore::new(tmp.path(), false);
store
.upsert_profile(
AuthProfile::new_oauth(
OPENAI_PROVIDER_KEY,
OPENAI_OAUTH_PROFILE_NAME,
TokenSet {
access_token: "oauth-access".into(),
refresh_token: None,
id_token: None,
expires_at: None,
token_type: Some("Bearer".into()),
scope: None,
},
),
true,
)
.unwrap();
let outcome = inference_openai_oauth_disconnect(&config)
.await
.expect("oauth disconnect");
assert_eq!(outcome.value["disconnected"], true);
assert_eq!(outcome.logs, vec!["openai oauth disconnected"]);
}
@@ -454,3 +454,22 @@ fn verify_session_active_called_for_custom_provider_not_for_openhuman() {
"verify_session_active must reject config without session",
);
}
#[test]
fn lookup_key_for_slug_routes_openai_oauth_lookup_path() {
let tmp = TempDir::new().expect("tempdir");
let config = config_in_tempdir(&tmp);
let auth = AuthService::new(tmp.path(), config.secrets.encrypt);
auth.store_provider_token(
"provider:openai",
"default",
"sk-openai",
Default::default(),
true,
)
.expect("store openai token");
let token = lookup_key_for_slug("openai", &config).expect("lookup openai token");
assert_eq!(token, "sk-openai");
}
+1 -3
View File
@@ -701,9 +701,7 @@ fn handle_inference_openai_oauth_status(_params: Map<String, Value>) -> Controll
fn handle_inference_openai_oauth_disconnect(_params: Map<String, Value>) -> ControllerFuture {
Box::pin(async move {
let config = config_rpc::load_config_with_timeout().await?;
to_json(
crate::openhuman::inference::rpc::inference_openai_oauth_disconnect(&config).await?,
)
to_json(crate::openhuman::inference::rpc::inference_openai_oauth_disconnect(&config).await?)
})
}
+43
View File
@@ -36,6 +36,10 @@ fn inference_schema_function_names_are_stable() {
assert!(functions.contains(&"presets"));
assert!(functions.contains(&"apply_preset"));
assert!(functions.contains(&"diagnostics"));
assert!(functions.contains(&"openai_oauth_start"));
assert!(functions.contains(&"openai_oauth_complete"));
assert!(functions.contains(&"openai_oauth_status"));
assert!(functions.contains(&"openai_oauth_disconnect"));
assert!(functions.contains(&"prompt"));
assert!(functions.contains(&"vision_prompt"));
assert!(functions.contains(&"embed"));
@@ -64,6 +68,45 @@ fn inference_chat_schema_requires_messages() {
.any(|field| field.name == "messages" && field.required));
}
#[test]
fn inference_openai_oauth_schemas_are_registered_with_expected_shapes() {
let registered: Vec<&str> = all_registered_controllers()
.into_iter()
.map(|controller| controller.schema.function)
.collect();
for function in [
"openai_oauth_start",
"openai_oauth_complete",
"openai_oauth_status",
"openai_oauth_disconnect",
] {
assert!(registered.contains(&function), "missing {function}");
let schema = schemas(function);
assert_eq!(schema.namespace, "inference");
assert_eq!(schema.function, function);
assert!(!schema.description.is_empty());
assert!(!schema.outputs.is_empty());
}
let complete = schemas("openai_oauth_complete");
assert_eq!(complete.inputs.len(), 1);
assert_eq!(complete.inputs[0].name, "callback_url");
assert!(complete.inputs[0].required);
assert!(schemas("openai_oauth_start").inputs.is_empty());
assert!(schemas("openai_oauth_status").inputs.is_empty());
assert!(schemas("openai_oauth_disconnect").inputs.is_empty());
}
#[tokio::test]
async fn inference_openai_oauth_complete_handler_rejects_invalid_params() {
let params = Map::from_iter([("callback_url".to_string(), Value::Bool(true))]);
let err = handle_inference_openai_oauth_complete(params)
.await
.expect_err("invalid params");
assert!(err.contains("invalid params"));
}
#[test]
fn inference_unknown_schema_panics() {
let panic = std::panic::catch_unwind(|| schemas("no_such_function"));