spikard 0.6.2 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +90 -508
- data/ext/spikard_rb/Cargo.lock +3287 -0
- data/ext/spikard_rb/Cargo.toml +1 -1
- data/ext/spikard_rb/extconf.rb +3 -3
- data/lib/spikard/app.rb +72 -49
- data/lib/spikard/background.rb +38 -7
- data/lib/spikard/testing.rb +42 -4
- data/lib/spikard/version.rb +1 -1
- data/sig/spikard.rbs +4 -0
- data/vendor/crates/spikard-bindings-shared/Cargo.toml +1 -1
- data/vendor/crates/spikard-bindings-shared/tests/config_extractor_behavior.rs +191 -0
- data/vendor/crates/spikard-core/Cargo.toml +1 -1
- data/vendor/crates/spikard-core/src/http.rs +1 -0
- data/vendor/crates/spikard-core/src/lifecycle.rs +63 -0
- data/vendor/crates/spikard-core/tests/bindings_response_tests.rs +136 -0
- data/vendor/crates/spikard-core/tests/di_dependency_defaults.rs +37 -0
- data/vendor/crates/spikard-core/tests/error_mapper.rs +761 -0
- data/vendor/crates/spikard-core/tests/parameters_edge_cases.rs +106 -0
- data/vendor/crates/spikard-core/tests/parameters_full.rs +701 -0
- data/vendor/crates/spikard-core/tests/parameters_schema_and_formats.rs +301 -0
- data/vendor/crates/spikard-core/tests/request_data_roundtrip.rs +67 -0
- data/vendor/crates/spikard-core/tests/validation_coverage.rs +250 -0
- data/vendor/crates/spikard-core/tests/validation_error_paths.rs +45 -0
- data/vendor/crates/spikard-http/Cargo.toml +1 -1
- data/vendor/crates/spikard-http/src/jsonrpc/http_handler.rs +502 -0
- data/vendor/crates/spikard-http/src/jsonrpc/method_registry.rs +648 -0
- data/vendor/crates/spikard-http/src/jsonrpc/mod.rs +58 -0
- data/vendor/crates/spikard-http/src/jsonrpc/protocol.rs +1207 -0
- data/vendor/crates/spikard-http/src/jsonrpc/router.rs +2262 -0
- data/vendor/crates/spikard-http/src/testing/test_client.rs +155 -2
- data/vendor/crates/spikard-http/src/testing.rs +171 -0
- data/vendor/crates/spikard-http/src/websocket.rs +79 -6
- data/vendor/crates/spikard-http/tests/auth_integration.rs +647 -0
- data/vendor/crates/spikard-http/tests/common/test_builders.rs +633 -0
- data/vendor/crates/spikard-http/tests/di_handler_error_responses.rs +162 -0
- data/vendor/crates/spikard-http/tests/middleware_stack_integration.rs +389 -0
- data/vendor/crates/spikard-http/tests/request_extraction_full.rs +513 -0
- data/vendor/crates/spikard-http/tests/server_auth_middleware_behavior.rs +244 -0
- data/vendor/crates/spikard-http/tests/server_configured_router_behavior.rs +200 -0
- data/vendor/crates/spikard-http/tests/server_cors_preflight.rs +82 -0
- data/vendor/crates/spikard-http/tests/server_handler_wrappers.rs +464 -0
- data/vendor/crates/spikard-http/tests/server_method_router_additional_behavior.rs +286 -0
- data/vendor/crates/spikard-http/tests/server_method_router_coverage.rs +118 -0
- data/vendor/crates/spikard-http/tests/server_middleware_behavior.rs +99 -0
- data/vendor/crates/spikard-http/tests/server_middleware_branches.rs +206 -0
- data/vendor/crates/spikard-http/tests/server_openapi_jsonrpc_static.rs +281 -0
- data/vendor/crates/spikard-http/tests/server_router_behavior.rs +121 -0
- data/vendor/crates/spikard-http/tests/sse_full_behavior.rs +584 -0
- data/vendor/crates/spikard-http/tests/sse_handler_behavior.rs +130 -0
- data/vendor/crates/spikard-http/tests/test_client_requests.rs +167 -0
- data/vendor/crates/spikard-http/tests/testing_helpers.rs +87 -0
- data/vendor/crates/spikard-http/tests/testing_module_coverage.rs +156 -0
- data/vendor/crates/spikard-http/tests/urlencoded_content_type.rs +82 -0
- data/vendor/crates/spikard-http/tests/websocket_full_behavior.rs +440 -0
- data/vendor/crates/spikard-http/tests/websocket_integration.rs +152 -0
- data/vendor/crates/spikard-rb/Cargo.toml +1 -1
- data/vendor/crates/spikard-rb/src/gvl.rs +80 -0
- data/vendor/crates/spikard-rb/src/handler.rs +12 -9
- data/vendor/crates/spikard-rb/src/lib.rs +137 -124
- data/vendor/crates/spikard-rb/src/request.rs +342 -0
- data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +1 -8
- data/vendor/crates/spikard-rb/src/server.rs +1 -8
- data/vendor/crates/spikard-rb/src/testing/client.rs +168 -9
- data/vendor/crates/spikard-rb/src/websocket.rs +119 -30
- data/vendor/crates/spikard-rb-macros/Cargo.toml +14 -0
- data/vendor/crates/spikard-rb-macros/src/lib.rs +52 -0
- metadata +44 -1
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
use axum::body::Body;
|
|
2
|
+
use axum::http::{Request, StatusCode};
|
|
3
|
+
use axum::response::IntoResponse;
|
|
4
|
+
use axum::{Router, routing::any};
|
|
5
|
+
use spikard_http::testing::{MultipartFilePart, TestClient};
|
|
6
|
+
|
|
7
|
+
async fn echo(req: Request<Body>) -> axum::response::Response {
|
|
8
|
+
let method = req.method().to_string();
|
|
9
|
+
let uri = req.uri().to_string();
|
|
10
|
+
let headers = req
|
|
11
|
+
.headers()
|
|
12
|
+
.iter()
|
|
13
|
+
.fold(serde_json::Map::new(), |mut map, (key, value)| {
|
|
14
|
+
map.insert(
|
|
15
|
+
key.to_string(),
|
|
16
|
+
serde_json::Value::String(value.to_str().unwrap_or("").to_string()),
|
|
17
|
+
);
|
|
18
|
+
map
|
|
19
|
+
});
|
|
20
|
+
let bytes = axum::body::to_bytes(req.into_body(), usize::MAX).await.unwrap();
|
|
21
|
+
let body_text = String::from_utf8_lossy(&bytes).to_string();
|
|
22
|
+
|
|
23
|
+
let payload = serde_json::json!({
|
|
24
|
+
"method": method,
|
|
25
|
+
"uri": uri,
|
|
26
|
+
"headers": headers,
|
|
27
|
+
"body": body_text,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
(StatusCode::OK, axum::Json(payload)).into_response()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
#[tokio::test]
|
|
34
|
+
async fn test_client_sends_query_headers_and_bodies() {
|
|
35
|
+
let app = Router::new().route("/{*path}", any(echo));
|
|
36
|
+
let client = TestClient::from_router(app).expect("client");
|
|
37
|
+
|
|
38
|
+
let snapshot = client
|
|
39
|
+
.get(
|
|
40
|
+
"/items",
|
|
41
|
+
Some(vec![("q".to_string(), "a b".to_string())]),
|
|
42
|
+
Some(vec![("x-test".to_string(), "1".to_string())]),
|
|
43
|
+
)
|
|
44
|
+
.await
|
|
45
|
+
.expect("get");
|
|
46
|
+
assert_eq!(snapshot.status, 200);
|
|
47
|
+
let json = snapshot.json().expect("json");
|
|
48
|
+
assert_eq!(json["method"], "GET");
|
|
49
|
+
assert!(json["uri"].as_str().unwrap().contains("/items?q=a%20b"));
|
|
50
|
+
assert_eq!(json["headers"]["x-test"], "1");
|
|
51
|
+
|
|
52
|
+
let snapshot = client
|
|
53
|
+
.post(
|
|
54
|
+
"/json",
|
|
55
|
+
Some(serde_json::json!({"hello":"world"})),
|
|
56
|
+
None,
|
|
57
|
+
None,
|
|
58
|
+
None,
|
|
59
|
+
None,
|
|
60
|
+
)
|
|
61
|
+
.await
|
|
62
|
+
.expect("post");
|
|
63
|
+
let json = snapshot.json().expect("json");
|
|
64
|
+
assert_eq!(json["method"], "POST");
|
|
65
|
+
assert!(json["body"].as_str().unwrap().contains("\"hello\":\"world\""));
|
|
66
|
+
|
|
67
|
+
let snapshot = client
|
|
68
|
+
.post(
|
|
69
|
+
"/form",
|
|
70
|
+
None,
|
|
71
|
+
Some(vec![("a".to_string(), "b".to_string())]),
|
|
72
|
+
None,
|
|
73
|
+
None,
|
|
74
|
+
None,
|
|
75
|
+
)
|
|
76
|
+
.await
|
|
77
|
+
.expect("post");
|
|
78
|
+
let json = snapshot.json().expect("json");
|
|
79
|
+
let body = json["body"].as_str().unwrap();
|
|
80
|
+
assert!(body.contains('a'));
|
|
81
|
+
assert!(body.contains('b'));
|
|
82
|
+
|
|
83
|
+
let snapshot = client
|
|
84
|
+
.post(
|
|
85
|
+
"/multipart",
|
|
86
|
+
None,
|
|
87
|
+
None,
|
|
88
|
+
Some((
|
|
89
|
+
vec![("field".to_string(), "value".to_string())],
|
|
90
|
+
vec![MultipartFilePart {
|
|
91
|
+
field_name: "file".to_string(),
|
|
92
|
+
filename: "hello.txt".to_string(),
|
|
93
|
+
content_type: Some("text/plain".to_string()),
|
|
94
|
+
content: b"hello".to_vec(),
|
|
95
|
+
}],
|
|
96
|
+
)),
|
|
97
|
+
None,
|
|
98
|
+
None,
|
|
99
|
+
)
|
|
100
|
+
.await
|
|
101
|
+
.expect("post");
|
|
102
|
+
let json = snapshot.json().expect("json");
|
|
103
|
+
assert!(
|
|
104
|
+
json["headers"]["content-type"]
|
|
105
|
+
.as_str()
|
|
106
|
+
.unwrap()
|
|
107
|
+
.contains("multipart/form-data")
|
|
108
|
+
);
|
|
109
|
+
assert!(json["body"].as_str().unwrap().contains("hello"));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
#[tokio::test]
|
|
113
|
+
async fn test_client_supports_other_http_methods_and_query_merging() {
|
|
114
|
+
let app = Router::new().route("/{*path}", any(echo));
|
|
115
|
+
let client = TestClient::from_router(app).expect("client");
|
|
116
|
+
|
|
117
|
+
let snapshot = client
|
|
118
|
+
.put(
|
|
119
|
+
"/put",
|
|
120
|
+
Some(serde_json::json!({"name":"spikard"})),
|
|
121
|
+
None,
|
|
122
|
+
Some(vec![("x-test".to_string(), "2".to_string())]),
|
|
123
|
+
)
|
|
124
|
+
.await
|
|
125
|
+
.expect("put");
|
|
126
|
+
let json = snapshot.json().expect("json");
|
|
127
|
+
assert_eq!(json["method"], "PUT");
|
|
128
|
+
assert_eq!(json["headers"]["x-test"], "2");
|
|
129
|
+
assert!(json["body"].as_str().unwrap().contains("\"name\":\"spikard\""));
|
|
130
|
+
|
|
131
|
+
let snapshot = client.delete("/delete", None, None).await.expect("delete");
|
|
132
|
+
assert_eq!(snapshot.json().expect("json")["method"], "DELETE");
|
|
133
|
+
|
|
134
|
+
let snapshot = client.options("/options", None, None).await.expect("options");
|
|
135
|
+
assert_eq!(snapshot.json().expect("json")["method"], "OPTIONS");
|
|
136
|
+
|
|
137
|
+
let snapshot = client.head("/head", None, None).await.expect("head");
|
|
138
|
+
assert_eq!(snapshot.status, 200);
|
|
139
|
+
assert!(snapshot.body.is_empty());
|
|
140
|
+
|
|
141
|
+
let snapshot = client.trace("/trace", None, None).await.expect("trace");
|
|
142
|
+
assert_eq!(snapshot.json().expect("json")["method"], "TRACE");
|
|
143
|
+
|
|
144
|
+
let snapshot = client
|
|
145
|
+
.get("/items?x=1", Some(vec![("y".to_string(), "2".to_string())]), None)
|
|
146
|
+
.await
|
|
147
|
+
.expect("get");
|
|
148
|
+
assert!(
|
|
149
|
+
snapshot.json().expect("json")["uri"]
|
|
150
|
+
.as_str()
|
|
151
|
+
.unwrap()
|
|
152
|
+
.contains("x=1&y=2")
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
#[tokio::test]
|
|
157
|
+
async fn test_client_rejects_invalid_header_names() {
|
|
158
|
+
let app = Router::new().route("/{*path}", any(echo));
|
|
159
|
+
let client = TestClient::from_router(app).expect("client");
|
|
160
|
+
|
|
161
|
+
let error = client
|
|
162
|
+
.get("/items", None, Some(vec![("bad\n".to_string(), "1".to_string())]))
|
|
163
|
+
.await
|
|
164
|
+
.expect_err("invalid header");
|
|
165
|
+
let message = error.to_string();
|
|
166
|
+
assert!(message.contains("Invalid header name"));
|
|
167
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
use axum::http::HeaderValue;
|
|
2
|
+
use axum::{Router, response::IntoResponse, routing::get};
|
|
3
|
+
use brotli::CompressorWriter;
|
|
4
|
+
use flate2::Compression;
|
|
5
|
+
use flate2::write::GzEncoder;
|
|
6
|
+
use spikard_http::testing::{MultipartFilePart, build_multipart_body, encode_urlencoded_body, snapshot_response};
|
|
7
|
+
use std::io::Write;
|
|
8
|
+
|
|
9
|
+
#[test]
|
|
10
|
+
fn urlencoded_encoding_handles_scalars_and_objects() {
|
|
11
|
+
let s = serde_json::Value::String("a=b&c=d".to_string());
|
|
12
|
+
assert_eq!(encode_urlencoded_body(&s).unwrap(), b"a=b&c=d".to_vec());
|
|
13
|
+
|
|
14
|
+
let mut obj = serde_json::Map::new();
|
|
15
|
+
obj.insert("name".to_string(), serde_json::Value::String("Alice".to_string()));
|
|
16
|
+
obj.insert("tags".to_string(), serde_json::json!(["a", "b"]));
|
|
17
|
+
let value = serde_json::Value::Object(obj);
|
|
18
|
+
let encoded = String::from_utf8(encode_urlencoded_body(&value).unwrap()).unwrap();
|
|
19
|
+
assert!(encoded.contains("name=Alice"));
|
|
20
|
+
assert!(encoded.contains("tags"));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#[test]
|
|
24
|
+
fn multipart_body_contains_fields_and_files() {
|
|
25
|
+
let (body, boundary) = build_multipart_body(
|
|
26
|
+
&[("field".to_string(), "value".to_string())],
|
|
27
|
+
&[MultipartFilePart {
|
|
28
|
+
field_name: "file".to_string(),
|
|
29
|
+
filename: "hello.txt".to_string(),
|
|
30
|
+
content_type: Some("text/plain".to_string()),
|
|
31
|
+
content: b"hello".to_vec(),
|
|
32
|
+
}],
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
let body_str = String::from_utf8_lossy(&body);
|
|
36
|
+
assert!(body_str.contains(&format!("--{boundary}")));
|
|
37
|
+
assert!(body_str.contains("name=\"field\""));
|
|
38
|
+
assert!(body_str.contains("value"));
|
|
39
|
+
assert!(body_str.contains("name=\"file\"; filename=\"hello.txt\""));
|
|
40
|
+
assert!(body_str.contains("Content-Type: text/plain"));
|
|
41
|
+
assert!(body_str.contains("hello"));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#[tokio::test]
|
|
45
|
+
async fn snapshot_response_decodes_gzip_body() {
|
|
46
|
+
let app = Router::new().route(
|
|
47
|
+
"/gzip",
|
|
48
|
+
get(|| async move {
|
|
49
|
+
let raw = b"hello gzip".to_vec();
|
|
50
|
+
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
|
|
51
|
+
encoder.write_all(&raw).unwrap();
|
|
52
|
+
let compressed = encoder.finish().unwrap();
|
|
53
|
+
|
|
54
|
+
([("content-encoding", HeaderValue::from_static("gzip"))], compressed).into_response()
|
|
55
|
+
}),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
let server = axum_test::TestServer::new(app).unwrap();
|
|
59
|
+
let response = server.get("/gzip").await;
|
|
60
|
+
|
|
61
|
+
let snapshot = snapshot_response(response).await.expect("snapshot failed");
|
|
62
|
+
assert_eq!(snapshot.status, 200);
|
|
63
|
+
assert_eq!(snapshot.text().unwrap(), "hello gzip");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
#[tokio::test]
|
|
67
|
+
async fn snapshot_response_decodes_brotli_body() {
|
|
68
|
+
let app = Router::new().route(
|
|
69
|
+
"/br",
|
|
70
|
+
get(|| async move {
|
|
71
|
+
let raw = b"hello br".to_vec();
|
|
72
|
+
let mut writer = CompressorWriter::new(Vec::new(), 4096, 6, 22);
|
|
73
|
+
writer.write_all(&raw).unwrap();
|
|
74
|
+
writer.flush().unwrap();
|
|
75
|
+
let compressed = writer.into_inner();
|
|
76
|
+
|
|
77
|
+
([("content-encoding", HeaderValue::from_static("br"))], compressed).into_response()
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
let server = axum_test::TestServer::new(app).unwrap();
|
|
82
|
+
let response = server.get("/br").await;
|
|
83
|
+
|
|
84
|
+
let snapshot = snapshot_response(response).await.expect("snapshot failed");
|
|
85
|
+
assert_eq!(snapshot.status, 200);
|
|
86
|
+
assert_eq!(snapshot.text().unwrap(), "hello br");
|
|
87
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
use axum::body::Body;
|
|
2
|
+
use axum::http::{HeaderValue, Request, StatusCode};
|
|
3
|
+
use axum::response::IntoResponse;
|
|
4
|
+
use axum::routing::get;
|
|
5
|
+
use flate2::Compression;
|
|
6
|
+
use flate2::write::GzEncoder;
|
|
7
|
+
use spikard_http::testing::{SnapshotError, WebSocketMessage, call_test_server, connect_websocket, snapshot_response};
|
|
8
|
+
use std::io::Write;
|
|
9
|
+
|
|
10
|
+
#[tokio::test]
|
|
11
|
+
async fn call_test_server_preserves_method_headers_query_and_body() {
|
|
12
|
+
let app = axum::Router::new().route(
|
|
13
|
+
"/echo",
|
|
14
|
+
get(|req: Request<Body>| async move {
|
|
15
|
+
let method = req.method().to_string();
|
|
16
|
+
let uri = req.uri().to_string();
|
|
17
|
+
let header = req
|
|
18
|
+
.headers()
|
|
19
|
+
.get("x-test")
|
|
20
|
+
.and_then(|v| v.to_str().ok())
|
|
21
|
+
.map_or_else(|| "<missing>".to_string(), str::to_string);
|
|
22
|
+
let bytes = axum::body::to_bytes(req.into_body(), usize::MAX).await.unwrap();
|
|
23
|
+
(StatusCode::OK, format!("{method} {uri} {header} {}", bytes.len())).into_response()
|
|
24
|
+
}),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
let server = axum_test::TestServer::new(app).expect("server");
|
|
28
|
+
let request = Request::builder()
|
|
29
|
+
.method("GET")
|
|
30
|
+
.uri("/echo?q=1")
|
|
31
|
+
.header("x-test", "1")
|
|
32
|
+
.body(Body::from("abc"))
|
|
33
|
+
.expect("request");
|
|
34
|
+
|
|
35
|
+
let response = call_test_server(&server, request).await;
|
|
36
|
+
assert_eq!(response.status_code(), StatusCode::OK);
|
|
37
|
+
let text = response.text();
|
|
38
|
+
assert!(text.contains("GET"));
|
|
39
|
+
assert!(text.contains("/echo"));
|
|
40
|
+
assert!(text.contains("q=1"));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#[tokio::test]
|
|
44
|
+
async fn snapshot_response_reports_invalid_headers_and_decompression_errors() {
|
|
45
|
+
let bad_header = HeaderValue::from_bytes(b"\xFF").expect("header value");
|
|
46
|
+
let app = axum::Router::new()
|
|
47
|
+
.route(
|
|
48
|
+
"/bad-header",
|
|
49
|
+
get(move || async move {
|
|
50
|
+
(
|
|
51
|
+
StatusCode::OK,
|
|
52
|
+
[(axum::http::header::HeaderName::from_static("x-bad"), bad_header.clone())],
|
|
53
|
+
"ok",
|
|
54
|
+
)
|
|
55
|
+
}),
|
|
56
|
+
)
|
|
57
|
+
.route(
|
|
58
|
+
"/bad-gzip",
|
|
59
|
+
get(|| async move {
|
|
60
|
+
(
|
|
61
|
+
StatusCode::OK,
|
|
62
|
+
[(axum::http::header::CONTENT_ENCODING, "gzip")],
|
|
63
|
+
vec![0_u8, 1, 2, 3],
|
|
64
|
+
)
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
let server = axum_test::TestServer::new(app).expect("server");
|
|
69
|
+
|
|
70
|
+
let err = snapshot_response(server.get("/bad-header").await)
|
|
71
|
+
.await
|
|
72
|
+
.expect_err("invalid header");
|
|
73
|
+
assert!(matches!(err, SnapshotError::InvalidHeader(_)));
|
|
74
|
+
|
|
75
|
+
let err = snapshot_response(server.get("/bad-gzip").await)
|
|
76
|
+
.await
|
|
77
|
+
.expect_err("bad gzip");
|
|
78
|
+
assert!(matches!(err, SnapshotError::Decompression(_)));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#[tokio::test]
|
|
82
|
+
async fn websocket_testing_wrappers_roundtrip_and_message_helpers() {
|
|
83
|
+
let app = axum::Router::new().route(
|
|
84
|
+
"/ws",
|
|
85
|
+
get(|ws: axum::extract::ws::WebSocketUpgrade| async move {
|
|
86
|
+
ws.on_upgrade(|mut socket| async move {
|
|
87
|
+
while let Some(msg) = socket.recv().await {
|
|
88
|
+
match msg {
|
|
89
|
+
Ok(axum::extract::ws::Message::Text(text)) => {
|
|
90
|
+
let _ = socket.send(axum::extract::ws::Message::Text(text)).await;
|
|
91
|
+
}
|
|
92
|
+
Ok(axum::extract::ws::Message::Binary(data)) => {
|
|
93
|
+
let _ = socket.send(axum::extract::ws::Message::Binary(data)).await;
|
|
94
|
+
}
|
|
95
|
+
Ok(axum::extract::ws::Message::Ping(data)) => {
|
|
96
|
+
let _ = socket.send(axum::extract::ws::Message::Pong(data)).await;
|
|
97
|
+
}
|
|
98
|
+
Ok(axum::extract::ws::Message::Close(_)) | Err(_) => break,
|
|
99
|
+
Ok(axum::extract::ws::Message::Pong(_)) => {}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
}),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
let server = axum_test::TestServer::new_with_config(
|
|
107
|
+
app,
|
|
108
|
+
axum_test::TestServerConfig {
|
|
109
|
+
transport: Some(axum_test::Transport::HttpRandomPort),
|
|
110
|
+
..axum_test::TestServerConfig::default()
|
|
111
|
+
},
|
|
112
|
+
)
|
|
113
|
+
.expect("server");
|
|
114
|
+
|
|
115
|
+
let mut ws = connect_websocket(&server, "/ws").await;
|
|
116
|
+
|
|
117
|
+
ws.send_text("hi").await;
|
|
118
|
+
let msg = ws.receive_message().await;
|
|
119
|
+
assert_eq!(msg.as_text(), Some("hi"));
|
|
120
|
+
assert!(msg.as_json().is_err());
|
|
121
|
+
|
|
122
|
+
ws.send_message(axum_test::WsMessage::Binary(bytes::Bytes::from_static(b"bin")))
|
|
123
|
+
.await;
|
|
124
|
+
let msg = ws.receive_message().await;
|
|
125
|
+
assert_eq!(msg.as_binary().expect("binary"), b"bin");
|
|
126
|
+
assert!(msg.as_json().is_err());
|
|
127
|
+
|
|
128
|
+
ws.send_message(axum_test::WsMessage::Ping(bytes::Bytes::from_static(b"ping")))
|
|
129
|
+
.await;
|
|
130
|
+
let msg = ws.receive_message().await;
|
|
131
|
+
assert!(matches!(msg, WebSocketMessage::Pong(_)));
|
|
132
|
+
|
|
133
|
+
ws.close().await;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
#[tokio::test]
|
|
137
|
+
async fn snapshot_response_decodes_gzip_body() {
|
|
138
|
+
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
|
|
139
|
+
encoder.write_all(b"hello gzip").expect("write");
|
|
140
|
+
let gzipped = encoder.finish().expect("finish");
|
|
141
|
+
|
|
142
|
+
let app = axum::Router::new().route(
|
|
143
|
+
"/gzip",
|
|
144
|
+
get(move || async move {
|
|
145
|
+
(
|
|
146
|
+
StatusCode::OK,
|
|
147
|
+
[(axum::http::header::CONTENT_ENCODING, "gzip")],
|
|
148
|
+
gzipped.clone(),
|
|
149
|
+
)
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
let server = axum_test::TestServer::new(app).expect("server");
|
|
154
|
+
let snapshot = snapshot_response(server.get("/gzip").await).await.expect("snapshot");
|
|
155
|
+
assert_eq!(snapshot.text().expect("text"), "hello gzip");
|
|
156
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#![allow(clippy::pedantic, clippy::nursery, clippy::all)]
|
|
2
|
+
//! Integration coverage for validate_content_type_middleware with urlencoded bodies.
|
|
3
|
+
|
|
4
|
+
use axum::Router;
|
|
5
|
+
use axum::extract::Extension;
|
|
6
|
+
use axum::http::{HeaderMap, StatusCode};
|
|
7
|
+
use axum::middleware;
|
|
8
|
+
use axum::routing::post;
|
|
9
|
+
use spikard_http::middleware::PreReadBody;
|
|
10
|
+
use spikard_http::middleware::{RouteInfo, validate_content_type_middleware};
|
|
11
|
+
|
|
12
|
+
/// Build a router with the content-type middleware and route configuration.
|
|
13
|
+
fn build_router(route_info: RouteInfo) -> Router {
|
|
14
|
+
Router::new()
|
|
15
|
+
.route(
|
|
16
|
+
"/forms",
|
|
17
|
+
post(
|
|
18
|
+
|headers: HeaderMap, Extension(pre_read): Extension<PreReadBody>| async move {
|
|
19
|
+
let content_type = headers
|
|
20
|
+
.get(axum::http::header::CONTENT_TYPE)
|
|
21
|
+
.and_then(|h| h.to_str().ok())
|
|
22
|
+
.unwrap_or_default()
|
|
23
|
+
.to_string();
|
|
24
|
+
let body_str = String::from_utf8(pre_read.0.to_vec()).unwrap();
|
|
25
|
+
(
|
|
26
|
+
StatusCode::OK,
|
|
27
|
+
axum::Json(serde_json::json!({ "content_type": content_type, "body": body_str })),
|
|
28
|
+
)
|
|
29
|
+
},
|
|
30
|
+
),
|
|
31
|
+
)
|
|
32
|
+
.layer(middleware::from_fn_with_state(
|
|
33
|
+
route_info,
|
|
34
|
+
validate_content_type_middleware,
|
|
35
|
+
))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
#[tokio::test]
|
|
39
|
+
async fn urlencoded_body_is_transformed_to_json() {
|
|
40
|
+
let app = build_router(RouteInfo {
|
|
41
|
+
expects_json_body: true,
|
|
42
|
+
});
|
|
43
|
+
let server = axum_test::TestServer::new(app).expect("start test server");
|
|
44
|
+
|
|
45
|
+
let response = server
|
|
46
|
+
.post("/forms")
|
|
47
|
+
.text("name=alice&active=true&count=3&empty=")
|
|
48
|
+
.content_type("application/x-www-form-urlencoded")
|
|
49
|
+
.await;
|
|
50
|
+
|
|
51
|
+
assert_eq!(response.status_code(), StatusCode::OK);
|
|
52
|
+
let payload: serde_json::Value = response.json();
|
|
53
|
+
assert_eq!(payload["content_type"], "application/json");
|
|
54
|
+
|
|
55
|
+
let body_json: serde_json::Value =
|
|
56
|
+
serde_json::from_str(payload["body"].as_str().expect("body string")).expect("valid json");
|
|
57
|
+
assert_eq!(body_json["name"], "alice");
|
|
58
|
+
assert_eq!(body_json["active"], true);
|
|
59
|
+
assert_eq!(body_json["count"], 3);
|
|
60
|
+
assert_eq!(body_json["empty"], "");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#[tokio::test]
|
|
64
|
+
async fn invalid_charset_on_json_returns_unsupported_media_type() {
|
|
65
|
+
let app = build_router(RouteInfo {
|
|
66
|
+
expects_json_body: true,
|
|
67
|
+
});
|
|
68
|
+
let server = axum_test::TestServer::new(app).expect("start test server");
|
|
69
|
+
|
|
70
|
+
let response = server
|
|
71
|
+
.post("/forms")
|
|
72
|
+
.text("{\"name\":\"alice\"}")
|
|
73
|
+
.content_type("application/json; charset=utf-16")
|
|
74
|
+
.await;
|
|
75
|
+
|
|
76
|
+
assert_eq!(response.status_code(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
|
|
77
|
+
let body: serde_json::Value = response.json();
|
|
78
|
+
assert_eq!(
|
|
79
|
+
body["type"],
|
|
80
|
+
serde_json::Value::String("https://spikard.dev/errors/unsupported-charset".to_string())
|
|
81
|
+
);
|
|
82
|
+
}
|