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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
//! Integration coverage for the `mkit+s3` dispatch branch in
//! [`mkit_cli::remote_dispatch::open`].
//!
//! Two things are under test here:
//!
//! 1. `remote_dispatch::open` no longer returns `UnsupportedScheme` for
//! `mkit+s3://` URLs โ it delegates to [`S3Transport::connect`], which
//! parses the URL and reads credentials from the environment.
//! 2. Requests issued by the returned transport carry the `SigV4`
//! `Authorization`, `x-amz-date`, and `x-amz-content-sha256` headers
//! that the R2 endpoint expects.
//!
//! ## Why `with_parts` for the wire check
//!
//! `S3Transport::connect` rebuilds the URL as `https://<host>` because R2
//! only speaks HTTPS. `mockito` exposes a plain `http://127.0.0.1:<port>`
//! base, so we cannot point a URL-constructed transport at a mockito
//! server. Instead, we cover the scheme-dispatch branch with a smoke
//! test (`open()` succeeds), and cover the signed-request wire shape by
//! constructing the transport directly via the test-only
//! `S3Transport::with_parts` and asserting on the mockito-captured
//! headers.
use std::process::Command;
use mkit_cli::remote_dispatch;
use mkit_core::protocol::{PackKey, Transport};
use mkit_transport_s3::S3Transport;
use mkit_transport_s3::sigv4::Credentials;
use mockito::{Matcher, Server};
fn mkit_bin() -> &'static str {
env!("CARGO_BIN_EXE_mkit")
}
fn run_in(cwd: &std::path::Path, args: &[&str]) -> std::process::Output {
Command::new(mkit_bin())
.args(args)
.current_dir(cwd)
.output()
.expect("spawn mkit")
}
#[test]
fn open_accepts_mkit_s3_url() {
// Previously this branch returned `UnsupportedScheme`. With the
// wiring in place, `open` should now succeed for a syntactically
// valid `mkit+s3://` URL โ even if the credentials env vars are
// unset. Construction does NOT hit the network.
let tx = remote_dispatch::open("mkit+s3://r2.example.com/mybucket")
.expect("mkit+s3:// must now dispatch to S3Transport");
drop(tx);
}
#[test]
fn open_accepts_mkit_s3_url_with_prefix() {
let tx = remote_dispatch::open("mkit+s3://r2.example.com/mybucket/myproject")
.expect("mkit+s3:// with prefix must dispatch");
drop(tx);
}
#[test]
fn open_rejects_malformed_mkit_s3_url() {
// Missing bucket component.
let Err(err) = remote_dispatch::open("mkit+s3://host-only") else {
panic!("expected error for mkit+s3 URL without bucket");
};
let msg = err.to_string();
assert!(
msg.contains("transport") || msg.contains("malformed") || msg.contains("bucket"),
"unexpected error for malformed mkit+s3 URL: {msg}"
);
}
#[test]
fn signed_put_request_carries_sigv4_headers() {
// Build a transport manually (bypassing the `https://` rewrite in
// `S3Transport::connect`) and issue an `upload_pack` against a
// mockito endpoint. Assert the captured request carries the
// SPEC-TRANSPORT ยง6 `SigV4` header set:
// - Authorization: AWS4-HMAC-SHA256 ...
// - x-amz-date
// - x-amz-content-sha256
let mut server = Server::new();
let _m = server
.mock(
"PUT",
Matcher::Regex(r"^/mybucket/packs/[0-9a-f]{64}$".to_string()),
)
.match_header(
"authorization",
Matcher::Regex(r"^AWS4-HMAC-SHA256 ".to_string()),
)
.match_header("x-amz-date", Matcher::Any)
.match_header("x-amz-content-sha256", Matcher::Any)
.with_status(200)
.create();
let creds = Credentials {
access_key_id: "TESTKEY".into(),
secret_access_key: "TESTSECRET".into(),
region: "auto".into(),
};
let tx = S3Transport::with_parts(server.url(), "mybucket", None, creds)
.expect("build S3Transport with mockito endpoint");
let key = PackKey::new([0xAB; 32]);
tx.upload_pack(b"hello-pack", &key)
.expect("signed PUT must succeed against mockito");
// mockito asserts mock.expect_at_least(1) on drop; an unsigned or
// malformed request would have failed the header matchers above.
}
#[test]
fn credential_env_vars_are_consulted_at_connect_time() {
// Document the env-var contract. `S3Transport::connect` reads the
// `MKIT_R2_ACCESS_KEY_ID` and `MKIT_R2_SECRET_ACCESS_KEY` variables
// at construction. Absent vars default to empty strings โ no error
// at connect time. The first signed request then surfaces
// `AccessDenied` because the signer emits an unusable signature.
//
// We do NOT mutate env in-process here because `cargo test` runs
// tests concurrently and env mutation would race against sibling
// tests. We assert the contract textually instead, then let the
// `mkit` subcommand do the end-to-end env read via a subprocess.
assert_eq!(mkit_transport_s3::ENV_ACCESS_KEY, "MKIT_R2_ACCESS_KEY_ID");
assert_eq!(
mkit_transport_s3::ENV_SECRET_KEY,
"MKIT_R2_SECRET_ACCESS_KEY"
);
// Subprocess smoke: `mkit remote add` + `mkit remote` round-trips a
// `mkit+s3://` URL through config without error, even with no creds
// set. This proves the CLI accepts the scheme.
let td = tempfile::tempdir().unwrap();
assert!(run_in(td.path(), &["init"]).status.success());
let out = run_in(
td.path(),
&["remote", "add", "mkit+s3://r2.example.com/bkt/proj"],
);
assert!(out.status.success(), "remote add failed: {out:?}");
let out = run_in(td.path(), &["remote"]);
let stdout = String::from_utf8(out.stdout).unwrap();
assert!(stdout.contains("mkit+s3://r2.example.com/bkt/proj"));
assert!(stdout.contains("s3"));
}