1#![warn(clippy::all)]
94#![warn(missing_copy_implementations)]
95#![warn(missing_debug_implementations)]
96
97pub mod error;
99
100pub mod acs;
102pub mod security;
103
104pub use security::security_and_compliance::{
106 SecurityAndComplianceV1Service, SecurityAndComplianceV2Service,
107};
108
109use acs::acs::AcsProject;
111use security::security_and_compliance::SecurityAndComplianceProject;
112
113pub use crate::error::SecurityError;
115
116pub use openlark_core::config::Config;
118
119#[derive(Debug, Clone)]
128pub struct SecurityClient {
129 config: openlark_core::config::Config,
130 pub acs: AcsProject,
132 pub security_and_compliance: SecurityAndComplianceProject,
134}
135
136impl SecurityClient {
137 pub fn new(config: openlark_core::config::Config) -> Self {
141 Self {
142 acs: AcsProject::new(config.clone()),
143 security_and_compliance: SecurityAndComplianceProject::new(config.clone()),
144 config,
145 }
146 }
147
148 pub fn config(&self) -> &openlark_core::config::Config {
150 &self.config
151 }
152}
153
154pub type SecurityResult<T> = Result<T, crate::error::SecurityError>;
159
160pub mod prelude {
162 pub use super::{SecurityClient, SecurityResult};
163
164 pub use super::acs::acs::AcsV1Service;
166 pub use super::security::security_and_compliance::{
167 SecurityAndComplianceV1Service, SecurityAndComplianceV2Service,
168 };
169}
170
171#[cfg(test)]
172mod construction_tests {
173 use super::*;
174 use openlark_core::auth::{TokenProvider, TokenRequest};
175 use openlark_core::error::ErrorTrait;
176 use std::future::Future;
177 use std::pin::Pin;
178 use wiremock::matchers::{header, method, path};
179 use wiremock::{Mock, MockServer, ResponseTemplate};
180
181 #[derive(Debug, Clone)]
183 struct TestTokenProvider(&'static str);
184
185 impl TokenProvider for TestTokenProvider {
186 fn get_token(
187 &self,
188 _request: TokenRequest,
189 ) -> Pin<Box<dyn Future<Output = openlark_core::SDKResult<String>> + Send + '_>> {
190 let token = self.0.to_string();
191 Box::pin(async move { Ok(token) })
192 }
193 }
194
195 #[tokio::test]
200 async fn security_client_new_canonical_config_propagates_base_headers_and_token_provider() {
201 let server = MockServer::start().await;
202
203 Mock::given(method("GET"))
205 .and(path("/open-apis/acs/v1/users"))
206 .and(header("Authorization", "Bearer test_tok_from_provider"))
207 .and(header("X-Custom-Prop", "yes"))
208 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
209 "code": 0,
210 "msg": "success",
211 "data": { "has_more": false, "items": [] }
212 })))
213 .mount(&server)
214 .await;
215
216 let base = Config::builder()
217 .app_id("test_app")
218 .app_secret("test_secret")
219 .base_url(server.uri())
220 .allow_custom_base_url(true)
221 .add_header("X-Custom-Prop", "yes")
222 .req_timeout(std::time::Duration::from_secs(30))
223 .max_response_size(8 * 1024 * 1024)
224 .build();
225
226 let config_with_provider =
227 base.with_token_provider(TestTokenProvider("test_tok_from_provider"));
228
229 let client = SecurityClient::new(config_with_provider);
230
231 let _resp = client
236 .acs
237 .v1()
238 .users()
239 .list()
240 .execute()
241 .await
242 .expect("wiremock 应返回成功响应");
243
244 let received = server.received_requests().await.unwrap_or_default();
245 assert_eq!(received.len(), 1, "应只发一次 security leaf 请求");
246
247 let req = &received[0];
250 assert!(
251 req.url.path() == "/open-apis/acs/v1/users"
252 || req.url.as_str().contains("/acs/v1/users"),
253 "请求路径应指向 ACS leaf,实际: {}",
254 req.url
255 );
256 }
257
258 #[tokio::test]
266 async fn security_client_new_canonical_config_propagates_to_compliance_v2_leaf() {
267 let server = MockServer::start().await;
268
269 Mock::given(method("GET"))
271 .and(path("/open-apis/security_and_compliance/v2/device_records"))
272 .and(header("Authorization", "Bearer test_tok_from_provider"))
273 .and(header("X-Compliance-Test", "propagated"))
274 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
275 "code": 0,
276 "msg": "success",
277 "data": { "has_more": false, "items": [] }
278 })))
279 .mount(&server)
280 .await;
281
282 let base = Config::builder()
283 .app_id("test_app")
284 .app_secret("test_secret")
285 .base_url(server.uri())
286 .allow_custom_base_url(true)
287 .add_header("X-Compliance-Test", "propagated")
288 .req_timeout(std::time::Duration::from_secs(30))
289 .max_response_size(8 * 1024 * 1024)
290 .build();
291
292 let config_with_provider =
293 base.with_token_provider(TestTokenProvider("test_tok_from_provider"));
294
295 let client = SecurityClient::new(config_with_provider);
296
297 let _ = client
300 .security_and_compliance
301 .v2()
302 .device_records()
303 .list()
304 .execute()
305 .await
306 .expect("compliance v2 leaf 应成功");
307
308 let received = server.received_requests().await.unwrap_or_default();
309 assert_eq!(received.len(), 1, "应只发一次 compliance leaf 请求");
310 assert!(
311 received[0].url.path().contains("/device_records"),
312 "请求路径应指向 compliance v2 leaf"
313 );
314 }
315
316 #[tokio::test]
318 async fn security_client_new_canonical_config_propagates_to_compliance_v1_leaf() {
319 let server = MockServer::start().await;
320
321 Mock::given(method("POST"))
322 .and(path(
323 "/open-apis/security_and_compliance/v1/openapi_logs/list_data",
324 ))
325 .and(header("Authorization", "Bearer test_tok_from_provider"))
326 .and(header("X-Compliance-V1", "propagated"))
327 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
328 "code": 0,
329 "msg": "success",
330 "data": { "items": [{ "request_id": "r1" }], "has_more": false }
331 })))
332 .mount(&server)
333 .await;
334
335 let base = Config::builder()
336 .app_id("test_app")
337 .app_secret("test_secret")
338 .base_url(server.uri())
339 .allow_custom_base_url(true)
340 .add_header("X-Compliance-V1", "propagated")
341 .build();
342
343 let config_with_provider =
344 base.with_token_provider(TestTokenProvider("test_tok_from_provider"));
345
346 let client = SecurityClient::new(config_with_provider);
347
348 use serde_json::json;
352 let _ = client
353 .security_and_compliance
354 .v1()
355 .openapi_logs()
356 .list_data()
357 .body(json!({ "start_time": 1700000000, "end_time": 1700003600 }))
358 .execute()
359 .await
360 .expect("compliance v1 leaf 应成功");
361
362 let received = server.received_requests().await.unwrap_or_default();
363 assert_eq!(received.len(), 1);
364 assert!(received[0].url.path().contains("/openapi_logs/list_data"));
365 }
366
367 #[tokio::test]
369 async fn compliance_v1_preserves_retryable_error_and_request_id() {
370 let server = MockServer::start().await;
371 Mock::given(method("POST"))
372 .and(path(
373 "/open-apis/security_and_compliance/v1/openapi_logs/list_data",
374 ))
375 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
376 "code": 503,
377 "msg": "compliance v1 unavailable",
378 "request_id": "req-compliance-v1-503"
379 })))
380 .mount(&server)
381 .await;
382
383 let config = Config::builder()
384 .app_id("test_app")
385 .app_secret("test_secret")
386 .base_url(server.uri())
387 .allow_custom_base_url(true)
388 .build()
389 .with_token_provider(TestTokenProvider("compliance_v1_token"));
390 let client = SecurityClient::new(config);
391
392 let err = client
393 .security_and_compliance
394 .v1()
395 .openapi_logs()
396 .list_data()
397 .body(serde_json::json!({}))
398 .execute()
399 .await
400 .expect_err("compliance v1 业务错误必须向上传播");
401
402 assert!(err.is_retryable());
403 assert_eq!(err.context().request_id(), Some("req-compliance-v1-503"));
404 }
405
406 #[tokio::test]
409 async fn compliance_v2_preserves_retryable_error_and_request_id() {
410 let server = MockServer::start().await;
411 Mock::given(method("GET"))
412 .and(path("/open-apis/security_and_compliance/v2/device_records"))
413 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
414 "code": 429,
415 "msg": "compliance v2 rate limited",
416 "request_id": "req-compliance-v2-429"
417 })))
418 .mount(&server)
419 .await;
420
421 let config = Config::builder()
422 .app_id("test_app")
423 .app_secret("test_secret")
424 .base_url(server.uri())
425 .allow_custom_base_url(true)
426 .build()
427 .with_token_provider(TestTokenProvider("compliance_v2_token"));
428 let client = SecurityClient::new(config);
429
430 let err = client
431 .security_and_compliance
432 .v2()
433 .device_records()
434 .list()
435 .execute()
436 .await
437 .expect_err("compliance v2 业务错误必须向上传播");
438
439 assert!(err.is_retryable());
440 assert_eq!(err.context().request_id(), Some("req-compliance-v2-429"));
441 }
442
443 #[tokio::test]
446 async fn security_client_timeout_propagates_and_triggers_timeout_error() {
447 let server = MockServer::start().await;
448
449 Mock::given(method("GET"))
451 .and(path("/open-apis/acs/v1/users"))
452 .respond_with(
453 ResponseTemplate::new(200)
454 .set_body_json(serde_json::json!({"code":0,"msg":"ok","data":{"has_more":false,"items":[]}}))
455 .set_delay(std::time::Duration::from_millis(800)),
456 )
457 .mount(&server)
458 .await;
459
460 let base = Config::builder()
461 .app_id("test_app")
462 .app_secret("test_secret")
463 .base_url(server.uri())
464 .allow_custom_base_url(true)
465 .req_timeout(std::time::Duration::from_millis(50)) .build();
467
468 let config = base.with_token_provider(TestTokenProvider("test_tok_for_timeout_test"));
469
470 let client = SecurityClient::new(config);
471
472 let result = client.acs.v1().users().list().execute().await;
473
474 assert!(result.is_err(), "应因 timeout 配置触发错误");
476 let err = result.unwrap_err().to_string().to_lowercase();
477 assert!(
479 err.contains("timeout")
480 || err.contains("time")
481 || err.contains("deadline")
482 || err.contains("network")
483 || err.contains("send"),
484 "错误应体现超时或网络失败,实际: {}",
485 err
486 );
487 }
488
489 #[tokio::test]
491 async fn security_client_max_response_size_propagates_and_triggers_size_error() {
492 let server = MockServer::start().await;
493
494 let big_body = serde_json::json!({
496 "code": 0,
497 "msg": "ok",
498 "data": { "has_more": false, "items": [ {"x": "y".repeat(1024)} ] }
499 });
500 let big_json = serde_json::to_vec(&big_body).unwrap();
501
502 Mock::given(method("GET"))
503 .and(path("/open-apis/acs/v1/users"))
504 .respond_with(ResponseTemplate::new(200).set_body_raw(big_json, "application/json"))
505 .mount(&server)
506 .await;
507
508 let base = Config::builder()
509 .app_id("test_app")
510 .app_secret("test_secret")
511 .base_url(server.uri())
512 .allow_custom_base_url(true)
513 .max_response_size(512) .build();
515
516 let config = base.with_token_provider(TestTokenProvider("test_tok_for_size_test"));
517
518 let client = SecurityClient::new(config);
519
520 let result = client.acs.v1().users().list().execute().await;
521
522 assert!(result.is_err(), "应因 response size 超限触发错误");
523 let err = result.unwrap_err().to_string();
524 assert!(
526 err.contains("响应体过大")
527 || err.to_lowercase().contains("large")
528 || err.to_lowercase().contains("size")
529 || err.contains("超过限制"),
530 "错误应体现响应过大,实际: {}",
531 err
532 );
533 }
534}