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
//! The real-server smoke test: the deploy path — session open, full
//! import, one poll — against Fastmail itself. Read-only, so a
//! read-scope token suffices. Ignored by default; `just smoke` runs it,
//! reading the token from 1Password via the `op` CLI.

use camino::Utf8PathBuf;
use docket::jmap::{Client, Credential};
use docket::store::{Clock, Store};

/// The smoke test's token, by item ID: the item's name contains a
/// colon, which `op://` references don't allow (alphanumerics, `-`,
/// `_`, `.`, and spaces only), so the ID stands in for the name.
const TOKEN_REF: &str = "op://Private/dpepkrnjuhnao5h7fd52kvqay4/credential";

/// Reads the token via `op`, which must be installed and signed in.
fn token() -> Result<String, Box<dyn std::error::Error>> {
    let output = match std::process::Command::new("op")
        .arg("read")
        .arg(TOKEN_REF)
        .output()
    {
        Ok(output) => output,
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
            return Err("the 1Password CLI (`op`) is not installed".into());
        }
        Err(e) => return Err(e.into()),
    };
    if !output.status.success() {
        return Err(format!(
            "op read failed: {}",
            String::from_utf8_lossy(&output.stderr).trim()
        )
        .into());
    }
    Ok(String::from_utf8(output.stdout)?.trim().to_owned())
}

#[test]
#[ignore = "run via `just smoke`; reads its token from 1Password"]
fn the_session_open_chain_runs_against_fastmail() -> Result<(), Box<dyn std::error::Error>> {
    let token = token()?;
    // The server sees only the bearer header, so a bad token surfaces
    // as an opaque 401; what 1Password handed over is the other half.
    let pieces = token.split_whitespace().count();
    if pieces != 1 {
        return Err(format!(
            "the 1Password field must hold exactly the token; got {pieces} \
             whitespace-separated pieces"
        )
        .into());
    }
    // Credentials are files (systemd LoadCredential in production), so
    // hand the token to the Client through one.
    let file = tempfile::NamedTempFile::new()?;
    std::fs::write(&file, token)?;
    let credential = Credential {
        name: "smoke".into(),
        token_file: Utf8PathBuf::from_path_buf(file.path().to_owned())
            .map_err(|_| "the temp token file must be valid UTF-8")?,
    };
    let store = Store::open_in_memory(Clock::System)?;
    tokio::runtime::Runtime::new()?.block_on(async {
        let client = Client::fastmail()?;
        let mut sync = client.sync_account(&credential, &store).await?;
        client.poll_once(&credential, &mut sync, &store).await?;
        Ok(())
    })
}