1use std::collections::{HashMap, HashSet};
10use std::sync::Arc;
11use std::time::{SystemTime, UNIX_EPOCH};
12
13use axum::extract::{Path, State};
14use axum::http::StatusCode;
15use axum::response::IntoResponse;
16use axum::{
17 Extension, Json, Router,
18 routing::{delete, get, post},
19};
20use scp_core::bridge::shadow::{CreateShadowParams, ShadowRegistry, create_shadow};
21use scp_core::bridge::{BridgeMode, ShadowProvenanceStatus};
22use scp_core::crypto::sender_keys::SenderKeyStore;
23use serde::{Deserialize, Serialize};
24use tokio::sync::RwLock;
25
26use crate::error::ApiError;
27
28#[derive(Debug, Clone, Serialize)]
34pub struct StoredAttestation {
35 pub attestation_id: String,
36 pub status: String,
37 pub bridge_id: String,
38 pub platform_handle: String,
39 pub platform_user_id: String,
40 pub evidence: AttestationEvidence,
41 pub issued_at: u64,
42 pub expires_at: u64,
43}
44
45#[derive(Debug)]
52pub struct BridgeState {
53 pub registries: RwLock<HashMap<String, ShadowRegistry>>,
55
56 pub sender_key_store: RwLock<SenderKeyStore>,
58
59 pub attestations: RwLock<HashMap<String, StoredAttestation>>,
61
62 pub deleted_shadows: RwLock<HashSet<String>>,
64
65 pub processed_event_ids: RwLock<HashSet<String>>,
67
68 pub messages: RwLock<Vec<EmittedMessage>>,
70
71 pub message_sequence: RwLock<u64>,
73}
74
75#[derive(Debug, Clone, Serialize)]
77pub struct EmittedMessage {
78 pub message_id: String,
80 pub shadow_id: String,
82 pub content: String,
84 pub content_type: String,
86 pub sequence: u64,
88 pub bridge_provenance: BridgeProvenanceResponse,
90}
91
92impl BridgeState {
93 #[must_use]
95 pub fn new() -> Self {
96 Self {
97 registries: RwLock::new(HashMap::new()),
98 sender_key_store: RwLock::new(SenderKeyStore::new()),
99 attestations: RwLock::new(HashMap::new()),
100 deleted_shadows: RwLock::new(HashSet::new()),
101 processed_event_ids: RwLock::new(HashSet::new()),
102 messages: RwLock::new(Vec::new()),
103 message_sequence: RwLock::new(0),
104 }
105 }
106}
107
108impl Default for BridgeState {
109 fn default() -> Self {
110 Self::new()
111 }
112}
113
114#[derive(Debug, Deserialize)]
120pub struct CreateShadowRequest {
121 pub platform_handle: String,
123
124 pub platform_user_id: String,
128
129 #[serde(default)]
131 pub metadata: Option<serde_json::Value>,
132}
133
134#[derive(Debug, Serialize)]
136pub struct CreateShadowResponse {
137 pub shadow_id: String,
139
140 pub platform_handle: String,
142
143 pub platform_user_id: String,
145
146 pub attributed_role: String,
148
149 pub created_at: u64,
151}
152
153#[derive(Debug, Deserialize)]
155pub struct AttestRequest {
156 pub platform_handle: String,
158
159 pub platform_user_id: String,
161
162 pub attestation_evidence: AttestationEvidence,
164}
165
166#[derive(Debug, Clone, Deserialize, Serialize)]
168pub struct AttestationEvidence {
169 pub evidence_type: String,
171
172 pub verification_method: String,
174
175 pub verified_at: u64,
177
178 pub platform_confidence: String,
180
181 #[serde(default)]
183 pub additional_signals: Option<serde_json::Value>,
184}
185
186#[derive(Debug, Serialize)]
188pub struct AttestResponse {
189 pub attestation_id: String,
191
192 pub status: String,
194
195 pub platform_handle: String,
197
198 pub issued_at: u64,
200
201 pub expires_at: u64,
203}
204
205const ATTESTATION_TTL_SECS: u64 = 86_400;
207
208#[derive(Debug, Deserialize)]
214pub struct EmitMessageRequest {
215 pub shadow_id: String,
217
218 pub content: String,
220
221 pub content_type: String,
223
224 #[serde(default)]
226 pub platform_message_id: Option<String>,
227
228 #[serde(default)]
230 pub platform_timestamp: Option<u64>,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct BridgeProvenanceResponse {
236 pub originating_platform: String,
238 pub bridge_mode: String,
240 pub shadow_status: String,
242 pub operator_did: String,
244}
245
246#[derive(Debug, Serialize)]
248pub struct EmitMessageResponse {
249 pub message_id: String,
251 pub sequence: u64,
253 pub bridge_provenance: BridgeProvenanceResponse,
255}
256
257#[derive(Debug, Serialize)]
263pub struct BridgeStatusResponse {
264 pub bridge_id: String,
266 pub status: String,
268 pub platform: String,
270 pub mode: String,
272 pub operator_did: String,
274 pub registered_at: u64,
276 pub shadow_count: usize,
278 pub shadows: Vec<ShadowSummary>,
280}
281
282#[derive(Debug, Serialize)]
284pub struct ShadowSummary {
285 pub shadow_id: String,
287 pub platform_handle: String,
289 pub attributed_role: String,
291 pub provenance_status: String,
293 pub created_at: u64,
295}
296
297#[derive(Debug, Deserialize)]
303pub struct WebhookRequest {
304 pub event_type: String,
306 pub event_id: String,
308 pub timestamp: u64,
310 pub payload: serde_json::Value,
312}
313
314#[derive(Debug, Serialize)]
316pub struct WebhookResponse {
317 pub accepted: bool,
319 pub event_id: String,
321 #[serde(skip_serializing_if = "Option::is_none")]
323 pub reason: Option<String>,
324}
325
326const VALID_EVENT_TYPES: &[&str] = &[
328 "message",
329 "presence",
330 "identity_update",
331 "user_departed",
332 "message_edit",
333 "message_delete",
334];
335
336fn derive_shadow_id(bridge_id: &str, platform_user_id: &str) -> String {
344 format!("shadow:{bridge_id}:{platform_user_id}")
345}
346
347#[allow(clippy::significant_drop_tightening)] async fn create_shadow_handler(
360 State(bridge_state): State<Arc<BridgeState>>,
361 Extension(auth_ctx): Extension<crate::bridge_auth::BridgeAuthContext>,
362 Json(body): Json<CreateShadowRequest>,
363) -> impl IntoResponse {
364 if body.platform_handle.is_empty() {
366 return ApiError::bad_request("platform_handle must not be empty").into_response();
367 }
368 if body.platform_user_id.is_empty() {
369 return ApiError::bad_request("platform_user_id must not be empty").into_response();
370 }
371
372 let bridge_id = &auth_ctx.claims.scp_bridge_id;
373 let context_id = &auth_ctx.claims.scp_context_id;
374 let shadow_id = derive_shadow_id(bridge_id, &body.platform_user_id);
375
376 let mut registries = bridge_state.registries.write().await;
377
378 let registry = registries
380 .entry(context_id.clone())
381 .or_insert_with(|| ShadowRegistry::new(context_id.clone()));
382
383 if let Some(existing) = registry.shadows().iter().find(|s| s.shadow_id == shadow_id) {
385 return (
386 StatusCode::OK,
387 Json(CreateShadowResponse {
388 shadow_id: existing.shadow_id.clone(),
389 platform_handle: existing.platform_handle.clone(),
390 platform_user_id: body.platform_user_id,
391 attributed_role: existing.attributed_role.clone(),
392 created_at: existing.created_at,
393 }),
394 )
395 .into_response();
396 }
397
398 let now = SystemTime::now()
399 .duration_since(UNIX_EPOCH)
400 .map(|d| d.as_secs())
401 .unwrap_or(0);
402
403 let bridge_mode = auth_ctx.bridge.mode.clone();
404
405 let params = CreateShadowParams {
406 shadow_id: &shadow_id,
407 bridge_id,
408 bridge_mode,
409 platform_handle: &body.platform_handle,
410 context_member_dids: &[],
411 timestamp: now,
412 };
413
414 let mut sender_key_store = bridge_state.sender_key_store.write().await;
415
416 match create_shadow(registry, &mut sender_key_store, ¶ms) {
417 Ok((shadow, _event)) => (
418 StatusCode::CREATED,
419 Json(CreateShadowResponse {
420 shadow_id: shadow.shadow_id,
421 platform_handle: shadow.platform_handle,
422 platform_user_id: body.platform_user_id,
423 attributed_role: shadow.attributed_role,
424 created_at: shadow.created_at,
425 }),
426 )
427 .into_response(),
428 Err(e) => ApiError::internal_error(e.to_string()).into_response(),
429 }
430}
431
432fn is_valid_confidence(value: &str) -> bool {
434 matches!(value, "high" | "medium" | "low")
435}
436
437#[allow(clippy::significant_drop_tightening)] async fn attest_handler(
448 State(bridge_state): State<Arc<BridgeState>>,
449 Extension(auth_ctx): Extension<crate::bridge_auth::BridgeAuthContext>,
450 Json(body): Json<AttestRequest>,
451) -> impl IntoResponse {
452 if body.platform_handle.is_empty() {
453 return ApiError::bad_request("platform_handle must not be empty").into_response();
454 }
455 if body.platform_user_id.is_empty() {
456 return ApiError::bad_request("platform_user_id must not be empty").into_response();
457 }
458 if body.attestation_evidence.evidence_type.is_empty() {
459 return ApiError::bad_request("attestation_evidence.evidence_type must not be empty")
460 .into_response();
461 }
462 if body.attestation_evidence.verification_method.is_empty() {
463 return ApiError::bad_request("attestation_evidence.verification_method must not be empty")
464 .into_response();
465 }
466 if !is_valid_confidence(&body.attestation_evidence.platform_confidence) {
467 return ApiError::bad_request(
468 "attestation_evidence.platform_confidence must be \"high\", \"medium\", or \"low\"",
469 )
470 .into_response();
471 }
472
473 let bridge_id = &auth_ctx.claims.scp_bridge_id;
474 let attestation_id = format!("attest:{bridge_id}:{}", body.platform_user_id);
475
476 let now = SystemTime::now()
477 .duration_since(UNIX_EPOCH)
478 .map(|d| d.as_secs())
479 .unwrap_or(0);
480
481 let stored = StoredAttestation {
482 attestation_id: attestation_id.clone(),
483 status: "active".to_owned(),
484 bridge_id: bridge_id.clone(),
485 platform_handle: body.platform_handle.clone(),
486 platform_user_id: body.platform_user_id,
487 evidence: body.attestation_evidence,
488 issued_at: now,
489 expires_at: now + ATTESTATION_TTL_SECS,
490 };
491
492 let response = AttestResponse {
493 attestation_id: stored.attestation_id.clone(),
494 status: stored.status.clone(),
495 platform_handle: stored.platform_handle.clone(),
496 issued_at: stored.issued_at,
497 expires_at: stored.expires_at,
498 };
499
500 let mut attestations = bridge_state.attestations.write().await;
501 attestations.insert(attestation_id, stored);
502
503 (StatusCode::CREATED, Json(response)).into_response()
504}
505
506fn find_shadow(
512 registries: &HashMap<String, ShadowRegistry>,
513 shadow_id: &str,
514) -> Option<(String, scp_core::bridge::ShadowIdentity)> {
515 for (ctx_id, registry) in registries {
516 if let Some(shadow) = registry.shadows().iter().find(|s| s.shadow_id == shadow_id) {
517 return Some((ctx_id.clone(), shadow.clone()));
518 }
519 }
520 None
521}
522
523async fn emit_message_handler(
531 State(bridge_state): State<Arc<BridgeState>>,
532 Extension(auth_ctx): Extension<crate::bridge_auth::BridgeAuthContext>,
533 Json(body): Json<EmitMessageRequest>,
534) -> impl IntoResponse {
535 if body.shadow_id.is_empty() {
536 return ApiError::bad_request("shadow_id must not be empty").into_response();
537 }
538 if body.content.is_empty() {
539 return ApiError::bad_request("content must not be empty").into_response();
540 }
541 if body.content_type.is_empty() {
542 return ApiError::bad_request("content_type must not be empty").into_response();
543 }
544
545 let registries = bridge_state.registries.read().await;
546 let deleted = bridge_state.deleted_shadows.read().await;
547
548 if deleted.contains(&body.shadow_id) {
550 return ApiError::not_found("SHADOW_NOT_FOUND: shadow has been deleted").into_response();
551 }
552
553 let shadow_info = find_shadow(®istries, &body.shadow_id);
554 drop(registries);
555 drop(deleted);
556
557 let Some((_ctx_id, shadow)) = shadow_info else {
558 return (
559 StatusCode::NOT_FOUND,
560 Json(ApiError {
561 error: "shadow not found".to_owned(),
562 code: "SHADOW_NOT_FOUND".to_owned(),
563 }),
564 )
565 .into_response();
566 };
567
568 let shadow_status = match shadow.provenance_status {
569 ShadowProvenanceStatus::Shadow => "Shadow",
570 ShadowProvenanceStatus::Claimed => "Claimed",
571 };
572
573 let bridge_mode_str = match auth_ctx.bridge.mode {
574 BridgeMode::Relay => "Relay",
575 BridgeMode::Puppet => "Puppet",
576 BridgeMode::Api => "Api",
577 BridgeMode::Cooperative => "Cooperative",
578 };
579
580 let provenance_resp = BridgeProvenanceResponse {
581 originating_platform: auth_ctx.bridge.platform.clone(),
582 bridge_mode: bridge_mode_str.to_owned(),
583 shadow_status: shadow_status.to_owned(),
584 operator_did: auth_ctx.bridge.operator_did.0.clone(),
585 };
586
587 let mut seq = bridge_state.message_sequence.write().await;
588 *seq += 1;
589 let sequence = *seq;
590 drop(seq);
591
592 let message_id = format!("msg:{}:{sequence}", auth_ctx.claims.scp_bridge_id);
593
594 let emitted = EmittedMessage {
595 message_id: message_id.clone(),
596 shadow_id: body.shadow_id,
597 content: body.content,
598 content_type: body.content_type,
599 sequence,
600 bridge_provenance: provenance_resp.clone(),
601 };
602
603 bridge_state.messages.write().await.push(emitted);
604
605 (
606 StatusCode::ACCEPTED,
607 Json(EmitMessageResponse {
608 message_id,
609 sequence,
610 bridge_provenance: provenance_resp,
611 }),
612 )
613 .into_response()
614}
615
616#[allow(clippy::significant_drop_tightening)] async fn status_handler(
628 State(bridge_state): State<Arc<BridgeState>>,
629 Extension(auth_ctx): Extension<crate::bridge_auth::BridgeAuthContext>,
630) -> impl IntoResponse {
631 let registries = bridge_state.registries.read().await;
632 let deleted = bridge_state.deleted_shadows.read().await;
633
634 let mut shadows = Vec::new();
635 for registry in registries.values() {
636 for shadow in registry.shadows() {
637 if !deleted.contains(&shadow.shadow_id) {
638 let status_str = match shadow.provenance_status {
639 ShadowProvenanceStatus::Shadow => "Shadow",
640 ShadowProvenanceStatus::Claimed => "Claimed",
641 };
642 shadows.push(ShadowSummary {
643 shadow_id: shadow.shadow_id.clone(),
644 platform_handle: shadow.platform_handle.clone(),
645 attributed_role: shadow.attributed_role.clone(),
646 provenance_status: status_str.to_owned(),
647 created_at: shadow.created_at,
648 });
649 }
650 }
651 }
652
653 let bridge_mode_str = match auth_ctx.bridge.mode {
654 BridgeMode::Relay => "Relay",
655 BridgeMode::Puppet => "Puppet",
656 BridgeMode::Api => "Api",
657 BridgeMode::Cooperative => "Cooperative",
658 };
659
660 let status_str = match auth_ctx.bridge.status {
661 scp_core::bridge::BridgeStatus::Active => "Active",
662 scp_core::bridge::BridgeStatus::Suspended => "Suspended",
663 scp_core::bridge::BridgeStatus::Revoked => "Revoked",
664 };
665
666 let shadow_count = shadows.len();
667
668 let resp = BridgeStatusResponse {
669 bridge_id: auth_ctx.bridge.bridge_id.clone(),
670 status: status_str.to_owned(),
671 platform: auth_ctx.bridge.platform.clone(),
672 mode: bridge_mode_str.to_owned(),
673 operator_did: auth_ctx.bridge.operator_did.0.clone(),
674 registered_at: auth_ctx.bridge.registered_at,
675 shadow_count,
676 shadows,
677 };
678
679 (StatusCode::OK, Json(resp)).into_response()
680}
681
682async fn delete_shadow_handler(
694 State(bridge_state): State<Arc<BridgeState>>,
695 Extension(_auth_ctx): Extension<crate::bridge_auth::BridgeAuthContext>,
696 Path(shadow_id): Path<String>,
697) -> impl IntoResponse {
698 let deleted = bridge_state.deleted_shadows.read().await;
699
700 if deleted.contains(&shadow_id) {
702 return StatusCode::NO_CONTENT.into_response();
703 }
704 drop(deleted);
705
706 let registries = bridge_state.registries.read().await;
707 let shadow_info = find_shadow(®istries, &shadow_id);
708 drop(registries);
709
710 match shadow_info {
711 None => (
712 StatusCode::NOT_FOUND,
713 Json(ApiError {
714 error: "shadow not found".to_owned(),
715 code: "SHADOW_NOT_FOUND".to_owned(),
716 }),
717 )
718 .into_response(),
719 Some((_ctx_id, shadow)) => {
720 if shadow.provenance_status == ShadowProvenanceStatus::Claimed {
722 return (
723 StatusCode::CONFLICT,
724 Json(ApiError {
725 error: "shadow has been claimed and cannot be deleted".to_owned(),
726 code: "SHADOW_ALREADY_CLAIMED".to_owned(),
727 }),
728 )
729 .into_response();
730 }
731
732 bridge_state.deleted_shadows.write().await.insert(shadow_id);
733
734 StatusCode::NO_CONTENT.into_response()
735 }
736 }
737}
738
739fn extract_shadow_id(payload: &serde_json::Value) -> &str {
745 payload
746 .get("shadow_id")
747 .and_then(|v| v.as_str())
748 .unwrap_or("")
749}
750
751fn webhook_reject(event_id: String, reason: &str) -> axum::response::Response {
753 (
754 StatusCode::OK,
755 Json(WebhookResponse {
756 accepted: false,
757 event_id,
758 reason: Some(reason.to_owned()),
759 }),
760 )
761 .into_response()
762}
763
764async fn process_webhook_event(
767 bridge_state: &BridgeState,
768 event_type: &str,
769 event_id: &str,
770 payload: &serde_json::Value,
771) -> Option<String> {
772 match event_type {
773 "message" => {
774 let shadow_id = extract_shadow_id(payload);
775 if shadow_id.is_empty() {
776 return Some("payload.shadow_id is required for message events".to_owned());
777 }
778 let registries = bridge_state.registries.read().await;
779 let exists = find_shadow(®istries, shadow_id).is_some();
780 drop(registries);
781 if !exists {
782 return Some("shadow not found".to_owned());
783 }
784 }
785 "identity_update" => {
786 let shadow_id = extract_shadow_id(payload);
787 if !shadow_id.is_empty() {
788 let registries = bridge_state.registries.read().await;
789 let exists = find_shadow(®istries, shadow_id).is_some();
790 drop(registries);
791 if !exists {
792 return Some("shadow not found for identity_update".to_owned());
793 }
794 }
795 }
796 "user_departed" => {
797 let shadow_id = extract_shadow_id(payload);
798 if !shadow_id.is_empty() {
799 bridge_state
800 .deleted_shadows
801 .write()
802 .await
803 .insert(shadow_id.to_owned());
804 }
805 }
806 _ => {}
809 }
810 let _ = event_id; None
812}
813
814async fn webhook_handler(
822 State(bridge_state): State<Arc<BridgeState>>,
823 Extension(_auth_ctx): Extension<crate::bridge_auth::BridgeAuthContext>,
824 Json(body): Json<WebhookRequest>,
825) -> impl IntoResponse {
826 if !VALID_EVENT_TYPES.contains(&body.event_type.as_str()) {
827 return webhook_reject(
828 body.event_id,
829 &format!("unknown event_type: {}", body.event_type),
830 );
831 }
832 if body.event_id.is_empty() {
833 return ApiError::bad_request("event_id must not be empty").into_response();
834 }
835
836 {
838 let processed = bridge_state.processed_event_ids.read().await;
839 if processed.contains(&body.event_id) {
840 return (
841 StatusCode::OK,
842 Json(WebhookResponse {
843 accepted: true,
844 event_id: body.event_id,
845 reason: None,
846 }),
847 )
848 .into_response();
849 }
850 }
851
852 if let Some(reason) = process_webhook_event(
853 &bridge_state,
854 &body.event_type,
855 &body.event_id,
856 &body.payload,
857 )
858 .await
859 {
860 return webhook_reject(body.event_id, &reason);
861 }
862
863 bridge_state
864 .processed_event_ids
865 .write()
866 .await
867 .insert(body.event_id.clone());
868
869 (
870 StatusCode::OK,
871 Json(WebhookResponse {
872 accepted: true,
873 event_id: body.event_id,
874 reason: None,
875 }),
876 )
877 .into_response()
878}
879
880pub fn bridge_router(state: Arc<BridgeState>) -> Router {
890 Router::new()
891 .route("/v1/scp/bridge/shadow", post(create_shadow_handler))
892 .route(
893 "/v1/scp/bridge/shadow/{shadow_id}",
894 delete(delete_shadow_handler),
895 )
896 .route("/v1/scp/bridge/attest", post(attest_handler))
897 .route("/v1/scp/bridge/message", post(emit_message_handler))
898 .route("/v1/scp/bridge/status", get(status_handler))
899 .route("/v1/scp/bridge/webhook", post(webhook_handler))
900 .with_state(state)
901}
902
903#[cfg(test)]
908#[allow(
909 clippy::unwrap_used,
910 clippy::expect_used,
911 clippy::panic,
912 clippy::needless_pass_by_value,
913 clippy::significant_drop_tightening
914)]
915mod tests {
916 use super::*;
917
918 use axum::body::Body;
919 use axum::http::Request;
920 use http_body_util::BodyExt;
921 use scp_core::bridge::{BridgeConnector, BridgeMode, BridgeStatus};
922 use tower::ServiceExt;
923
924 use crate::bridge_auth::{BridgeAuthContext, BridgeJwtClaims};
925
926 fn test_claims() -> BridgeJwtClaims {
927 BridgeJwtClaims {
928 iss: "did:dht:z6MkTestOperator".to_owned(),
929 aud: "https://node.example.com".to_owned(),
930 iat: 1_700_000_000,
931 exp: 1_700_003_600,
932 scp_bridge_id: "bridge-test-001".to_owned(),
933 scp_context_id: "ctx-test-001".to_owned(),
934 }
935 }
936
937 fn test_auth_ctx() -> BridgeAuthContext {
938 BridgeAuthContext {
939 claims: test_claims(),
940 bridge: BridgeConnector {
941 bridge_id: "bridge-test-001".to_owned(),
942 operator_did: scp_identity::DID("did:dht:z6MkTestOperator".to_owned()),
943 platform: "discord".to_owned(),
944 mode: BridgeMode::Relay,
945 status: BridgeStatus::Active,
946 registration_context: "ctx-test-001".to_owned(),
947 registered_at: 1_700_000_000,
948 },
949 }
950 }
951
952 fn test_app(state: Arc<BridgeState>) -> Router {
955 let auth_ctx = test_auth_ctx();
956 Router::new()
957 .route("/v1/scp/bridge/shadow", post(create_shadow_handler))
958 .route(
959 "/v1/scp/bridge/shadow/{shadow_id}",
960 delete(delete_shadow_handler),
961 )
962 .route("/v1/scp/bridge/attest", post(attest_handler))
963 .route("/v1/scp/bridge/message", post(emit_message_handler))
964 .route("/v1/scp/bridge/status", get(status_handler))
965 .route("/v1/scp/bridge/webhook", post(webhook_handler))
966 .layer(axum::Extension(auth_ctx))
967 .with_state(state)
968 }
969
970 fn create_request(body: serde_json::Value) -> Request<Body> {
971 Request::builder()
972 .method("POST")
973 .uri("/v1/scp/bridge/shadow")
974 .header("content-type", "application/json")
975 .body(Body::from(serde_json::to_vec(&body).expect("test")))
976 .expect("test")
977 }
978
979 fn attest_request(body: serde_json::Value) -> Request<Body> {
980 Request::builder()
981 .method("POST")
982 .uri("/v1/scp/bridge/attest")
983 .header("content-type", "application/json")
984 .body(Body::from(serde_json::to_vec(&body).expect("test")))
985 .expect("test")
986 }
987
988 async fn response_json(resp: axum::response::Response) -> serde_json::Value {
989 let bytes = resp.into_body().collect().await.expect("test").to_bytes();
990 serde_json::from_slice(&bytes).expect("test")
991 }
992
993 #[tokio::test]
994 async fn successful_creation_returns_201() {
995 let state = Arc::new(BridgeState::new());
996 let app = test_app(state);
997
998 let req = create_request(serde_json::json!({
999 "platform_handle": "@alice#1234",
1000 "platform_user_id": "user-alice-001"
1001 }));
1002
1003 let resp = app.oneshot(req).await.expect("test");
1004 assert_eq!(resp.status(), StatusCode::CREATED);
1005
1006 let json = response_json(resp).await;
1007 assert_eq!(json["platform_handle"], "@alice#1234");
1008 assert_eq!(json["platform_user_id"], "user-alice-001");
1009 assert_eq!(json["attributed_role"], "observer");
1010 assert_eq!(json["shadow_id"], "shadow:bridge-test-001:user-alice-001");
1011 assert!(json["created_at"].as_u64().is_some());
1012 }
1013
1014 #[tokio::test]
1015 async fn idempotent_creation_returns_200() {
1016 let state = Arc::new(BridgeState::new());
1017
1018 let app = test_app(Arc::clone(&state));
1020 let req = create_request(serde_json::json!({
1021 "platform_handle": "@alice#1234",
1022 "platform_user_id": "user-alice-001"
1023 }));
1024 let resp = app.oneshot(req).await.expect("test");
1025 assert_eq!(resp.status(), StatusCode::CREATED);
1026
1027 let app = test_app(state);
1029 let req = create_request(serde_json::json!({
1030 "platform_handle": "@alice#1234",
1031 "platform_user_id": "user-alice-001"
1032 }));
1033 let resp = app.oneshot(req).await.expect("test");
1034 assert_eq!(resp.status(), StatusCode::OK);
1035
1036 let json = response_json(resp).await;
1037 assert_eq!(json["shadow_id"], "shadow:bridge-test-001:user-alice-001");
1038 assert_eq!(json["attributed_role"], "observer");
1039 }
1040
1041 #[tokio::test]
1042 async fn missing_platform_handle_returns_400() {
1043 let state = Arc::new(BridgeState::new());
1044 let app = test_app(state);
1045
1046 let req = create_request(serde_json::json!({
1047 "platform_handle": "",
1048 "platform_user_id": "user-001"
1049 }));
1050
1051 let resp = app.oneshot(req).await.expect("test");
1052 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
1053 }
1054
1055 #[tokio::test]
1056 async fn missing_platform_user_id_returns_400() {
1057 let state = Arc::new(BridgeState::new());
1058 let app = test_app(state);
1059
1060 let req = create_request(serde_json::json!({
1061 "platform_handle": "@alice",
1062 "platform_user_id": ""
1063 }));
1064
1065 let resp = app.oneshot(req).await.expect("test");
1066 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
1067 }
1068
1069 #[tokio::test]
1070 async fn missing_required_fields_returns_400() {
1071 let state = Arc::new(BridgeState::new());
1072 let app = test_app(state);
1073
1074 let req = create_request(serde_json::json!({}));
1076
1077 let resp = app.oneshot(req).await.expect("test");
1078 assert!(
1081 resp.status() == StatusCode::BAD_REQUEST
1082 || resp.status() == StatusCode::UNPROCESSABLE_ENTITY
1083 );
1084 }
1085
1086 fn valid_attest_body() -> serde_json::Value {
1091 serde_json::json!({
1092 "platform_handle": "@dave#1234",
1093 "platform_user_id": "usr_abc123",
1094 "attestation_evidence": {
1095 "evidence_type": "platform-verified",
1096 "verification_method": "oauth2",
1097 "verified_at": 1_700_000_300,
1098 "platform_confidence": "high",
1099 "additional_signals": {
1100 "account_age_days": 730,
1101 "email_verified": true
1102 }
1103 }
1104 })
1105 }
1106
1107 #[tokio::test]
1108 async fn attest_successful_returns_201() {
1109 let state = Arc::new(BridgeState::new());
1110 let app = test_app(state);
1111
1112 let req = attest_request(valid_attest_body());
1113 let resp = app.oneshot(req).await.expect("test");
1114 assert_eq!(resp.status(), StatusCode::CREATED);
1115
1116 let json = response_json(resp).await;
1117 assert_eq!(json["status"], "active");
1118 assert_eq!(json["platform_handle"], "@dave#1234");
1119 assert_eq!(json["attestation_id"], "attest:bridge-test-001:usr_abc123");
1120 assert!(json["issued_at"].as_u64().is_some());
1121 assert!(json["expires_at"].as_u64().is_some());
1122
1123 let issued = json["issued_at"].as_u64().unwrap();
1124 let expires = json["expires_at"].as_u64().unwrap();
1125 assert_eq!(expires - issued, 86_400);
1126 }
1127
1128 #[tokio::test]
1129 async fn attest_stores_attestation() {
1130 let state = Arc::new(BridgeState::new());
1131 let app = test_app(Arc::clone(&state));
1132
1133 let req = attest_request(valid_attest_body());
1134 let resp = app.oneshot(req).await.expect("test");
1135 assert_eq!(resp.status(), StatusCode::CREATED);
1136
1137 let attestations = state.attestations.read().await;
1138 let stored = attestations
1139 .get("attest:bridge-test-001:usr_abc123")
1140 .expect("attestation should be stored");
1141 assert_eq!(stored.platform_handle, "@dave#1234");
1142 assert_eq!(stored.evidence.evidence_type, "platform-verified");
1143 assert_eq!(stored.evidence.platform_confidence, "high");
1144 }
1145
1146 #[tokio::test]
1147 async fn attest_empty_handle_returns_400() {
1148 let state = Arc::new(BridgeState::new());
1149 let app = test_app(state);
1150
1151 let req = attest_request(serde_json::json!({
1152 "platform_handle": "",
1153 "platform_user_id": "usr_abc123",
1154 "attestation_evidence": {
1155 "evidence_type": "platform-verified",
1156 "verification_method": "oauth2",
1157 "verified_at": 1_700_000_300,
1158 "platform_confidence": "high"
1159 }
1160 }));
1161
1162 let resp = app.oneshot(req).await.expect("test");
1163 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
1164 }
1165
1166 #[tokio::test]
1167 async fn attest_empty_user_id_returns_400() {
1168 let state = Arc::new(BridgeState::new());
1169 let app = test_app(state);
1170
1171 let req = attest_request(serde_json::json!({
1172 "platform_handle": "@dave#1234",
1173 "platform_user_id": "",
1174 "attestation_evidence": {
1175 "evidence_type": "platform-verified",
1176 "verification_method": "oauth2",
1177 "verified_at": 1_700_000_300,
1178 "platform_confidence": "high"
1179 }
1180 }));
1181
1182 let resp = app.oneshot(req).await.expect("test");
1183 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
1184 }
1185
1186 #[tokio::test]
1187 async fn attest_invalid_confidence_returns_400() {
1188 let state = Arc::new(BridgeState::new());
1189 let app = test_app(state);
1190
1191 let req = attest_request(serde_json::json!({
1192 "platform_handle": "@dave#1234",
1193 "platform_user_id": "usr_abc123",
1194 "attestation_evidence": {
1195 "evidence_type": "platform-verified",
1196 "verification_method": "oauth2",
1197 "verified_at": 1_700_000_300,
1198 "platform_confidence": "very-high"
1199 }
1200 }));
1201
1202 let resp = app.oneshot(req).await.expect("test");
1203 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
1204 }
1205
1206 #[tokio::test]
1207 async fn attest_missing_evidence_returns_422() {
1208 let state = Arc::new(BridgeState::new());
1209 let app = test_app(state);
1210
1211 let req = attest_request(serde_json::json!({
1212 "platform_handle": "@dave#1234",
1213 "platform_user_id": "usr_abc123"
1214 }));
1215
1216 let resp = app.oneshot(req).await.expect("test");
1217 assert!(
1218 resp.status() == StatusCode::BAD_REQUEST
1219 || resp.status() == StatusCode::UNPROCESSABLE_ENTITY
1220 );
1221 }
1222
1223 #[tokio::test]
1224 async fn attest_without_additional_signals() {
1225 let state = Arc::new(BridgeState::new());
1226 let app = test_app(state);
1227
1228 let req = attest_request(serde_json::json!({
1229 "platform_handle": "@dave#1234",
1230 "platform_user_id": "usr_abc123",
1231 "attestation_evidence": {
1232 "evidence_type": "oauth2",
1233 "verification_method": "oauth2-flow",
1234 "verified_at": 1_700_000_300,
1235 "platform_confidence": "medium"
1236 }
1237 }));
1238
1239 let resp = app.oneshot(req).await.expect("test");
1240 assert_eq!(resp.status(), StatusCode::CREATED);
1241
1242 let json = response_json(resp).await;
1243 assert_eq!(json["status"], "active");
1244 }
1245
1246 fn message_request(body: serde_json::Value) -> Request<Body> {
1251 Request::builder()
1252 .method("POST")
1253 .uri("/v1/scp/bridge/message")
1254 .header("content-type", "application/json")
1255 .body(Body::from(serde_json::to_vec(&body).expect("test")))
1256 .expect("test")
1257 }
1258
1259 async fn create_test_shadow(state: &Arc<BridgeState>) -> String {
1261 let app = test_app(Arc::clone(state));
1262 let req = create_request(serde_json::json!({
1263 "platform_handle": "@emitter#1234",
1264 "platform_user_id": "user-emitter-001"
1265 }));
1266 let resp = app.oneshot(req).await.expect("test");
1267 assert_eq!(resp.status(), StatusCode::CREATED);
1268 let json = response_json(resp).await;
1269 json["shadow_id"].as_str().expect("shadow_id").to_owned()
1270 }
1271
1272 #[tokio::test]
1273 async fn emit_message_returns_202() {
1274 let state = Arc::new(BridgeState::new());
1275 let shadow_id = create_test_shadow(&state).await;
1276
1277 let app = test_app(Arc::clone(&state));
1278 let req = message_request(serde_json::json!({
1279 "shadow_id": shadow_id,
1280 "content": "Hello from bridge!",
1281 "content_type": "text/plain"
1282 }));
1283
1284 let resp = app.oneshot(req).await.expect("test");
1285 assert_eq!(resp.status(), StatusCode::ACCEPTED);
1286
1287 let json = response_json(resp).await;
1288 assert!(json["message_id"].as_str().is_some());
1289 assert_eq!(json["sequence"], 1);
1290 assert_eq!(json["bridge_provenance"]["originating_platform"], "discord");
1291 assert_eq!(json["bridge_provenance"]["bridge_mode"], "Relay");
1292 assert_eq!(json["bridge_provenance"]["shadow_status"], "Shadow");
1293 assert_eq!(
1294 json["bridge_provenance"]["operator_did"],
1295 "did:dht:z6MkTestOperator"
1296 );
1297 }
1298
1299 #[tokio::test]
1300 async fn emit_message_shadow_not_found_returns_404() {
1301 let state = Arc::new(BridgeState::new());
1302 let app = test_app(state);
1303
1304 let req = message_request(serde_json::json!({
1305 "shadow_id": "shadow:nonexistent",
1306 "content": "Hello",
1307 "content_type": "text/plain"
1308 }));
1309
1310 let resp = app.oneshot(req).await.expect("test");
1311 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1312 }
1313
1314 #[tokio::test]
1315 async fn emit_message_empty_content_returns_400() {
1316 let state = Arc::new(BridgeState::new());
1317 let shadow_id = create_test_shadow(&state).await;
1318
1319 let app = test_app(state);
1320 let req = message_request(serde_json::json!({
1321 "shadow_id": shadow_id,
1322 "content": "",
1323 "content_type": "text/plain"
1324 }));
1325
1326 let resp = app.oneshot(req).await.expect("test");
1327 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
1328 }
1329
1330 fn status_request() -> Request<Body> {
1335 Request::builder()
1336 .method("GET")
1337 .uri("/v1/scp/bridge/status")
1338 .body(Body::empty())
1339 .expect("test")
1340 }
1341
1342 #[tokio::test]
1343 async fn status_returns_bridge_info() {
1344 let state = Arc::new(BridgeState::new());
1345 let _shadow_id = create_test_shadow(&state).await;
1346
1347 let app = test_app(state);
1348 let req = status_request();
1349
1350 let resp = app.oneshot(req).await.expect("test");
1351 assert_eq!(resp.status(), StatusCode::OK);
1352
1353 let json = response_json(resp).await;
1354 assert_eq!(json["bridge_id"], "bridge-test-001");
1355 assert_eq!(json["status"], "Active");
1356 assert_eq!(json["platform"], "discord");
1357 assert_eq!(json["mode"], "Relay");
1358 assert_eq!(json["operator_did"], "did:dht:z6MkTestOperator");
1359 assert_eq!(json["shadow_count"], 1);
1360 assert_eq!(json["shadows"].as_array().map(std::vec::Vec::len), Some(1));
1361 }
1362
1363 #[tokio::test]
1364 async fn status_empty_returns_zero_shadows() {
1365 let state = Arc::new(BridgeState::new());
1366 let app = test_app(state);
1367
1368 let req = status_request();
1369 let resp = app.oneshot(req).await.expect("test");
1370 assert_eq!(resp.status(), StatusCode::OK);
1371
1372 let json = response_json(resp).await;
1373 assert_eq!(json["shadow_count"], 0);
1374 }
1375
1376 fn delete_shadow_request(shadow_id: &str) -> Request<Body> {
1381 Request::builder()
1382 .method("DELETE")
1383 .uri(format!("/v1/scp/bridge/shadow/{shadow_id}"))
1384 .body(Body::empty())
1385 .expect("test")
1386 }
1387
1388 #[tokio::test]
1389 async fn delete_shadow_returns_204() {
1390 let state = Arc::new(BridgeState::new());
1391 let shadow_id = create_test_shadow(&state).await;
1392
1393 let app = test_app(state);
1394 let req = delete_shadow_request(&shadow_id);
1395
1396 let resp = app.oneshot(req).await.expect("test");
1397 assert_eq!(resp.status(), StatusCode::NO_CONTENT);
1398 }
1399
1400 #[tokio::test]
1401 async fn delete_shadow_not_found_returns_404() {
1402 let state = Arc::new(BridgeState::new());
1403 let app = test_app(state);
1404
1405 let req = delete_shadow_request("shadow:nonexistent");
1406 let resp = app.oneshot(req).await.expect("test");
1407 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1408 }
1409
1410 #[tokio::test]
1411 async fn delete_shadow_idempotent_returns_204() {
1412 let state = Arc::new(BridgeState::new());
1413 let shadow_id = create_test_shadow(&state).await;
1414
1415 let app = test_app(Arc::clone(&state));
1417 let req = delete_shadow_request(&shadow_id);
1418 let resp = app.oneshot(req).await.expect("test");
1419 assert_eq!(resp.status(), StatusCode::NO_CONTENT);
1420
1421 let app = test_app(state);
1423 let req = delete_shadow_request(&shadow_id);
1424 let resp = app.oneshot(req).await.expect("test");
1425 assert_eq!(resp.status(), StatusCode::NO_CONTENT);
1426 }
1427
1428 fn webhook_request(body: serde_json::Value) -> Request<Body> {
1433 Request::builder()
1434 .method("POST")
1435 .uri("/v1/scp/bridge/webhook")
1436 .header("content-type", "application/json")
1437 .body(Body::from(serde_json::to_vec(&body).expect("test")))
1438 .expect("test")
1439 }
1440
1441 #[tokio::test]
1442 async fn webhook_message_event_accepted() {
1443 let state = Arc::new(BridgeState::new());
1444 let shadow_id = create_test_shadow(&state).await;
1445
1446 let app = test_app(state);
1447 let req = webhook_request(serde_json::json!({
1448 "event_type": "message",
1449 "event_id": "evt-001",
1450 "timestamp": 1_700_000_500,
1451 "payload": {
1452 "shadow_id": shadow_id,
1453 "content": "Hello from webhook"
1454 }
1455 }));
1456
1457 let resp = app.oneshot(req).await.expect("test");
1458 assert_eq!(resp.status(), StatusCode::OK);
1459
1460 let json = response_json(resp).await;
1461 assert_eq!(json["accepted"], true);
1462 assert_eq!(json["event_id"], "evt-001");
1463 }
1464
1465 #[tokio::test]
1466 async fn webhook_deduplication() {
1467 let state = Arc::new(BridgeState::new());
1468
1469 let app = test_app(Arc::clone(&state));
1470 let req = webhook_request(serde_json::json!({
1471 "event_type": "presence",
1472 "event_id": "evt-dedup-001",
1473 "timestamp": 1_700_000_500,
1474 "payload": {}
1475 }));
1476 let resp = app.oneshot(req).await.expect("test");
1477 assert_eq!(resp.status(), StatusCode::OK);
1478 let json = response_json(resp).await;
1479 assert_eq!(json["accepted"], true);
1480
1481 let app = test_app(state);
1483 let req = webhook_request(serde_json::json!({
1484 "event_type": "presence",
1485 "event_id": "evt-dedup-001",
1486 "timestamp": 1_700_000_600,
1487 "payload": {}
1488 }));
1489 let resp = app.oneshot(req).await.expect("test");
1490 assert_eq!(resp.status(), StatusCode::OK);
1491 let json = response_json(resp).await;
1492 assert_eq!(json["accepted"], true);
1493 }
1494
1495 #[tokio::test]
1496 async fn webhook_unknown_event_type_rejected() {
1497 let state = Arc::new(BridgeState::new());
1498 let app = test_app(state);
1499
1500 let req = webhook_request(serde_json::json!({
1501 "event_type": "unknown_type",
1502 "event_id": "evt-002",
1503 "timestamp": 1_700_000_500,
1504 "payload": {}
1505 }));
1506
1507 let resp = app.oneshot(req).await.expect("test");
1508 assert_eq!(resp.status(), StatusCode::OK);
1509
1510 let json = response_json(resp).await;
1511 assert_eq!(json["accepted"], false);
1512 assert!(json["reason"].as_str().is_some());
1513 }
1514
1515 #[tokio::test]
1516 async fn webhook_user_departed_triggers_shadow_deletion() {
1517 let state = Arc::new(BridgeState::new());
1518 let shadow_id = create_test_shadow(&state).await;
1519
1520 let app = test_app(Arc::clone(&state));
1521 let req = webhook_request(serde_json::json!({
1522 "event_type": "user_departed",
1523 "event_id": "evt-depart-001",
1524 "timestamp": 1_700_000_500,
1525 "payload": {
1526 "shadow_id": shadow_id
1527 }
1528 }));
1529
1530 let resp = app.oneshot(req).await.expect("test");
1531 assert_eq!(resp.status(), StatusCode::OK);
1532 let json = response_json(resp).await;
1533 assert_eq!(json["accepted"], true);
1534
1535 let deleted = state.deleted_shadows.read().await;
1537 assert!(deleted.contains(&shadow_id));
1538 }
1539
1540 #[tokio::test]
1541 async fn webhook_all_event_types_accepted() {
1542 let state = Arc::new(BridgeState::new());
1543
1544 for (i, event_type) in [
1545 "message",
1546 "presence",
1547 "identity_update",
1548 "user_departed",
1549 "message_edit",
1550 "message_delete",
1551 ]
1552 .iter()
1553 .enumerate()
1554 {
1555 let shadow_id = if *event_type == "message" {
1557 create_test_shadow(&state).await
1558 } else {
1559 String::new()
1560 };
1561
1562 let payload = if *event_type == "message" {
1563 serde_json::json!({ "shadow_id": shadow_id, "content": "test" })
1564 } else {
1565 serde_json::json!({})
1566 };
1567
1568 let app = test_app(Arc::clone(&state));
1569 let req = webhook_request(serde_json::json!({
1570 "event_type": event_type,
1571 "event_id": format!("evt-type-{i}"),
1572 "timestamp": 1_700_000_500,
1573 "payload": payload
1574 }));
1575
1576 let resp = app.oneshot(req).await.expect("test");
1577 assert_eq!(resp.status(), StatusCode::OK);
1578 let json = response_json(resp).await;
1579 assert_eq!(
1580 json["accepted"], true,
1581 "event type '{event_type}' should be accepted"
1582 );
1583 }
1584 }
1585
1586 #[tokio::test]
1594 async fn integration_full_lifecycle() {
1595 let state = Arc::new(BridgeState::new());
1596
1597 let app = test_app(Arc::clone(&state));
1599 let req = create_request(serde_json::json!({
1600 "platform_handle": "@lifecycle-user#1234",
1601 "platform_user_id": "lifecycle-user-001"
1602 }));
1603 let resp = app.oneshot(req).await.expect("test");
1604 assert_eq!(resp.status(), StatusCode::CREATED);
1605 let create_json = response_json(resp).await;
1606 let shadow_id = create_json["shadow_id"]
1607 .as_str()
1608 .expect("shadow_id")
1609 .to_owned();
1610 assert_eq!(create_json["attributed_role"], "observer");
1611
1612 let app = test_app(Arc::clone(&state));
1614 let req = message_request(serde_json::json!({
1615 "shadow_id": &shadow_id,
1616 "content": "Hello from lifecycle test!",
1617 "content_type": "text/plain",
1618 "platform_message_id": "ext-msg-001",
1619 "platform_timestamp": 1_700_001_000
1620 }));
1621 let resp = app.oneshot(req).await.expect("test");
1622 assert_eq!(resp.status(), StatusCode::ACCEPTED);
1623 let msg_json = response_json(resp).await;
1624 assert!(msg_json["message_id"].as_str().is_some());
1625 assert_eq!(msg_json["sequence"], 1);
1626 assert_eq!(
1628 msg_json["bridge_provenance"]["originating_platform"],
1629 "discord"
1630 );
1631 assert_eq!(msg_json["bridge_provenance"]["bridge_mode"], "Relay");
1632 assert_eq!(
1633 msg_json["bridge_provenance"]["operator_did"],
1634 "did:dht:z6MkTestOperator"
1635 );
1636 assert_eq!(msg_json["bridge_provenance"]["shadow_status"], "Shadow");
1637
1638 let app = test_app(Arc::clone(&state));
1640 let req = attest_request(serde_json::json!({
1641 "platform_handle": "@lifecycle-user#1234",
1642 "platform_user_id": "lifecycle-user-001",
1643 "attestation_evidence": {
1644 "evidence_type": "platform-verified",
1645 "verification_method": "oauth2",
1646 "verified_at": 1_700_001_200,
1647 "platform_confidence": "high"
1648 }
1649 }));
1650 let resp = app.oneshot(req).await.expect("test");
1651 assert_eq!(resp.status(), StatusCode::CREATED);
1652
1653 let app = test_app(Arc::clone(&state));
1655 let req = status_request();
1656 let resp = app.oneshot(req).await.expect("test");
1657 assert_eq!(resp.status(), StatusCode::OK);
1658 let status_json = response_json(resp).await;
1659 assert_eq!(status_json["bridge_id"], "bridge-test-001");
1660 assert_eq!(status_json["status"], "Active");
1661 assert_eq!(status_json["shadow_count"], 1);
1662 let shadows = status_json["shadows"].as_array().expect("shadows array");
1663 assert_eq!(shadows.len(), 1);
1664 assert_eq!(shadows[0]["shadow_id"], shadow_id);
1665
1666 let app = test_app(Arc::clone(&state));
1668 let req = webhook_request(serde_json::json!({
1669 "event_type": "presence",
1670 "event_id": "evt-lifecycle-001",
1671 "timestamp": 1_700_001_500,
1672 "payload": {}
1673 }));
1674 let resp = app.oneshot(req).await.expect("test");
1675 assert_eq!(resp.status(), StatusCode::OK);
1676 let wh_json = response_json(resp).await;
1677 assert_eq!(wh_json["accepted"], true);
1678
1679 let app = test_app(Arc::clone(&state));
1681 let req = delete_shadow_request(&shadow_id);
1682 let resp = app.oneshot(req).await.expect("test");
1683 assert_eq!(resp.status(), StatusCode::NO_CONTENT);
1684
1685 let app = test_app(state);
1687 let req = status_request();
1688 let resp = app.oneshot(req).await.expect("test");
1689 assert_eq!(resp.status(), StatusCode::OK);
1690 let status_after = response_json(resp).await;
1691 assert_eq!(status_after["shadow_count"], 0);
1692 }
1693
1694 #[tokio::test]
1696 async fn all_endpoints_return_json_content_type() {
1697 let state = Arc::new(BridgeState::new());
1698 let shadow_id = create_test_shadow(&state).await;
1699
1700 let app = test_app(Arc::clone(&state));
1702 let req = create_request(serde_json::json!({
1703 "platform_handle": "@ct-user",
1704 "platform_user_id": "ct-user-001"
1705 }));
1706 let resp = app.oneshot(req).await.expect("test");
1707 assert_eq!(
1708 resp.headers()
1709 .get("content-type")
1710 .and_then(|v| v.to_str().ok()),
1711 Some("application/json"),
1712 "POST /shadow must return application/json"
1713 );
1714
1715 let app = test_app(Arc::clone(&state));
1717 let req = message_request(serde_json::json!({
1718 "shadow_id": &shadow_id,
1719 "content": "content-type test",
1720 "content_type": "text/plain"
1721 }));
1722 let resp = app.oneshot(req).await.expect("test");
1723 assert_eq!(
1724 resp.headers()
1725 .get("content-type")
1726 .and_then(|v| v.to_str().ok()),
1727 Some("application/json"),
1728 "POST /message must return application/json"
1729 );
1730
1731 let app = test_app(Arc::clone(&state));
1733 let req = status_request();
1734 let resp = app.oneshot(req).await.expect("test");
1735 assert_eq!(
1736 resp.headers()
1737 .get("content-type")
1738 .and_then(|v| v.to_str().ok()),
1739 Some("application/json"),
1740 "GET /status must return application/json"
1741 );
1742
1743 let app = test_app(Arc::clone(&state));
1745 let req = attest_request(valid_attest_body());
1746 let resp = app.oneshot(req).await.expect("test");
1747 assert_eq!(
1748 resp.headers()
1749 .get("content-type")
1750 .and_then(|v| v.to_str().ok()),
1751 Some("application/json"),
1752 "POST /attest must return application/json"
1753 );
1754
1755 let app = test_app(Arc::clone(&state));
1757 let req = webhook_request(serde_json::json!({
1758 "event_type": "presence",
1759 "event_id": "ct-evt-001",
1760 "timestamp": 1_700_000_500,
1761 "payload": {}
1762 }));
1763 let resp = app.oneshot(req).await.expect("test");
1764 assert_eq!(
1765 resp.headers()
1766 .get("content-type")
1767 .and_then(|v| v.to_str().ok()),
1768 Some("application/json"),
1769 "POST /webhook must return application/json"
1770 );
1771 }
1772
1773 #[tokio::test]
1775 async fn error_responses_use_scp_format() {
1776 let state = Arc::new(BridgeState::new());
1777 let app = test_app(state);
1778
1779 let req = message_request(serde_json::json!({
1781 "shadow_id": "shadow:nonexistent",
1782 "content": "test",
1783 "content_type": "text/plain"
1784 }));
1785 let resp = app.oneshot(req).await.expect("test");
1786 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1787 let json = response_json(resp).await;
1788 assert!(
1789 json["code"].as_str().is_some(),
1790 "error response must have code field"
1791 );
1792 assert!(
1793 json["error"].as_str().is_some(),
1794 "error response must have error field"
1795 );
1796 }
1797
1798 #[tokio::test]
1806 async fn delete_shadow_through_router() {
1807 let state = Arc::new(BridgeState::new());
1808 let shadow_id = create_test_shadow(&state).await;
1809
1810 let app = test_app(Arc::clone(&state));
1812 let req = delete_shadow_request(&shadow_id);
1813 let resp = app.oneshot(req).await.expect("test");
1814 assert_eq!(resp.status(), StatusCode::NO_CONTENT);
1815
1816 let app = test_app(state);
1818 let req = delete_shadow_request("shadow:nonexistent:claimed");
1819 let resp = app.oneshot(req).await.expect("test");
1820 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1821 let json = response_json(resp).await;
1822 assert_eq!(json["code"], "SHADOW_NOT_FOUND");
1823 }
1824
1825 #[tokio::test]
1827 async fn bridge_router_mounts_all_endpoints() {
1828 let state = Arc::new(BridgeState::new());
1829 let auth_ctx = test_auth_ctx();
1830 let router = bridge_router(state).layer(axum::Extension(auth_ctx));
1831
1832 let req = create_request(serde_json::json!({
1834 "platform_handle": "@router-test",
1835 "platform_user_id": "router-user-001"
1836 }));
1837 let resp = router.clone().oneshot(req).await.expect("test");
1838 assert_eq!(resp.status(), StatusCode::CREATED);
1839 }
1840}