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,244 @@
|
|
|
1
|
+
use axum::body::Body;
|
|
2
|
+
use axum::http::{Request, StatusCode};
|
|
3
|
+
use jsonwebtoken::{EncodingKey, Header, encode};
|
|
4
|
+
use spikard_http::server::build_router_with_handlers_and_config;
|
|
5
|
+
use spikard_http::{ApiKeyConfig, Claims, Handler, HandlerResult, JwtConfig, Method, RequestData, Route, ServerConfig};
|
|
6
|
+
use std::future::Future;
|
|
7
|
+
use std::pin::Pin;
|
|
8
|
+
use std::sync::Arc;
|
|
9
|
+
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
|
10
|
+
use tower::ServiceExt;
|
|
11
|
+
|
|
12
|
+
struct OkHandler;
|
|
13
|
+
|
|
14
|
+
impl Handler for OkHandler {
|
|
15
|
+
fn call(
|
|
16
|
+
&self,
|
|
17
|
+
_request: Request<Body>,
|
|
18
|
+
_request_data: RequestData,
|
|
19
|
+
) -> Pin<Box<dyn Future<Output = HandlerResult> + Send + '_>> {
|
|
20
|
+
Box::pin(async move {
|
|
21
|
+
Ok(axum::http::Response::builder()
|
|
22
|
+
.status(StatusCode::OK)
|
|
23
|
+
.body(Body::from("ok"))
|
|
24
|
+
.expect("response"))
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
fn route(method: Method, path: &str, handler_name: &str) -> Route {
|
|
30
|
+
Route {
|
|
31
|
+
method,
|
|
32
|
+
path: path.to_string(),
|
|
33
|
+
handler_name: handler_name.to_string(),
|
|
34
|
+
expects_json_body: false,
|
|
35
|
+
cors: None,
|
|
36
|
+
is_async: true,
|
|
37
|
+
file_params: None,
|
|
38
|
+
request_validator: None,
|
|
39
|
+
response_validator: None,
|
|
40
|
+
parameter_validator: None,
|
|
41
|
+
jsonrpc_method: None,
|
|
42
|
+
#[cfg(feature = "di")]
|
|
43
|
+
handler_dependencies: Vec::new(),
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
fn now_plus(seconds: u64) -> usize {
|
|
48
|
+
let now = SystemTime::now().duration_since(UNIX_EPOCH).expect("time");
|
|
49
|
+
usize::try_from((now + Duration::from_secs(seconds)).as_secs()).expect("timestamp fits usize")
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
#[tokio::test]
|
|
53
|
+
async fn jwt_auth_layer_rejects_missing_authorization() {
|
|
54
|
+
let config = ServerConfig {
|
|
55
|
+
jwt_auth: Some(JwtConfig {
|
|
56
|
+
secret: "secret".to_string(),
|
|
57
|
+
algorithm: "HS256".to_string(),
|
|
58
|
+
audience: None,
|
|
59
|
+
issuer: None,
|
|
60
|
+
leeway: 0,
|
|
61
|
+
}),
|
|
62
|
+
..Default::default()
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
let router = build_router_with_handlers_and_config(
|
|
66
|
+
vec![(
|
|
67
|
+
route(Method::Get, "/protected", "ok"),
|
|
68
|
+
Arc::new(OkHandler) as Arc<dyn Handler>,
|
|
69
|
+
)],
|
|
70
|
+
config,
|
|
71
|
+
Vec::new(),
|
|
72
|
+
)
|
|
73
|
+
.expect("router");
|
|
74
|
+
|
|
75
|
+
let response = router
|
|
76
|
+
.oneshot(
|
|
77
|
+
Request::builder()
|
|
78
|
+
.method("GET")
|
|
79
|
+
.uri("/protected")
|
|
80
|
+
.body(Body::empty())
|
|
81
|
+
.expect("request"),
|
|
82
|
+
)
|
|
83
|
+
.await
|
|
84
|
+
.expect("response");
|
|
85
|
+
|
|
86
|
+
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#[tokio::test]
|
|
90
|
+
async fn jwt_auth_layer_accepts_valid_bearer_token() {
|
|
91
|
+
let secret = "secret";
|
|
92
|
+
let config = ServerConfig {
|
|
93
|
+
jwt_auth: Some(JwtConfig {
|
|
94
|
+
secret: secret.to_string(),
|
|
95
|
+
algorithm: "HS256".to_string(),
|
|
96
|
+
audience: None,
|
|
97
|
+
issuer: None,
|
|
98
|
+
leeway: 0,
|
|
99
|
+
}),
|
|
100
|
+
..Default::default()
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
let claims = Claims {
|
|
104
|
+
sub: "user123".to_string(),
|
|
105
|
+
exp: now_plus(60),
|
|
106
|
+
iat: None,
|
|
107
|
+
nbf: None,
|
|
108
|
+
aud: None,
|
|
109
|
+
iss: None,
|
|
110
|
+
};
|
|
111
|
+
let token = encode(
|
|
112
|
+
&Header::default(),
|
|
113
|
+
&claims,
|
|
114
|
+
&EncodingKey::from_secret(secret.as_bytes()),
|
|
115
|
+
)
|
|
116
|
+
.expect("token");
|
|
117
|
+
|
|
118
|
+
let router = build_router_with_handlers_and_config(
|
|
119
|
+
vec![(
|
|
120
|
+
route(Method::Get, "/protected", "ok"),
|
|
121
|
+
Arc::new(OkHandler) as Arc<dyn Handler>,
|
|
122
|
+
)],
|
|
123
|
+
config,
|
|
124
|
+
Vec::new(),
|
|
125
|
+
)
|
|
126
|
+
.expect("router");
|
|
127
|
+
|
|
128
|
+
let response = router
|
|
129
|
+
.oneshot(
|
|
130
|
+
Request::builder()
|
|
131
|
+
.method("GET")
|
|
132
|
+
.uri("/protected")
|
|
133
|
+
.header("authorization", format!("Bearer {token}"))
|
|
134
|
+
.body(Body::empty())
|
|
135
|
+
.expect("request"),
|
|
136
|
+
)
|
|
137
|
+
.await
|
|
138
|
+
.expect("response");
|
|
139
|
+
|
|
140
|
+
assert_eq!(response.status(), StatusCode::OK);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
#[tokio::test]
|
|
144
|
+
async fn api_key_auth_layer_rejects_missing_key() {
|
|
145
|
+
let config = ServerConfig {
|
|
146
|
+
api_key_auth: Some(ApiKeyConfig {
|
|
147
|
+
keys: vec!["k1".to_string()],
|
|
148
|
+
header_name: "X-API-Key".to_string(),
|
|
149
|
+
}),
|
|
150
|
+
..Default::default()
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
let router = build_router_with_handlers_and_config(
|
|
154
|
+
vec![(
|
|
155
|
+
route(Method::Get, "/protected", "ok"),
|
|
156
|
+
Arc::new(OkHandler) as Arc<dyn Handler>,
|
|
157
|
+
)],
|
|
158
|
+
config,
|
|
159
|
+
Vec::new(),
|
|
160
|
+
)
|
|
161
|
+
.expect("router");
|
|
162
|
+
|
|
163
|
+
let response = router
|
|
164
|
+
.oneshot(
|
|
165
|
+
Request::builder()
|
|
166
|
+
.method("GET")
|
|
167
|
+
.uri("/protected")
|
|
168
|
+
.body(Body::empty())
|
|
169
|
+
.expect("request"),
|
|
170
|
+
)
|
|
171
|
+
.await
|
|
172
|
+
.expect("response");
|
|
173
|
+
|
|
174
|
+
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
#[tokio::test]
|
|
178
|
+
async fn api_key_auth_layer_accepts_valid_key_from_header() {
|
|
179
|
+
let config = ServerConfig {
|
|
180
|
+
api_key_auth: Some(ApiKeyConfig {
|
|
181
|
+
keys: vec!["k1".to_string()],
|
|
182
|
+
header_name: "X-API-Key".to_string(),
|
|
183
|
+
}),
|
|
184
|
+
..Default::default()
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
let router = build_router_with_handlers_and_config(
|
|
188
|
+
vec![(
|
|
189
|
+
route(Method::Get, "/protected", "ok"),
|
|
190
|
+
Arc::new(OkHandler) as Arc<dyn Handler>,
|
|
191
|
+
)],
|
|
192
|
+
config,
|
|
193
|
+
Vec::new(),
|
|
194
|
+
)
|
|
195
|
+
.expect("router");
|
|
196
|
+
|
|
197
|
+
let response = router
|
|
198
|
+
.oneshot(
|
|
199
|
+
Request::builder()
|
|
200
|
+
.method("GET")
|
|
201
|
+
.uri("/protected")
|
|
202
|
+
.header("x-api-key", "k1")
|
|
203
|
+
.body(Body::empty())
|
|
204
|
+
.expect("request"),
|
|
205
|
+
)
|
|
206
|
+
.await
|
|
207
|
+
.expect("response");
|
|
208
|
+
|
|
209
|
+
assert_eq!(response.status(), StatusCode::OK);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
#[tokio::test]
|
|
213
|
+
async fn api_key_auth_layer_accepts_valid_key_from_query_param() {
|
|
214
|
+
let config = ServerConfig {
|
|
215
|
+
api_key_auth: Some(ApiKeyConfig {
|
|
216
|
+
keys: vec!["k1".to_string()],
|
|
217
|
+
header_name: "X-API-Key".to_string(),
|
|
218
|
+
}),
|
|
219
|
+
..Default::default()
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
let router = build_router_with_handlers_and_config(
|
|
223
|
+
vec![(
|
|
224
|
+
route(Method::Get, "/protected", "ok"),
|
|
225
|
+
Arc::new(OkHandler) as Arc<dyn Handler>,
|
|
226
|
+
)],
|
|
227
|
+
config,
|
|
228
|
+
Vec::new(),
|
|
229
|
+
)
|
|
230
|
+
.expect("router");
|
|
231
|
+
|
|
232
|
+
let response = router
|
|
233
|
+
.oneshot(
|
|
234
|
+
Request::builder()
|
|
235
|
+
.method("GET")
|
|
236
|
+
.uri("/protected?api_key=k1")
|
|
237
|
+
.body(Body::empty())
|
|
238
|
+
.expect("request"),
|
|
239
|
+
)
|
|
240
|
+
.await
|
|
241
|
+
.expect("response");
|
|
242
|
+
|
|
243
|
+
assert_eq!(response.status(), StatusCode::OK);
|
|
244
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
use axum::body::Body;
|
|
2
|
+
use http_body_util::BodyExt;
|
|
3
|
+
use spikard_core::JsonRpcMethodInfo;
|
|
4
|
+
use spikard_http::handler_trait::{Handler, HandlerResult, RequestData};
|
|
5
|
+
use spikard_http::jsonrpc::JsonRpcConfig;
|
|
6
|
+
use spikard_http::openapi::OpenApiConfig;
|
|
7
|
+
use spikard_http::{Method, Route, Server, ServerConfig, StaticFilesConfig};
|
|
8
|
+
use std::pin::Pin;
|
|
9
|
+
use std::sync::Arc;
|
|
10
|
+
use tempfile::tempdir;
|
|
11
|
+
use tower::ServiceExt;
|
|
12
|
+
|
|
13
|
+
struct JsonOkHandler;
|
|
14
|
+
|
|
15
|
+
impl Handler for JsonOkHandler {
|
|
16
|
+
fn call(
|
|
17
|
+
&self,
|
|
18
|
+
_request: axum::http::Request<Body>,
|
|
19
|
+
_request_data: RequestData,
|
|
20
|
+
) -> Pin<Box<dyn std::future::Future<Output = HandlerResult> + Send + '_>> {
|
|
21
|
+
Box::pin(async move {
|
|
22
|
+
Ok(axum::http::Response::builder()
|
|
23
|
+
.status(200)
|
|
24
|
+
.header("content-type", "application/json")
|
|
25
|
+
.body(Body::from(r#"{"ok":true}"#))
|
|
26
|
+
.unwrap())
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fn route(path: &str, method: Method) -> Route {
|
|
32
|
+
Route {
|
|
33
|
+
path: path.to_string(),
|
|
34
|
+
method,
|
|
35
|
+
handler_name: "ok".to_string(),
|
|
36
|
+
expects_json_body: true,
|
|
37
|
+
cors: None,
|
|
38
|
+
is_async: true,
|
|
39
|
+
file_params: None,
|
|
40
|
+
request_validator: None,
|
|
41
|
+
response_validator: None,
|
|
42
|
+
parameter_validator: None,
|
|
43
|
+
jsonrpc_method: None,
|
|
44
|
+
#[cfg(feature = "di")]
|
|
45
|
+
handler_dependencies: vec![],
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
#[tokio::test]
|
|
50
|
+
async fn server_with_openapi_and_static_files_serves_expected_endpoints() {
|
|
51
|
+
let dir = tempdir().unwrap();
|
|
52
|
+
let index_path = dir.path().join("index.html");
|
|
53
|
+
std::fs::write(&index_path, "<h1>hello</h1>").unwrap();
|
|
54
|
+
|
|
55
|
+
let openapi = OpenApiConfig {
|
|
56
|
+
enabled: true,
|
|
57
|
+
title: "Test".to_string(),
|
|
58
|
+
version: "0.4.0".to_string(),
|
|
59
|
+
openapi_json_path: "/openapi.json".to_string(),
|
|
60
|
+
swagger_ui_path: "/docs".to_string(),
|
|
61
|
+
redoc_path: "/redoc".to_string(),
|
|
62
|
+
..Default::default()
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
let jsonrpc = JsonRpcConfig {
|
|
66
|
+
enabled: true,
|
|
67
|
+
endpoint_path: "/rpc".to_string(),
|
|
68
|
+
..Default::default()
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
let config = ServerConfig {
|
|
72
|
+
openapi: Some(openapi),
|
|
73
|
+
jsonrpc: Some(jsonrpc),
|
|
74
|
+
static_files: vec![StaticFilesConfig {
|
|
75
|
+
directory: dir.path().display().to_string(),
|
|
76
|
+
route_prefix: "/static".to_string(),
|
|
77
|
+
index_file: true,
|
|
78
|
+
cache_control: Some("public, max-age=60".to_string()),
|
|
79
|
+
}],
|
|
80
|
+
..Default::default()
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
let handler: Arc<dyn Handler> = Arc::new(JsonOkHandler);
|
|
84
|
+
let app = Server::with_handlers(config, vec![(route("/ping", Method::Get), handler)]).unwrap();
|
|
85
|
+
|
|
86
|
+
let openapi_response = app
|
|
87
|
+
.clone()
|
|
88
|
+
.oneshot(
|
|
89
|
+
axum::http::Request::builder()
|
|
90
|
+
.method("GET")
|
|
91
|
+
.uri("/openapi.json")
|
|
92
|
+
.body(Body::empty())
|
|
93
|
+
.unwrap(),
|
|
94
|
+
)
|
|
95
|
+
.await
|
|
96
|
+
.unwrap();
|
|
97
|
+
assert_eq!(openapi_response.status(), 200);
|
|
98
|
+
assert_eq!(
|
|
99
|
+
openapi_response
|
|
100
|
+
.headers()
|
|
101
|
+
.get("content-type")
|
|
102
|
+
.unwrap()
|
|
103
|
+
.to_str()
|
|
104
|
+
.unwrap(),
|
|
105
|
+
"application/json"
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
let openapi_body = openapi_response.into_body().collect().await.unwrap().to_bytes();
|
|
109
|
+
let openapi_json: serde_json::Value = serde_json::from_slice(&openapi_body).unwrap();
|
|
110
|
+
assert_eq!(openapi_json.get("openapi").and_then(|v| v.as_str()), Some("3.1.0"));
|
|
111
|
+
|
|
112
|
+
let docs_response = app
|
|
113
|
+
.clone()
|
|
114
|
+
.oneshot(
|
|
115
|
+
axum::http::Request::builder()
|
|
116
|
+
.method("GET")
|
|
117
|
+
.uri("/docs")
|
|
118
|
+
.body(Body::empty())
|
|
119
|
+
.unwrap(),
|
|
120
|
+
)
|
|
121
|
+
.await
|
|
122
|
+
.unwrap();
|
|
123
|
+
assert_eq!(docs_response.status(), 200);
|
|
124
|
+
let docs_body = docs_response.into_body().collect().await.unwrap().to_bytes();
|
|
125
|
+
assert!(String::from_utf8_lossy(&docs_body).contains("SwaggerUIBundle"));
|
|
126
|
+
|
|
127
|
+
let static_response = app
|
|
128
|
+
.oneshot(
|
|
129
|
+
axum::http::Request::builder()
|
|
130
|
+
.method("GET")
|
|
131
|
+
.uri("/static/")
|
|
132
|
+
.body(Body::empty())
|
|
133
|
+
.unwrap(),
|
|
134
|
+
)
|
|
135
|
+
.await
|
|
136
|
+
.unwrap();
|
|
137
|
+
assert_eq!(static_response.status(), 200);
|
|
138
|
+
assert_eq!(
|
|
139
|
+
static_response
|
|
140
|
+
.headers()
|
|
141
|
+
.get(axum::http::header::CACHE_CONTROL)
|
|
142
|
+
.unwrap()
|
|
143
|
+
.to_str()
|
|
144
|
+
.unwrap(),
|
|
145
|
+
"public, max-age=60"
|
|
146
|
+
);
|
|
147
|
+
let static_body = static_response.into_body().collect().await.unwrap().to_bytes();
|
|
148
|
+
assert!(String::from_utf8_lossy(&static_body).contains("<h1>hello</h1>"));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
#[tokio::test]
|
|
152
|
+
async fn server_registers_jsonrpc_endpoint_when_method_metadata_present() {
|
|
153
|
+
let jsonrpc = JsonRpcConfig {
|
|
154
|
+
enabled: true,
|
|
155
|
+
endpoint_path: "/rpc".to_string(),
|
|
156
|
+
..Default::default()
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
let config = ServerConfig {
|
|
160
|
+
jsonrpc: Some(jsonrpc),
|
|
161
|
+
..Default::default()
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
let mut rpc_route = route("/rpc_method", Method::Post);
|
|
165
|
+
rpc_route.jsonrpc_method = Some(JsonRpcMethodInfo {
|
|
166
|
+
method_name: "math.ping".to_string(),
|
|
167
|
+
description: Some("ping".to_string()),
|
|
168
|
+
params_schema: None,
|
|
169
|
+
result_schema: None,
|
|
170
|
+
deprecated: false,
|
|
171
|
+
tags: vec!["math".to_string()],
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
let handler: Arc<dyn Handler> = Arc::new(JsonOkHandler);
|
|
175
|
+
let app = Server::with_handlers(config, vec![(rpc_route, handler)]).unwrap();
|
|
176
|
+
|
|
177
|
+
let response = app
|
|
178
|
+
.oneshot(
|
|
179
|
+
axum::http::Request::builder()
|
|
180
|
+
.method("POST")
|
|
181
|
+
.uri("/rpc")
|
|
182
|
+
.header("content-type", "application/json")
|
|
183
|
+
.body(Body::from(r#"{"jsonrpc":"2.0","method":"math.ping","id":1}"#))
|
|
184
|
+
.unwrap(),
|
|
185
|
+
)
|
|
186
|
+
.await
|
|
187
|
+
.unwrap();
|
|
188
|
+
|
|
189
|
+
assert_eq!(response.status(), 200);
|
|
190
|
+
assert_eq!(
|
|
191
|
+
response.headers().get("content-type").unwrap().to_str().unwrap(),
|
|
192
|
+
"application/json"
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
let bytes = response.into_body().collect().await.unwrap().to_bytes();
|
|
196
|
+
let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
|
|
197
|
+
assert_eq!(json.get("jsonrpc").and_then(|v| v.as_str()), Some("2.0"));
|
|
198
|
+
assert_eq!(json.get("id").and_then(serde_json::Value::as_i64), Some(1));
|
|
199
|
+
assert_eq!(json.get("result"), Some(&serde_json::json!({"ok": true})));
|
|
200
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
use axum::http::StatusCode;
|
|
2
|
+
use spikard_http::server::build_router_with_handlers_and_config;
|
|
3
|
+
use spikard_http::{CorsConfig, Handler, HandlerResult, RequestData, Route, ServerConfig};
|
|
4
|
+
use std::future::Future;
|
|
5
|
+
use std::pin::Pin;
|
|
6
|
+
use std::sync::Arc;
|
|
7
|
+
|
|
8
|
+
struct OkHandler;
|
|
9
|
+
|
|
10
|
+
impl Handler for OkHandler {
|
|
11
|
+
fn call(
|
|
12
|
+
&self,
|
|
13
|
+
_request: axum::http::Request<axum::body::Body>,
|
|
14
|
+
_request_data: RequestData,
|
|
15
|
+
) -> Pin<Box<dyn Future<Output = HandlerResult> + Send + '_>> {
|
|
16
|
+
Box::pin(async move {
|
|
17
|
+
Ok(axum::http::Response::builder()
|
|
18
|
+
.status(StatusCode::OK)
|
|
19
|
+
.header("content-type", "application/json")
|
|
20
|
+
.body(axum::body::Body::from("{\"ok\":true}"))
|
|
21
|
+
.unwrap())
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#[tokio::test]
|
|
27
|
+
async fn router_generates_cors_preflight_when_missing_options_handler() {
|
|
28
|
+
let cors = CorsConfig {
|
|
29
|
+
allowed_origins: vec!["https://example.com".to_string()],
|
|
30
|
+
allowed_methods: vec!["GET".to_string(), "OPTIONS".to_string()],
|
|
31
|
+
allowed_headers: vec!["x-test".to_string()],
|
|
32
|
+
expose_headers: None,
|
|
33
|
+
max_age: Some(600),
|
|
34
|
+
allow_credentials: Some(true),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
let route = Route {
|
|
38
|
+
method: "GET".parse().unwrap(),
|
|
39
|
+
path: "/cors".to_string(),
|
|
40
|
+
handler_name: "ok".to_string(),
|
|
41
|
+
expects_json_body: false,
|
|
42
|
+
cors: Some(cors.clone()),
|
|
43
|
+
is_async: true,
|
|
44
|
+
file_params: None,
|
|
45
|
+
request_validator: None,
|
|
46
|
+
response_validator: None,
|
|
47
|
+
parameter_validator: None,
|
|
48
|
+
jsonrpc_method: None,
|
|
49
|
+
#[cfg(feature = "di")]
|
|
50
|
+
handler_dependencies: vec![],
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
let config = ServerConfig::default();
|
|
54
|
+
let router =
|
|
55
|
+
build_router_with_handlers_and_config(vec![(route, Arc::new(OkHandler))], config, Vec::new()).expect("router");
|
|
56
|
+
|
|
57
|
+
let server = axum_test::TestServer::new(router).unwrap();
|
|
58
|
+
|
|
59
|
+
let preflight = server
|
|
60
|
+
.method(axum::http::Method::OPTIONS, "/cors")
|
|
61
|
+
.add_header("origin", "https://example.com")
|
|
62
|
+
.add_header("access-control-request-method", "GET")
|
|
63
|
+
.add_header("access-control-request-headers", "x-test")
|
|
64
|
+
.await;
|
|
65
|
+
|
|
66
|
+
assert_eq!(preflight.status_code(), StatusCode::NO_CONTENT);
|
|
67
|
+
assert_eq!(
|
|
68
|
+
preflight.header("access-control-allow-origin").to_str().unwrap(),
|
|
69
|
+
"https://example.com"
|
|
70
|
+
);
|
|
71
|
+
assert!(
|
|
72
|
+
preflight
|
|
73
|
+
.header("access-control-allow-methods")
|
|
74
|
+
.to_str()
|
|
75
|
+
.unwrap()
|
|
76
|
+
.contains("GET")
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
let response = server.get("/cors").add_header("origin", "https://example.com").await;
|
|
80
|
+
assert_eq!(response.status_code(), StatusCode::OK);
|
|
81
|
+
assert!(response.text().contains("\"ok\":true"));
|
|
82
|
+
}
|