OXYGENHUB
# e8b4e156f2fa oxygen / crates / oxy-server / tests / http.rs
6.3 KiB · Rust · last changed TLS: oxy serve --tls-cert/--tls-key via rustls; remote.<name>.ca and OXY_CA for self-hosted roots 1 month ago source blame history raw
750251b2 Andy McCall ↶ 1
//! End-to-end: a real server on a loopback port, clients over HTTP.
2
3
use oxy_core::repo::{Repo, SignatureStatus};
4
use std::fs;
5
use std::net::TcpListener;
6
use std::path::Path;
7
8
fn write(root: &Path, rel: &str, content: &[u8]) {
9
    let p = root.join(rel);
10
    fs::create_dir_all(p.parent().unwrap()).unwrap();
11
    fs::write(p, content).unwrap();
12
}
13
98286406 Andy McCall ↶ 14
/// Point `$OXY_HOME` at a throwaway directory once per test process so no
15
/// test reads or writes the developer's real user configuration or keys.
16
fn hermetic() {
17
    static ONCE: std::sync::Once = std::sync::Once::new();
18
    static DIR: std::sync::OnceLock<tempfile::TempDir> = std::sync::OnceLock::new();
19
    ONCE.call_once(|| {
20
        let d = tempfile::tempdir().unwrap();
21
        std::env::set_var("OXY_HOME", d.path());
22
        DIR.set(d).ok();
23
    });
24
}
25
750251b2 Andy McCall ↶ 26
fn start(root: &Path, tokens: Vec<String>) -> String {
2b5ee371 Andy McCall ↶ 27
    start_with(root, tokens, None)
28
}
29
30
fn start_with(root: &Path, tokens: Vec<String>, tls: Option<oxy_server::TlsFiles>) -> String {
750251b2 Andy McCall ↶ 31
    let listener = TcpListener::bind("127.0.0.1:0").unwrap();
32
    let port = listener.local_addr().unwrap().port();
2b5ee371 Andy McCall ↶ 33
    let scheme = if tls.is_some() { "https" } else { "http" };
34
    let cfg = oxy_server::ServerConfig { root: root.to_path_buf(), tokens, tls };
750251b2 Andy McCall ↶ 35
    std::thread::spawn(move || oxy_server::run_blocking(listener, cfg).unwrap());
2b5ee371 Andy McCall ↶ 36
    // Give the acceptor a moment; the first request otherwise races the bind.
37
    std::thread::sleep(std::time::Duration::from_millis(50));
38
    format!("{scheme}://127.0.0.1:{port}")
750251b2 Andy McCall ↶ 39
}
40
2b5ee371 Andy McCall ↶ 41
/// A self-signed certificate for 127.0.0.1 written to `dir`: (cert, key).
42
fn self_signed(dir: &Path) -> (std::path::PathBuf, std::path::PathBuf) {
43
    fs::create_dir_all(dir).unwrap();
44
    let ck = rcgen::generate_simple_self_signed(vec!["127.0.0.1".into(), "localhost".into()]).unwrap();
45
    let cert = dir.join("cert.pem");
46
    let key = dir.join("key.pem");
47
    fs::write(&cert, ck.cert.pem()).unwrap();
48
    fs::write(&key, ck.signing_key.serialize_pem()).unwrap();
49
    (cert, key)
50
}
51
750251b2 Andy McCall ↶ 52
#[test]
53
fn clone_push_pull_over_http() {
98286406 Andy McCall ↶ 54
    hermetic();
750251b2 Andy McCall ↶ 55
    let dir = tempfile::tempdir().unwrap();
56
    let root = dir.path();
57
    let served = root.join("served");
58
    Repo::init(&served.join("origin")).unwrap();
59
    let base = start(&served, vec![]);
60
    let url = format!("{base}/origin");
61
fce44ae9 Andy McCall ↶ 62
    let mut a = Repo::clone(&url, &root.join("a")).unwrap().with_home(root.join("home"));
63
    let (key, _) = a.create_identity("Ada", "[email protected]", oxy_core::repo::IdentityScope::Repo).unwrap();
750251b2 Andy McCall ↶ 64
    write(&root.join("a"), "f.txt", b"hello\n");
65
    let h1 = a.commit("hello", &[]).unwrap();
66
    let r = a.push("origin", false).unwrap();
67
    assert!(r[0].ok, "{}", r[0].reason);
68
69
    let origin = Repo::open(&served.join("origin")).unwrap();
70
    assert_eq!(origin.bookmark_heads().unwrap(), vec![h1]);
71
    assert!(matches!(origin.authenticate_change(&h1).unwrap(), SignatureStatus::Valid { key: k, sealed: true, .. } if k == key));
72
73
    let b = Repo::clone(&url, &root.join("b")).unwrap();
74
    assert_eq!(fs::read(root.join("b/f.txt")).unwrap(), b"hello\n");
75
    write(&root.join("b"), "g.txt", b"g\n");
76
    let h2 = b.commit("g", &[]).unwrap();
77
    assert!(b.push("origin", false).unwrap()[0].ok);
78
79
    let pr = a.pull("origin").unwrap();
80
    assert!(pr.changed && pr.checked_out);
81
    assert_eq!(fs::read(root.join("a/g.txt")).unwrap(), b"g\n");
82
    let mut heads = a.bookmark_heads().unwrap();
83
    heads.sort();
84
    let mut want = vec![h1, h2];
85
    want.sort();
86
    assert_eq!(heads, want);
87
88
    // Unknown repositories and names the server refuses are surfaced as errors.
89
    let err = Repo::clone(&format!("{base}/nope"), &root.join("c")).unwrap_err().to_string();
90
    assert!(err.contains("404"), "{err}");
91
    fs::create_dir_all(served.join(".hidden/.oxy")).unwrap();
92
    assert!(Repo::clone(&format!("{base}/.hidden"), &root.join("d")).unwrap_err().to_string().contains("404"));
93
}
94
95
#[test]
96
fn push_requires_a_token_when_configured() {
98286406 Andy McCall ↶ 97
    hermetic();
750251b2 Andy McCall ↶ 98
    let dir = tempfile::tempdir().unwrap();
99
    let root = dir.path();
100
    let served = root.join("served");
101
    Repo::init(&served.join("origin")).unwrap();
102
    let base = start(&served, vec!["s3cret".into()]);
103
    let url = format!("{base}/origin");
104
105
    // Reads are open.
106
    let mut a = Repo::clone(&url, &root.join("a")).unwrap();
107
    write(&root.join("a"), "f.txt", b"x\n");
108
    a.commit("x", &[]).unwrap();
109
    let err = a.push("origin", false).unwrap_err().to_string();
110
    assert!(err.contains("401"), "{err}");
111
112
    a.config.set("remote.origin.token", "wrong").unwrap();
113
    a.save_config().unwrap();
114
    assert!(a.push("origin", false).unwrap_err().to_string().contains("401"));
115
116
    a.config.set("remote.origin.token", "s3cret").unwrap();
117
    a.save_config().unwrap();
118
    assert!(a.push("origin", false).unwrap()[0].ok);
119
    let origin = Repo::open(&served.join("origin")).unwrap();
120
    assert_eq!(origin.bookmark_heads().unwrap().len(), 1);
121
}
2b5ee371 Andy McCall ↶ 122
123
#[test]
124
fn https_with_a_self_signed_certificate() {
125
    hermetic();
126
    let dir = tempfile::tempdir().unwrap();
127
    let root = dir.path();
128
    let served = root.join("served");
129
    Repo::init(&served.join("origin")).unwrap();
130
    let (cert, key) = self_signed(root);
131
    let base = start_with(&served, vec![], Some(oxy_server::TlsFiles { cert: cert.clone(), key }));
132
    let url = format!("{base}/origin");
133
134
    // Without trusting the certificate the clone fails; with `OXY_CA` it works.
135
    let err = Repo::clone(&url, &root.join("untrusted")).err().unwrap().to_string();
136
    assert!(err.contains("certificate") || err.contains("tls") || err.contains("TLS") || err.contains("handshake"), "{err}");
137
    std::env::set_var("OXY_CA", &cert);
138
    let mut a = Repo::clone(&url, &root.join("a")).unwrap();
139
    std::env::remove_var("OXY_CA");
140
    // Persist the trust for this remote so later commands need no env var.
141
    a.config.set("remote.origin.ca", cert.to_str().unwrap()).unwrap();
142
    a.save_config().unwrap();
143
    write(&root.join("a"), "f.txt", b"tls\n");
144
    let h = a.commit("over tls", &[]).unwrap();
145
    assert!(a.push("origin", false).unwrap()[0].ok);
146
    let origin = Repo::open(&served.join("origin")).unwrap();
147
    assert_eq!(origin.bookmark_heads().unwrap(), vec![h]);
148
    // A wrong CA is refused.
149
    let (other_cert, _) = self_signed(&root.join("other"));
150
    a.config.set("remote.origin.ca", other_cert.to_str().unwrap()).unwrap();
151
    a.save_config().unwrap();
152
    assert!(a.fetch("origin").is_err());
153
}