Skip to content

Commit 9ea3ea6

Browse files
Merge branch 'main' into dev
1 parent cf0d123 commit 9ea3ea6

File tree

4 files changed

+289
-0
lines changed

4 files changed

+289
-0
lines changed

.github/scripts/check_published.sh

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
CRATE_NAME="${1:?crate name is required}"
5+
6+
# Resolve the version using cargo metadata (works with workspace versioning)
7+
TARGET_VERSION=$(cargo metadata --format-version 1 --no-deps \
8+
| jq -r --arg N "$CRATE_NAME" '.packages[] | select(.name==$N) | .version')
9+
10+
if [ -z "$TARGET_VERSION" ] || [ "$TARGET_VERSION" = "null" ]; then
11+
echo "Could not resolve version for $CRATE_NAME" >&2
12+
exit 1
13+
fi
14+
15+
echo "Target version for $CRATE_NAME: $TARGET_VERSION"
16+
17+
# Query crates.io for existing versions
18+
EXISTING=$(curl -fsSL "https://crates.io/api/v1/crates/$CRATE_NAME" \
19+
| jq -r --arg V "$TARGET_VERSION" '.versions[] | select(.num==$V) | .num' \
20+
| head -n1 || true)
21+
22+
if [ "$EXISTING" = "$TARGET_VERSION" ]; then
23+
echo "already=true" >> "$GITHUB_OUTPUT"
24+
echo "Version $TARGET_VERSION already published for $CRATE_NAME; will skip."
25+
else
26+
echo "already=false" >> "$GITHUB_OUTPUT"
27+
echo "Version $TARGET_VERSION not yet published for $CRATE_NAME; will publish."
28+
fi
29+
30+
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
: "${CRATES:?CRATES env is required}"
5+
6+
# Ensure tags are available
7+
git fetch --tags --force || true
8+
9+
# Determine base ref to diff against (previous tag or repo root commit)
10+
if PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null); then
11+
BASE_REF="$PREV_TAG"
12+
else
13+
BASE_REF=$(git rev-list --max-parents=0 HEAD | tail -n1)
14+
fi
15+
echo "Using BASE_REF=$BASE_REF -> HEAD=${GITHUB_SHA:-HEAD}"
16+
17+
# Build JSON matrix with a changed flag per crate
18+
JSON='{"include":['
19+
SEP=""
20+
ANY_CHANGED=false
21+
while IFS= read -r entry; do
22+
[ -z "$entry" ] && continue
23+
name="${entry%%|*}"
24+
path="${entry##*|}"
25+
if git diff --quiet "$BASE_REF" "${GITHUB_SHA:-HEAD}" -- "$path"; then
26+
changed=false
27+
else
28+
changed=true
29+
ANY_CHANGED=true
30+
fi
31+
JSON+="$SEP{\"name\":\"$name\",\"path\":\"$path\",\"changed\":$changed}"
32+
SEP=","
33+
done <<< "${CRATES}"
34+
JSON+=']}'
35+
36+
echo "matrix=$JSON" >> "$GITHUB_OUTPUT"
37+
echo "any_changed=$ANY_CHANGED" >> "$GITHUB_OUTPUT"
38+
echo "Computed matrix: $JSON"
39+
40+

.github/scripts/publish_crate.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
cargo package --allow-dirty
5+
cargo publish --allow-dirty
6+
7+
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
use std::time::Duration;
2+
3+
use axum::response::sse::{Event as SseEvent, Sse};
4+
use axum::{
5+
extract::Path,
6+
http::{HeaderMap, HeaderValue, StatusCode},
7+
response::IntoResponse,
8+
routing::{get, post},
9+
Json, Router,
10+
};
11+
use futures_util::stream;
12+
use std::convert::Infallible;
13+
use tokio::net::TcpListener;
14+
use tokio::task::JoinHandle;
15+
16+
use verity_client::client::{NotaryInformation, VerityClient, VerityClientConfig};
17+
18+
async fn spawn_mock_server() -> (String, JoinHandle<()>) {
19+
// Bind to a random local port
20+
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
21+
let addr = listener.local_addr().unwrap();
22+
23+
let app = Router::new()
24+
.route(
25+
"/notaryinfo",
26+
get(|| async {
27+
let info = NotaryInformation {
28+
version: "test".to_string(),
29+
public_key: "-----BEGIN PUBLIC KEY---...".to_string(),
30+
git_commit_hash: "0000000000000000000000000000000000000000".to_string(),
31+
};
32+
Json(info)
33+
}),
34+
)
35+
.route("/proxy", post(proxy_handler).get(proxy_handler))
36+
.route("/proof/:id", get(proof_handler));
37+
38+
let handle = tokio::spawn(async move {
39+
axum::serve(listener, app.into_make_service())
40+
.await
41+
.unwrap();
42+
});
43+
44+
(format!("http://{}", addr), handle)
45+
}
46+
47+
async fn proxy_handler() -> impl IntoResponse {
48+
// Return a response header to indicate proof will arrive on SSE
49+
let mut headers = HeaderMap::new();
50+
headers.insert("T-PROOF-ID", HeaderValue::from_static("1"));
51+
(StatusCode::OK, headers, "ok")
52+
}
53+
54+
async fn proof_handler(Path(id): Path<String>) -> impl IntoResponse {
55+
// Simulate SSE stream. For simplicity, send a single event and close.
56+
// Format: notary_pub_key|proof
57+
let body = format!(
58+
"data: {}|proof-for-{}\n\n",
59+
"-----BEGIN PUBLIC KEY---...", id
60+
);
61+
let headers = {
62+
let mut h = HeaderMap::new();
63+
h.insert(
64+
"content-type",
65+
HeaderValue::from_static("text/event-stream"),
66+
);
67+
h
68+
};
69+
(headers, body)
70+
}
71+
72+
#[tokio::test]
73+
async fn get_notary_info_works() {
74+
let (base, _server) = spawn_mock_server().await;
75+
let client = VerityClient::new(
76+
VerityClientConfig::new(base.clone()).with_proof_timeout(Duration::from_millis(3000)),
77+
);
78+
79+
let info = client.get_notary_info().await.unwrap();
80+
assert_eq!(info.git_commit_hash.len(), 40);
81+
assert!(info.public_key.starts_with("-----BEGIN PUBLIC KEY"));
82+
}
83+
84+
#[tokio::test]
85+
async fn get_and_post_request_with_proof() {
86+
let (base, _server) = spawn_mock_server().await;
87+
let client = VerityClient::new(
88+
VerityClientConfig::new(base.clone()).with_proof_timeout(Duration::from_millis(3000)),
89+
);
90+
91+
// GET
92+
let res = client
93+
.get("https://jsonplaceholder.typicode.com/posts")
94+
.send()
95+
.await
96+
.unwrap();
97+
assert_eq!(res.subject.status(), StatusCode::OK);
98+
assert!(res.notary_pub_key.starts_with("-----BEGIN PUBLIC KEY"));
99+
assert!(res.proof.starts_with("proof-for-"));
100+
101+
// POST
102+
let res = client
103+
.post("https://jsonplaceholder.typicode.com/posts")
104+
.json(&serde_json::json!({"title":"foo","body":"bar","userId":1}))
105+
.send()
106+
.await
107+
.unwrap();
108+
assert_eq!(res.subject.status(), StatusCode::OK);
109+
assert!(res.notary_pub_key.starts_with("-----BEGIN PUBLIC KEY"));
110+
assert!(res.proof.starts_with("proof-for-"));
111+
}
112+
113+
#[tokio::test]
114+
async fn no_proof_header_returns_immediately() {
115+
use axum::routing::get as axget;
116+
use axum::Router as AxRouter;
117+
use tokio::net::TcpListener as TokioListener;
118+
119+
// Custom server where /proxy does NOT include T-PROOF-ID
120+
let listener = TokioListener::bind("127.0.0.1:0").await.unwrap();
121+
let addr = listener.local_addr().unwrap();
122+
123+
async fn proxy_no_proof() -> impl IntoResponse {
124+
(StatusCode::OK, "ok")
125+
}
126+
127+
async fn proof_never() -> impl IntoResponse {
128+
// Never send SSE events; keep the connection open
129+
let pending = stream::pending::<Result<SseEvent, Infallible>>();
130+
Sse::new(pending)
131+
}
132+
133+
let app = AxRouter::new()
134+
.route("/proxy", axget(proxy_no_proof).post(proxy_no_proof))
135+
.route("/proof/:id", axget(proof_never))
136+
.route(
137+
"/notaryinfo",
138+
axget(|| async {
139+
Json(NotaryInformation {
140+
version: "test".to_string(),
141+
public_key: "-----BEGIN PUBLIC KEY---...".to_string(),
142+
git_commit_hash: "0000000000000000000000000000000000000000".to_string(),
143+
})
144+
}),
145+
);
146+
147+
let _handle = tokio::spawn(async move {
148+
axum::serve(listener, app.into_make_service())
149+
.await
150+
.unwrap()
151+
});
152+
153+
let client = VerityClient::new(
154+
VerityClientConfig::new(format!("http://{}", addr))
155+
.with_proof_timeout(Duration::from_millis(500)),
156+
);
157+
158+
let res = client.get("https://example.com/").send().await.unwrap();
159+
160+
assert_eq!(res.subject.status(), StatusCode::OK);
161+
assert_eq!(res.notary_pub_key, "");
162+
assert_eq!(res.proof, "");
163+
}
164+
165+
#[tokio::test]
166+
async fn proof_timeout_errors() {
167+
// Server that sets T-PROOF-ID but never emits SSE
168+
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
169+
let addr = listener.local_addr().unwrap();
170+
171+
async fn proxy_with_proof_header(mut headers: HeaderMap) -> impl IntoResponse {
172+
headers.insert("T-PROOF-ID", HeaderValue::from_static("1"));
173+
(StatusCode::OK, headers, "ok")
174+
}
175+
176+
async fn proof_never() -> impl IntoResponse {
177+
// Never send SSE events; keep the connection open so client times out
178+
let pending = stream::pending::<Result<SseEvent, Infallible>>();
179+
Sse::new(pending)
180+
}
181+
182+
let app = Router::new()
183+
.route(
184+
"/proxy",
185+
get(proxy_with_proof_header).post(proxy_with_proof_header),
186+
)
187+
.route(
188+
"/notaryinfo",
189+
get(|| async {
190+
Json(NotaryInformation {
191+
version: "test".to_string(),
192+
public_key: "-----BEGIN PUBLIC KEY---...".to_string(),
193+
git_commit_hash: "0000000000000000000000000000000000000000".to_string(),
194+
})
195+
}),
196+
)
197+
.route("/proof/:id", get(proof_never));
198+
199+
let _handle = tokio::spawn(async move {
200+
axum::serve(listener, app.into_make_service())
201+
.await
202+
.unwrap()
203+
});
204+
205+
let client = VerityClient::new(
206+
VerityClientConfig::new(format!("http://{}", addr))
207+
.with_proof_timeout(Duration::from_millis(200)),
208+
);
209+
210+
let res = client.get("https://example.com/").send().await;
211+
assert!(res.is_err());
212+
}

0 commit comments

Comments
 (0)