Skip to content

Commit 8be80a2

Browse files
committed
Add a smoketest
Added a smoketest that publishes a module with some routes, then makes requests against those routes and makes some simple assertions about the responses. This revealed a bug introduced by the previous commit in the `/v1/database PUT` route, which was incorrectly not getting the `anon_auth_middleware` applied.
1 parent 3f61a28 commit 8be80a2

File tree

5 files changed

+185
-10
lines changed

5 files changed

+185
-10
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/client-api/src/routes/database.rs

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ async fn handle_http_route_impl<S: ControlStateDelegate + NodeDelegate>(
266266
let module_def = &module.info().module_def;
267267

268268
let Some((handler_id, _handler_def, _route_def)) = module_def.match_http_route(&st_method, &handler_path) else {
269-
return Ok(StatusCode::NOT_FOUND.into_response());
269+
return Ok((StatusCode::NOT_FOUND, NO_SUCH_ROUTE).into_response());
270270
};
271271

272272
let body = body.collect().await.map_err(log_and_500)?.to_bytes();
@@ -1399,7 +1399,15 @@ where
13991399
.route("/pre_publish", self.pre_publish)
14001400
.route("/reset", self.db_reset);
14011401

1402-
let authed_router = axum::Router::new()
1402+
let authed_root_router = axum::Router::new().route(
1403+
"/",
1404+
self.root_post.layer(axum::middleware::from_fn_with_state(
1405+
ctx.clone(),
1406+
anon_auth_middleware::<S>,
1407+
)),
1408+
);
1409+
1410+
let authed_named_router = axum::Router::new()
14031411
.nest("/:name_or_identity", db_router)
14041412
.route_layer(axum::middleware::from_fn_with_state(ctx, anon_auth_middleware::<S>));
14051413

@@ -1413,8 +1421,8 @@ where
14131421
.route("/:name_or_identity/route/*path", any(handle_http_route::<S>));
14141422

14151423
axum::Router::new()
1416-
.route("/", self.root_post)
1417-
.merge(authed_router)
1424+
.merge(authed_root_router)
1425+
.merge(authed_named_router)
14181426
.merge(http_route_router)
14191427
}
14201428
}
@@ -1658,21 +1666,21 @@ mod tests {
16581666
}
16591667

16601668
impl Authorization for DummyState {
1661-
fn authorize_action(
1669+
async fn authorize_action(
16621670
&self,
16631671
_subject: Identity,
16641672
_database: Identity,
16651673
_action: Action,
1666-
) -> impl std::future::Future<Output = Result<(), Unauthorized>> + Send {
1667-
async { Err(Unauthorized::InternalError(anyhow::anyhow!("unused"))) }
1674+
) -> Result<(), Unauthorized> {
1675+
Err(Unauthorized::InternalError(anyhow::anyhow!("unused")))
16681676
}
16691677

1670-
fn authorize_sql(
1678+
async fn authorize_sql(
16711679
&self,
16721680
_subject: Identity,
16731681
_database: Identity,
1674-
) -> impl std::future::Future<Output = Result<AuthCtx, Unauthorized>> + Send {
1675-
async { Err(Unauthorized::InternalError(anyhow::anyhow!("unused"))) }
1682+
) -> Result<AuthCtx, Unauthorized> {
1683+
Err(Unauthorized::InternalError(anyhow::anyhow!("unused")))
16761684
}
16771685
}
16781686

crates/smoketests/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ assert_cmd = "2"
2121
predicates = "3"
2222
tokio.workspace = true
2323
tokio-postgres.workspace = true
24+
reqwest = { workspace = true, features = ["blocking"] }
2425

2526
[lints]
2627
workspace = true
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
use spacetimedb_smoketests::Smoketest;
2+
3+
const MODULE_CODE: &str = r#"
4+
use spacetimedb::http::{Body, Request, Response, Router};
5+
use spacetimedb::HandlerContext;
6+
use spacetimedb::Table;
7+
8+
#[spacetimedb::table(accessor = entries, public)]
9+
pub struct Entry {
10+
id: u64,
11+
value: String,
12+
}
13+
14+
#[spacetimedb::http::handler]
15+
fn get_simple(_ctx: &mut HandlerContext, _req: Request) -> Response {
16+
Response::new(Body::from_bytes("ok"))
17+
}
18+
19+
#[spacetimedb::http::handler]
20+
fn post_insert(ctx: &mut HandlerContext, _req: Request) -> Response {
21+
ctx.with_tx(|tx| {
22+
let id = tx.db.entries().iter().count() as u64;
23+
tx.db.entries().insert(Entry {
24+
id,
25+
value: "posted".to_string(),
26+
});
27+
});
28+
Response::new(Body::from_bytes("inserted"))
29+
}
30+
31+
#[spacetimedb::http::handler]
32+
fn get_count(ctx: &mut HandlerContext, _req: Request) -> Response {
33+
let count = ctx.with_tx(|tx| tx.db.entries().iter().count());
34+
Response::new(Body::from_bytes(count.to_string()))
35+
}
36+
37+
#[spacetimedb::http::handler]
38+
fn any_handler(_ctx: &mut HandlerContext, _req: Request) -> Response {
39+
Response::new(Body::from_bytes("any"))
40+
}
41+
42+
#[spacetimedb::http::handler]
43+
fn header_echo(_ctx: &mut HandlerContext, req: Request) -> Response {
44+
let value = req
45+
.headers()
46+
.get("x-echo")
47+
.and_then(|value| value.to_str().ok())
48+
.unwrap_or("");
49+
Response::new(Body::from_bytes(value.to_string()))
50+
}
51+
52+
#[spacetimedb::http::handler]
53+
fn set_response_header(_ctx: &mut HandlerContext, _req: Request) -> Response {
54+
Response::builder()
55+
.header("x-response", "set")
56+
.body(Body::from_bytes("header-set"))
57+
.expect("response builder should not fail")
58+
}
59+
60+
#[spacetimedb::http::handler]
61+
fn body_handler(_ctx: &mut HandlerContext, _req: Request) -> Response {
62+
Response::new(Body::from_bytes("non-empty"))
63+
}
64+
65+
#[spacetimedb::http::handler]
66+
fn teapot(_ctx: &mut HandlerContext, _req: Request) -> Response {
67+
Response::builder()
68+
.status(418)
69+
.body(Body::from_bytes("teapot"))
70+
.expect("response builder should not fail")
71+
}
72+
73+
#[spacetimedb::http::router]
74+
fn router() -> Router {
75+
Router::new()
76+
.get("/get", get_simple)
77+
.post("/post", post_insert)
78+
.get("/count", get_count)
79+
.any("/any", any_handler)
80+
.get("/header", header_echo)
81+
.get("/set-header", set_response_header)
82+
.get("/body", body_handler)
83+
.get("/teapot", teapot)
84+
}
85+
"#;
86+
87+
const NO_SUCH_ROUTE_BODY: &str = "Database has not registered a handler for this route";
88+
89+
#[test]
90+
fn http_routes_end_to_end() {
91+
let test = Smoketest::builder().module_code(MODULE_CODE).build();
92+
let identity = test.database_identity.as_ref().expect("database identity missing");
93+
94+
let base = format!("{}/v1/database/{}/route", test.server_url, identity);
95+
let client = reqwest::blocking::Client::new();
96+
97+
let resp = client.get(format!("{base}/get")).send().expect("get failed");
98+
assert!(resp.status().is_success());
99+
assert_eq!(resp.text().expect("get body"), "ok");
100+
101+
let resp = client
102+
.post(format!("{base}/post"))
103+
.body("payload")
104+
.send()
105+
.expect("post failed");
106+
assert!(resp.status().is_success());
107+
108+
let resp = client.get(format!("{base}/count")).send().expect("count failed");
109+
assert!(resp.status().is_success());
110+
assert_eq!(resp.text().expect("count body"), "1");
111+
112+
let resp = client.put(format!("{base}/any")).send().expect("any failed");
113+
assert!(resp.status().is_success());
114+
assert_eq!(resp.text().expect("any body"), "any");
115+
116+
let resp = client
117+
.get(format!("{base}/header"))
118+
.header("x-echo", "hello")
119+
.send()
120+
.expect("header echo failed");
121+
assert!(resp.status().is_success());
122+
assert_eq!(resp.text().expect("header body"), "hello");
123+
124+
let resp = client
125+
.get(format!("{base}/set-header"))
126+
.send()
127+
.expect("set-header failed");
128+
assert!(resp.status().is_success());
129+
assert_eq!(
130+
resp.headers().get("x-response").and_then(|value| value.to_str().ok()),
131+
Some("set")
132+
);
133+
134+
let resp = client.get(format!("{base}/body")).send().expect("body failed");
135+
assert!(resp.status().is_success());
136+
assert_eq!(resp.text().expect("body text"), "non-empty");
137+
138+
let resp = client.get(format!("{base}/teapot")).send().expect("teapot failed");
139+
assert_eq!(resp.status().as_u16(), 418);
140+
141+
let resp = client
142+
.get(format!("{base}/missing"))
143+
.send()
144+
.expect("missing route failed");
145+
assert_eq!(resp.status().as_u16(), 404);
146+
assert_eq!(resp.text().expect("missing route body"), NO_SUCH_ROUTE_BODY);
147+
148+
let resp = client
149+
.get(format!(
150+
"{}/v1/database/{}/schema?version=10",
151+
test.server_url, identity
152+
))
153+
.header("authorization", "Bearer not-a-jwt")
154+
.send()
155+
.expect("schema request failed");
156+
assert!(resp.status().is_client_error());
157+
158+
let resp = client
159+
.get(format!("{base}/get"))
160+
.header("authorization", "Bearer not-a-jwt")
161+
.send()
162+
.expect("route request failed");
163+
assert!(resp.status().is_success());
164+
}

crates/smoketests/tests/smoketests/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ mod domains;
1919
mod fail_initial_publish;
2020
mod filtering;
2121
mod http_egress;
22+
mod http_routes;
2223
mod logs_level_filter;
2324
mod module_nested_op;
2425
mod modules;

0 commit comments

Comments
 (0)