File size: 3,434 Bytes
afa0cbf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
//! Exercise Cloud credential isolation through the CLI using a synthetic login.

use anyhow::Result;
use predicates::str::contains;
use serde_json::json;
use tempfile::TempDir;
use wiremock::Mock;
use wiremock::MockServer;
use wiremock::ResponseTemplate;
use wiremock::matchers::header;
use wiremock::matchers::path;

#[tokio::test]
async fn cloud_list_only_allows_trusted_credential_destinations() -> Result<()> {
    let server = MockServer::start().await;
    let codex_home = TempDir::new()?;
    std::fs::write(
        codex_home.path().join("config.toml"),
        "cli_auth_credentials_store = 'file'\n",
    )?;
    std::fs::write(
        codex_home.path().join("auth.json"),
        serde_json::to_vec(&json!({
            "auth_mode": "chatgpt",
            "tokens": {
                "id_token": "eyJhbGciOiJub25lIn0.e30.c2ln",
                "access_token": "synthetic-cloud-access-token",
                "refresh_token": "synthetic-cloud-refresh-token",
                "account_id": "synthetic-cloud-account",
            },
            "last_refresh": chrono::Utc::now(),
        }))?,
    )?;

    let command = || -> Result<assert_cmd::Command> {
        let mut command = assert_cmd::Command::new(codex_utils_cargo_bin::cargo_bin("codex")?);
        command
            .current_dir(codex_home.path())
            .env("CODEX_HOME", codex_home.path())
            .env_remove("CODEX_ACCESS_TOKEN")
            .env_remove("OPENAI_API_KEY")
            .env_remove("CODEX_CLOUD_TASKS_MODE")
            .env("NO_PROXY", "127.0.0.1,localhost")
            .env("no_proxy", "127.0.0.1,localhost")
            .timeout(std::time::Duration::from_secs(15));
        Ok(command)
    };
    // Establish that the isolated profile has a usable ChatGPT login.
    command()?
        .args(["login", "status"])
        .assert()
        .success()
        .stderr(contains("Logged in using ChatGPT"));

    let output = command()?
        .env(
            "CODEX_CLOUD_TASKS_BASE_URL",
            format!("{}/backend-api", server.uri()),
        )
        .args(["cloud", "list", "--limit", "1", "--json"])
        .assert()
        .failure()
        .get_output()
        .stderr
        .clone();
    insta::assert_snapshot!(String::from_utf8(output)?, @"Error: CODEX_CLOUD_TASKS_BASE_URL must use a trusted HTTPS origin on port 443, without user information, a query, or a fragment; custom backends cannot use saved ChatGPT credentials");
    assert!(server.received_requests().await.unwrap().is_empty());

    // Staging must reach explicit PAT authentication. Reject the synthetic PAT locally so
    // this test never sends a request to the real staging backend.
    let auth_server = MockServer::start().await;
    Mock::given(path("/v1/user-auth-credential/whoami"))
        .and(header("authorization", "Bearer at-synthetic-cloud"))
        .respond_with(ResponseTemplate::new(401))
        .expect(1)
        .mount(&auth_server)
        .await;
    command()?
        .env(
            "CODEX_CLOUD_TASKS_BASE_URL",
            "https://chatgpt-staging.com/backend-api",
        )
        .env("CODEX_ACCESS_TOKEN", "at-synthetic-cloud")
        .env("CODEX_AUTHAPI_BASE_URL", auth_server.uri())
        .args(["cloud", "list", "--limit", "1", "--json"])
        .assert()
        .failure()
        .stderr(contains("Not signed in. Please run 'codex login'"));
    auth_server.verify().await;
    Ok(())
}