mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-08 10:27:46 +08:00
feat(security): harden gateway boundaries and usage policies
Consolidate subscription usage policy enforcement, privacy-safe persistence, and gateway security hardening into one reviewable change. Includes bounded HTTP and execution envelopes, header and protocol guards, DNS and relay validation, authentication and secret projection hardening, secure backup/install paths, and regression coverage.
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use aether_data::repository::auth::{
|
||||
InMemoryAuthApiKeySnapshotRepository, StoredAuthApiKeySnapshot,
|
||||
};
|
||||
use aether_data::repository::video_tasks::InMemoryVideoTaskRepository;
|
||||
use aether_data_contracts::repository::video_tasks::{
|
||||
UpsertVideoTask, VideoTaskStatus, VideoTaskWriteRepository,
|
||||
@@ -9,9 +12,43 @@ use axum::routing::any;
|
||||
use axum::{extract::Request, Json, Router};
|
||||
use http::StatusCode;
|
||||
use serde_json::json;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use super::{build_router_with_state, build_state_with_execution_runtime_override, start_server};
|
||||
|
||||
fn hash_api_key(value: &str) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(value.as_bytes());
|
||||
format!("{:x}", hasher.finalize())
|
||||
}
|
||||
|
||||
fn sample_auth_snapshot(api_key_id: &str, user_id: &str) -> StoredAuthApiKeySnapshot {
|
||||
StoredAuthApiKeySnapshot::new(
|
||||
user_id.to_string(),
|
||||
"video-user".to_string(),
|
||||
Some("[email protected]".to_string()),
|
||||
"user".to_string(),
|
||||
"local".to_string(),
|
||||
true,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
api_key_id.to_string(),
|
||||
Some("default".to_string()),
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
Some(60),
|
||||
Some(5),
|
||||
Some(4_102_444_800),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.expect("auth snapshot should build")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_reads_openai_video_task_via_data_read_side_without_hitting_public_route() {
|
||||
let public_hits = Arc::new(Mutex::new(0usize));
|
||||
@@ -92,15 +129,25 @@ async fn gateway_reads_openai_video_task_via_data_read_side_without_hitting_publ
|
||||
})
|
||||
.await
|
||||
.expect("upsert should succeed");
|
||||
let auth_repository = Arc::new(InMemoryAuthApiKeySnapshotRepository::seed(vec![(
|
||||
Some(hash_api_key("client-video-read-owner-key")),
|
||||
sample_auth_snapshot("api-key-video-db-rotated-123", "user-video-db-123"),
|
||||
)]));
|
||||
|
||||
let gateway = build_router_with_state(
|
||||
build_state_with_execution_runtime_override(upstream_url.clone())
|
||||
.with_video_task_data_reader_for_tests(repository),
|
||||
.with_data_state_for_tests(
|
||||
crate::data::GatewayDataState::with_auth_and_video_task_repository_for_tests(
|
||||
auth_repository,
|
||||
repository,
|
||||
),
|
||||
),
|
||||
);
|
||||
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||
|
||||
let response = reqwest::Client::new()
|
||||
.get(format!("{gateway_url}/v1/videos/task-db-123"))
|
||||
.bearer_auth("client-video-read-owner-key")
|
||||
.send()
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
@@ -211,10 +258,19 @@ async fn gateway_reads_gemini_video_task_via_data_read_side_without_hitting_publ
|
||||
})
|
||||
.await
|
||||
.expect("upsert should succeed");
|
||||
let auth_repository = Arc::new(InMemoryAuthApiKeySnapshotRepository::seed(vec![(
|
||||
Some(hash_api_key("client-gemini-video-read-owner-key")),
|
||||
sample_auth_snapshot("api-key-video-db-rotated-123", "user-video-db-123"),
|
||||
)]));
|
||||
|
||||
let gateway = build_router_with_state(
|
||||
build_state_with_execution_runtime_override(upstream_url.clone())
|
||||
.with_video_task_data_reader_for_tests(repository),
|
||||
.with_data_state_for_tests(
|
||||
crate::data::GatewayDataState::with_auth_and_video_task_repository_for_tests(
|
||||
auth_repository,
|
||||
repository,
|
||||
),
|
||||
),
|
||||
);
|
||||
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||
|
||||
@@ -222,6 +278,7 @@ async fn gateway_reads_gemini_video_task_via_data_read_side_without_hitting_publ
|
||||
.get(format!(
|
||||
"{gateway_url}/v1beta/models/veo-3/operations/localshort123"
|
||||
))
|
||||
.header("x-goog-api-key", "client-gemini-video-read-owner-key")
|
||||
.send()
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
@@ -239,3 +296,118 @@ async fn gateway_reads_gemini_video_task_via_data_read_side_without_hitting_publ
|
||||
gateway_handle.abort();
|
||||
upstream_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_hides_data_backed_video_task_from_non_owner() {
|
||||
let public_hits = Arc::new(Mutex::new(0usize));
|
||||
let public_hits_clone = Arc::clone(&public_hits);
|
||||
let upstream = Router::new()
|
||||
.route(
|
||||
"/api/internal/gateway/resolve",
|
||||
any(|_request: Request| async move {
|
||||
Json(json!({
|
||||
"action": "proxy_public",
|
||||
"route_class": "ai_public",
|
||||
"route_family": "openai",
|
||||
"route_kind": "video",
|
||||
"auth_endpoint_signature": "openai:video",
|
||||
"execution_runtime_candidate": true,
|
||||
"auth_context": {
|
||||
"user_id": "user-video-foreign",
|
||||
"api_key_id": "key-video-foreign",
|
||||
"access_allowed": true
|
||||
},
|
||||
"public_path": "/v1/videos/task-owned-123"
|
||||
}))
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/v1/videos/task-owned-123",
|
||||
any(move |_request: Request| {
|
||||
let public_hits_inner = Arc::clone(&public_hits_clone);
|
||||
async move {
|
||||
*public_hits_inner.lock().expect("mutex should lock") += 1;
|
||||
(StatusCode::IM_A_TEAPOT, Body::from("public-route-hit"))
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
let (upstream_url, upstream_handle) = start_server(upstream).await;
|
||||
let repository = Arc::new(InMemoryVideoTaskRepository::default());
|
||||
repository
|
||||
.upsert(UpsertVideoTask {
|
||||
id: "task-owned-123".to_string(),
|
||||
short_id: None,
|
||||
request_id: "request-owned-123".to_string(),
|
||||
user_id: Some("user-video-owner".to_string()),
|
||||
api_key_id: Some("key-video-owner".to_string()),
|
||||
username: None,
|
||||
api_key_name: None,
|
||||
external_task_id: Some("ext-owned-123".to_string()),
|
||||
provider_id: Some("provider-owned-123".to_string()),
|
||||
endpoint_id: Some("endpoint-owned-123".to_string()),
|
||||
key_id: Some("provider-key-owned-123".to_string()),
|
||||
client_api_format: Some("openai:video".to_string()),
|
||||
provider_api_format: Some("openai:video".to_string()),
|
||||
format_converted: false,
|
||||
model: Some("sora-2".to_string()),
|
||||
prompt: Some("private video".to_string()),
|
||||
original_request_body: Some(json!({"prompt": "private video"})),
|
||||
duration_seconds: Some(4),
|
||||
resolution: Some("720p".to_string()),
|
||||
aspect_ratio: Some("16:9".to_string()),
|
||||
size: Some("1280x720".to_string()),
|
||||
status: VideoTaskStatus::Processing,
|
||||
progress_percent: 50,
|
||||
progress_message: None,
|
||||
retry_count: 0,
|
||||
poll_interval_seconds: 10,
|
||||
next_poll_at_unix_secs: Some(124),
|
||||
poll_count: 1,
|
||||
max_poll_count: 360,
|
||||
created_at_unix_ms: 123,
|
||||
submitted_at_unix_secs: Some(123),
|
||||
completed_at_unix_secs: None,
|
||||
updated_at_unix_secs: 124,
|
||||
error_code: None,
|
||||
error_message: None,
|
||||
video_url: None,
|
||||
request_metadata: None,
|
||||
})
|
||||
.await
|
||||
.expect("upsert should succeed");
|
||||
let auth_repository = Arc::new(InMemoryAuthApiKeySnapshotRepository::seed(vec![(
|
||||
Some(hash_api_key("client-video-read-foreign-key")),
|
||||
sample_auth_snapshot("key-video-foreign", "user-video-foreign"),
|
||||
)]));
|
||||
let gateway = build_router_with_state(
|
||||
build_state_with_execution_runtime_override(upstream_url.clone())
|
||||
.with_data_state_for_tests(
|
||||
crate::data::GatewayDataState::with_auth_and_video_task_repository_for_tests(
|
||||
auth_repository,
|
||||
repository,
|
||||
),
|
||||
),
|
||||
);
|
||||
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||
|
||||
let response = reqwest::Client::new()
|
||||
.get(format!("{gateway_url}/v1/videos/task-owned-123"))
|
||||
.bearer_auth("client-video-read-foreign-key")
|
||||
.send()
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||
assert_eq!(
|
||||
response
|
||||
.json::<serde_json::Value>()
|
||||
.await
|
||||
.expect("json body"),
|
||||
json!({"detail": "Video task not found"})
|
||||
);
|
||||
assert_eq!(*public_hits.lock().expect("mutex should lock"), 0);
|
||||
|
||||
gateway_handle.abort();
|
||||
upstream_handle.abort();
|
||||
}
|
||||
|
||||
@@ -24,7 +24,10 @@ use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::constants::TRACE_ID_HEADER;
|
||||
|
||||
use super::{build_router_with_state, build_state_with_execution_runtime_override, start_server};
|
||||
use super::{
|
||||
build_router_with_state, build_state_with_execution_runtime_override, start_server,
|
||||
video_proxy_node_repository,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_executes_gemini_video_create_via_local_decision_gate_with_local_planning_only() {
|
||||
@@ -404,7 +407,10 @@ async fn gateway_executes_gemini_video_create_via_local_decision_gate_with_local
|
||||
provider_catalog_repository,
|
||||
Arc::clone(&request_candidate_repository),
|
||||
DEVELOPMENT_ENCRYPTION_KEY,
|
||||
),
|
||||
)
|
||||
.attach_proxy_node_repository_for_tests(video_proxy_node_repository([
|
||||
"proxy-node-gemini-video-local",
|
||||
])),
|
||||
);
|
||||
let gateway = build_router_with_state(gateway_state);
|
||||
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
use aether_crypto::{encrypt_python_fernet_plaintext, DEVELOPMENT_ENCRYPTION_KEY};
|
||||
use aether_crypto::DEVELOPMENT_ENCRYPTION_KEY;
|
||||
use aether_data::repository::auth::{
|
||||
InMemoryAuthApiKeySnapshotRepository, StoredAuthApiKeySnapshot,
|
||||
};
|
||||
use aether_data::repository::candidates::InMemoryRequestCandidateRepository;
|
||||
use aether_data::repository::provider_catalog::InMemoryProviderCatalogReadRepository;
|
||||
use aether_data::repository::video_tasks::InMemoryVideoTaskRepository;
|
||||
use aether_data_contracts::repository::candidates::{
|
||||
RequestCandidateReadRepository, RequestCandidateStatus,
|
||||
};
|
||||
use aether_data_contracts::repository::provider_catalog::{
|
||||
StoredProviderCatalogEndpoint, StoredProviderCatalogKey, StoredProviderCatalogProvider,
|
||||
};
|
||||
use aether_data_contracts::repository::video_tasks::{
|
||||
UpsertVideoTask, VideoTaskStatus, VideoTaskWriteRepository,
|
||||
};
|
||||
@@ -18,15 +17,49 @@ use axum::{extract::Request, Json, Router};
|
||||
use http::header::{HeaderName, HeaderValue};
|
||||
use http::StatusCode;
|
||||
use serde_json::json;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::constants::{CONTROL_EXECUTED_HEADER, CONTROL_EXECUTE_FALLBACK_HEADER, TRACE_ID_HEADER};
|
||||
|
||||
use super::{
|
||||
build_router_with_state, build_state_with_execution_runtime_override, start_server,
|
||||
VideoTaskTruthSourceMode,
|
||||
video_provider_catalog_repository, VideoTaskTruthSourceMode,
|
||||
};
|
||||
|
||||
fn hash_api_key(value: &str) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(value.as_bytes());
|
||||
format!("{:x}", hasher.finalize())
|
||||
}
|
||||
|
||||
fn sample_auth_snapshot(api_key_id: &str, user_id: &str) -> StoredAuthApiKeySnapshot {
|
||||
StoredAuthApiKeySnapshot::new(
|
||||
user_id.to_string(),
|
||||
"video-user".to_string(),
|
||||
Some("[email protected]".to_string()),
|
||||
"user".to_string(),
|
||||
"local".to_string(),
|
||||
true,
|
||||
false,
|
||||
Some(json!(["gemini"])),
|
||||
Some(json!(["gemini:video"])),
|
||||
Some(json!(["veo-3"])),
|
||||
api_key_id.to_string(),
|
||||
Some("default".to_string()),
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
Some(60),
|
||||
Some(5),
|
||||
Some(4_102_444_800),
|
||||
Some(json!(["gemini"])),
|
||||
Some(json!(["gemini:video"])),
|
||||
Some(json!(["veo-3"])),
|
||||
)
|
||||
.expect("auth snapshot should build")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_executes_gemini_video_cancel_via_data_backed_local_follow_up_with_local_planning_only(
|
||||
) {
|
||||
@@ -241,16 +274,35 @@ async fn gateway_executes_gemini_video_cancel_via_data_backed_local_follow_up_wi
|
||||
})
|
||||
.await
|
||||
.expect("upsert should succeed");
|
||||
let auth_repository = Arc::new(InMemoryAuthApiKeySnapshotRepository::seed(vec![(
|
||||
Some(hash_api_key("client-gemini-video-cancel-local-key")),
|
||||
sample_auth_snapshot(
|
||||
"key-gemini-video-cancel-rotated-local-123",
|
||||
"user-gemini-video-cancel-local-123",
|
||||
),
|
||||
)]));
|
||||
let provider_catalog_repository = video_provider_catalog_repository(
|
||||
"provider-gemini-video-local-1",
|
||||
"gemini",
|
||||
"endpoint-gemini-video-local-1",
|
||||
"gemini:video",
|
||||
"https://generativelanguage.googleapis.com",
|
||||
"key-gemini-video-local-1",
|
||||
"sk-upstream-gemini-video",
|
||||
);
|
||||
|
||||
let (upstream_url, upstream_handle) = start_server(upstream).await;
|
||||
let (execution_runtime_url, execution_runtime_handle) = start_server(execution_runtime).await;
|
||||
let gateway_state = build_state_with_execution_runtime_override(execution_runtime_url)
|
||||
.with_data_state_for_tests(
|
||||
crate::data::GatewayDataState::with_video_task_and_request_candidate_repository_for_tests(
|
||||
repository,
|
||||
Arc::clone(&request_candidate_repository),
|
||||
),
|
||||
);
|
||||
crate::data::GatewayDataState::with_video_task_provider_transport_and_request_candidate_repository_for_tests(
|
||||
repository,
|
||||
provider_catalog_repository,
|
||||
Arc::clone(&request_candidate_repository),
|
||||
DEVELOPMENT_ENCRYPTION_KEY,
|
||||
)
|
||||
.with_auth_api_key_reader(auth_repository),
|
||||
);
|
||||
let gateway = build_router_with_state(gateway_state);
|
||||
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||
|
||||
@@ -258,7 +310,7 @@ async fn gateway_executes_gemini_video_cancel_via_data_backed_local_follow_up_wi
|
||||
.post(format!(
|
||||
"{gateway_url}/v1beta/models/veo-3/operations/localshort123:cancel"
|
||||
))
|
||||
.header("x-goog-api-key", "client-key")
|
||||
.header("x-goog-api-key", "client-gemini-video-cancel-local-key")
|
||||
.header(http::header::CONTENT_TYPE, "application/json")
|
||||
.header(TRACE_ID_HEADER, "trace-gemini-video-cancel-local-123")
|
||||
.body("{}")
|
||||
@@ -267,13 +319,8 @@ async fn gateway_executes_gemini_video_cancel_via_data_backed_local_follow_up_wi
|
||||
.expect("request should succeed");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
response
|
||||
.json::<serde_json::Value>()
|
||||
.await
|
||||
.expect("body should parse"),
|
||||
json!({})
|
||||
);
|
||||
let response: serde_json::Value = response.json().await.expect("body should parse");
|
||||
assert_eq!(response, json!({}));
|
||||
|
||||
let seen_execution_runtime_request = seen_execution_runtime
|
||||
.lock()
|
||||
@@ -318,75 +365,6 @@ async fn gateway_executes_gemini_video_cancel_via_reconstructed_data_backed_loca
|
||||
api_key: String,
|
||||
}
|
||||
|
||||
fn sample_provider() -> StoredProviderCatalogProvider {
|
||||
StoredProviderCatalogProvider::new(
|
||||
"provider-gemini-video-followup-1".to_string(),
|
||||
"gemini".to_string(),
|
||||
Some("https://example.com".to_string()),
|
||||
"custom".to_string(),
|
||||
)
|
||||
.expect("provider should build")
|
||||
.with_transport_fields(
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
Some(2),
|
||||
None,
|
||||
Some(20.0),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
fn sample_endpoint() -> StoredProviderCatalogEndpoint {
|
||||
StoredProviderCatalogEndpoint::new(
|
||||
"endpoint-gemini-video-followup-1".to_string(),
|
||||
"provider-gemini-video-followup-1".to_string(),
|
||||
"gemini:video".to_string(),
|
||||
Some("gemini".to_string()),
|
||||
Some("video".to_string()),
|
||||
true,
|
||||
)
|
||||
.expect("endpoint should build")
|
||||
.with_transport_fields(
|
||||
"https://generativelanguage.googleapis.com".to_string(),
|
||||
None,
|
||||
None,
|
||||
Some(2),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.expect("endpoint transport should build")
|
||||
}
|
||||
|
||||
fn sample_key() -> StoredProviderCatalogKey {
|
||||
StoredProviderCatalogKey::new(
|
||||
"key-gemini-video-followup-1".to_string(),
|
||||
"provider-gemini-video-followup-1".to_string(),
|
||||
"prod".to_string(),
|
||||
"api_key".to_string(),
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.expect("key should build")
|
||||
.with_transport_fields(
|
||||
Some(json!(["gemini:video"])),
|
||||
encrypt_python_fernet_plaintext(DEVELOPMENT_ENCRYPTION_KEY, "sk-upstream-gemini-video")
|
||||
.expect("api key should encrypt"),
|
||||
None,
|
||||
None,
|
||||
Some(json!({"gemini:video": 1})),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.expect("key transport should build")
|
||||
}
|
||||
|
||||
let decision_hits = Arc::new(Mutex::new(0usize));
|
||||
let decision_hits_clone = Arc::clone(&decision_hits);
|
||||
let execute_hits = Arc::new(Mutex::new(0usize));
|
||||
@@ -562,31 +540,43 @@ async fn gateway_executes_gemini_video_cancel_via_reconstructed_data_backed_loca
|
||||
})
|
||||
.await
|
||||
.expect("upsert should succeed");
|
||||
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
|
||||
vec![sample_provider()],
|
||||
vec![sample_endpoint()],
|
||||
vec![sample_key()],
|
||||
));
|
||||
let provider_catalog_repository = video_provider_catalog_repository(
|
||||
"provider-gemini-video-followup-1",
|
||||
"gemini",
|
||||
"endpoint-gemini-video-followup-1",
|
||||
"gemini:video",
|
||||
"https://generativelanguage.googleapis.com",
|
||||
"key-gemini-video-followup-1",
|
||||
"sk-upstream-gemini-video",
|
||||
);
|
||||
let request_candidate_repository = Arc::new(InMemoryRequestCandidateRepository::default());
|
||||
let auth_repository = Arc::new(InMemoryAuthApiKeySnapshotRepository::seed(vec![(
|
||||
Some(hash_api_key("client-gemini-video-cancel-op-key")),
|
||||
sample_auth_snapshot(
|
||||
"key-gemini-video-cancel-rotated-op-123",
|
||||
"user-gemini-video-cancel-op-123",
|
||||
),
|
||||
)]));
|
||||
|
||||
let gateway = build_router_with_state(
|
||||
build_state_with_execution_runtime_override(execution_runtime_url)
|
||||
let gateway_state = build_state_with_execution_runtime_override(execution_runtime_url)
|
||||
.with_video_task_truth_source_mode(VideoTaskTruthSourceMode::RustAuthoritative)
|
||||
.with_data_state_for_tests(
|
||||
crate::data::GatewayDataState::with_video_task_provider_transport_and_request_candidate_repository_for_tests(
|
||||
repository,
|
||||
repository.clone(),
|
||||
provider_catalog_repository,
|
||||
Arc::clone(&request_candidate_repository),
|
||||
DEVELOPMENT_ENCRYPTION_KEY,
|
||||
),
|
||||
),
|
||||
);
|
||||
)
|
||||
.with_auth_api_key_reader(auth_repository),
|
||||
);
|
||||
let gateway = build_router_with_state(gateway_state);
|
||||
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||
|
||||
let response = reqwest::Client::new()
|
||||
.post(format!(
|
||||
"{gateway_url}/v1beta/models/veo-3/operations/opshort123:cancel"
|
||||
))
|
||||
.header("x-goog-api-key", "client-gemini-video-cancel-op-key")
|
||||
.header(CONTROL_EXECUTE_FALLBACK_HEADER, "true")
|
||||
.header(TRACE_ID_HEADER, "trace-gemini-video-cancel-op-123")
|
||||
.send()
|
||||
@@ -594,13 +584,8 @@ async fn gateway_executes_gemini_video_cancel_via_reconstructed_data_backed_loca
|
||||
.expect("request should succeed");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
response
|
||||
.json::<serde_json::Value>()
|
||||
.await
|
||||
.expect("body should parse"),
|
||||
json!({})
|
||||
);
|
||||
let response: serde_json::Value = response.json().await.expect("body should parse");
|
||||
assert_eq!(response, json!({}));
|
||||
|
||||
let seen_execution_runtime_request = seen_execution_runtime
|
||||
.lock()
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use aether_crypto::{encrypt_python_fernet_plaintext, DEVELOPMENT_ENCRYPTION_KEY};
|
||||
use aether_data::repository::provider_catalog::InMemoryProviderCatalogReadRepository;
|
||||
use aether_data::repository::proxy_nodes::{InMemoryProxyNodeRepository, StoredProxyNode};
|
||||
use aether_data::repository::video_tasks::InMemoryVideoTaskRepository;
|
||||
use aether_data_contracts::repository::provider_catalog::{
|
||||
StoredProviderCatalogEndpoint, StoredProviderCatalogKey, StoredProviderCatalogProvider,
|
||||
};
|
||||
use aether_data_contracts::repository::video_tasks::{
|
||||
UpsertVideoTask, VideoTaskLookupKey, VideoTaskReadRepository, VideoTaskWriteRepository,
|
||||
};
|
||||
@@ -30,3 +36,137 @@ mod openai_sync_task;
|
||||
mod registry_poller;
|
||||
mod routing;
|
||||
mod stream;
|
||||
|
||||
/// Seed online manual proxy nodes for video execution fixtures.
|
||||
///
|
||||
/// Production resolution intentionally fails closed when a provider refers to
|
||||
/// an unregistered node. Tests that exercise a configured node therefore need
|
||||
/// the same deployment-state record; the loopback URL is never contacted when
|
||||
/// the execution-runtime override is active.
|
||||
pub(super) fn video_proxy_node_repository<I, S>(node_ids: I) -> Arc<InMemoryProxyNodeRepository>
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: AsRef<str>,
|
||||
{
|
||||
let nodes = node_ids.into_iter().map(|node_id| {
|
||||
let node_id = node_id.as_ref();
|
||||
StoredProxyNode::new(
|
||||
node_id.to_string(),
|
||||
format!("video-test-{node_id}"),
|
||||
"127.0.0.1".to_string(),
|
||||
1,
|
||||
true,
|
||||
"online".to_string(),
|
||||
30,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
false,
|
||||
1,
|
||||
)
|
||||
.expect("video test proxy node should build")
|
||||
.with_manual_proxy_fields(Some("http://127.0.0.1:1".to_string()), None, None)
|
||||
.with_tunnel_generation(format!("video-test-generation-{node_id}"))
|
||||
});
|
||||
Arc::new(InMemoryProxyNodeRepository::seed(nodes))
|
||||
}
|
||||
|
||||
/// Build a provider catalog row set for tasks whose sensitive snapshot fields
|
||||
/// have been removed by the persistence boundary. The credential is sealed
|
||||
/// with the record-bound v2 envelope used by production, so a read-only test
|
||||
/// state can reconstruct transport without relying on a migration writer.
|
||||
pub(super) fn video_provider_catalog_repository(
|
||||
provider_id: &str,
|
||||
provider_type: &str,
|
||||
endpoint_id: &str,
|
||||
api_format: &str,
|
||||
endpoint_base_url: &str,
|
||||
key_id: &str,
|
||||
upstream_api_key: &str,
|
||||
) -> Arc<InMemoryProviderCatalogReadRepository> {
|
||||
fn seal_bound_credential(
|
||||
provider_id: &str,
|
||||
key_id: &str,
|
||||
field: &str,
|
||||
plaintext: &str,
|
||||
) -> String {
|
||||
let purpose = format!(
|
||||
"provider-catalog-credential-bound-v2\0provider-id-bytes={}\0{provider_id}\0key-id-bytes={}\0{key_id}\0field={field}",
|
||||
provider_id.len(),
|
||||
key_id.len(),
|
||||
);
|
||||
let protected = format!("{purpose}\0{plaintext}");
|
||||
let ciphertext = encrypt_python_fernet_plaintext(DEVELOPMENT_ENCRYPTION_KEY, &protected)
|
||||
.expect("provider test credential should encrypt");
|
||||
format!("aether-provider-catalog-credential-v2:aether-runtime-secret-v1:{ciphertext}")
|
||||
}
|
||||
|
||||
let provider = StoredProviderCatalogProvider::new(
|
||||
provider_id.to_string(),
|
||||
format!("video-{provider_type}"),
|
||||
Some("https://example.com".to_string()),
|
||||
provider_type.to_string(),
|
||||
)
|
||||
.expect("provider should build")
|
||||
.with_transport_fields(
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
Some(2),
|
||||
None,
|
||||
Some(20.0),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let endpoint = StoredProviderCatalogEndpoint::new(
|
||||
endpoint_id.to_string(),
|
||||
provider_id.to_string(),
|
||||
api_format.to_string(),
|
||||
Some(provider_type.to_string()),
|
||||
Some("video".to_string()),
|
||||
true,
|
||||
)
|
||||
.expect("endpoint should build")
|
||||
.with_transport_fields(
|
||||
endpoint_base_url.to_string(),
|
||||
None,
|
||||
None,
|
||||
Some(2),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.expect("endpoint transport should build");
|
||||
let key = StoredProviderCatalogKey::new(
|
||||
key_id.to_string(),
|
||||
provider_id.to_string(),
|
||||
"prod".to_string(),
|
||||
"api_key".to_string(),
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.expect("key should build")
|
||||
.with_transport_fields(
|
||||
Some(serde_json::json!([api_format])),
|
||||
seal_bound_credential(provider_id, key_id, "api-key", upstream_api_key),
|
||||
None,
|
||||
None,
|
||||
Some(serde_json::json!({api_format: 1})),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.expect("key transport should build");
|
||||
|
||||
Arc::new(InMemoryProviderCatalogReadRepository::seed(
|
||||
vec![provider],
|
||||
vec![endpoint],
|
||||
vec![key],
|
||||
))
|
||||
}
|
||||
|
||||
@@ -28,7 +28,10 @@ use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::constants::TRACE_ID_HEADER;
|
||||
|
||||
use super::{build_router_with_state, build_state_with_execution_runtime_override, start_server};
|
||||
use super::{
|
||||
build_router_with_state, build_state_with_execution_runtime_override, start_server,
|
||||
video_provider_catalog_repository, video_proxy_node_repository,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_executes_openai_video_create_via_local_decision_gate_with_local_planning_only() {
|
||||
@@ -417,7 +420,10 @@ async fn gateway_executes_openai_video_create_via_local_decision_gate_with_local
|
||||
provider_catalog_repository,
|
||||
Arc::clone(&request_candidate_repository),
|
||||
DEVELOPMENT_ENCRYPTION_KEY,
|
||||
),
|
||||
)
|
||||
.attach_proxy_node_repository_for_tests(video_proxy_node_repository([
|
||||
"proxy-node-openai-video-local",
|
||||
])),
|
||||
);
|
||||
let gateway = build_router_with_state(gateway_state);
|
||||
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||
@@ -516,6 +522,39 @@ async fn gateway_executes_openai_video_remix_via_data_backed_local_follow_up_wit
|
||||
prompt: String,
|
||||
}
|
||||
|
||||
fn hash_api_key(value: &str) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(value.as_bytes());
|
||||
format!("{:x}", hasher.finalize())
|
||||
}
|
||||
|
||||
fn sample_auth_snapshot(api_key_id: &str, user_id: &str) -> StoredAuthApiKeySnapshot {
|
||||
StoredAuthApiKeySnapshot::new(
|
||||
user_id.to_string(),
|
||||
"video-user".to_string(),
|
||||
Some("[email protected]".to_string()),
|
||||
"user".to_string(),
|
||||
"local".to_string(),
|
||||
true,
|
||||
false,
|
||||
Some(json!(["openai"])),
|
||||
Some(json!(["openai:video"])),
|
||||
Some(json!(["sora-2"])),
|
||||
api_key_id.to_string(),
|
||||
Some("default".to_string()),
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
Some(60),
|
||||
Some(5),
|
||||
Some(4_102_444_800),
|
||||
Some(json!(["openai"])),
|
||||
Some(json!(["openai:video"])),
|
||||
Some(json!(["sora-2"])),
|
||||
)
|
||||
.expect("auth snapshot should build")
|
||||
}
|
||||
|
||||
let decision_hits = Arc::new(Mutex::new(0usize));
|
||||
let decision_hits_clone = Arc::clone(&decision_hits);
|
||||
let plan_hits = Arc::new(Mutex::new(0usize));
|
||||
@@ -738,21 +777,42 @@ async fn gateway_executes_openai_video_remix_via_data_backed_local_follow_up_wit
|
||||
.await
|
||||
.expect("upsert should succeed");
|
||||
|
||||
let auth_repository = Arc::new(InMemoryAuthApiKeySnapshotRepository::seed(vec![(
|
||||
Some(hash_api_key("client-openai-video-remix-local-key")),
|
||||
sample_auth_snapshot(
|
||||
"key-openai-video-remix-rotated-local-123",
|
||||
"user-openai-video-remix-local-123",
|
||||
),
|
||||
)]));
|
||||
let provider_catalog_repository = video_provider_catalog_repository(
|
||||
"provider-openai-video-local-1",
|
||||
"openai",
|
||||
"endpoint-openai-video-local-1",
|
||||
"openai:video",
|
||||
"https://api.openai.example/v1",
|
||||
"key-openai-video-local-1",
|
||||
"sk-upstream-openai-video",
|
||||
);
|
||||
|
||||
let (upstream_url, upstream_handle) = start_server(upstream).await;
|
||||
let (execution_runtime_url, execution_runtime_handle) = start_server(execution_runtime).await;
|
||||
let gateway_state = build_state_with_execution_runtime_override(execution_runtime_url)
|
||||
.with_data_state_for_tests(
|
||||
crate::data::GatewayDataState::with_video_task_and_request_candidate_repository_for_tests(
|
||||
repository,
|
||||
Arc::clone(&request_candidate_repository),
|
||||
),
|
||||
);
|
||||
crate::data::GatewayDataState::with_video_task_provider_transport_and_request_candidate_repository_for_tests(
|
||||
repository,
|
||||
provider_catalog_repository,
|
||||
Arc::clone(&request_candidate_repository),
|
||||
DEVELOPMENT_ENCRYPTION_KEY,
|
||||
)
|
||||
.with_auth_api_key_reader(auth_repository),
|
||||
);
|
||||
let gateway = build_router_with_state(gateway_state);
|
||||
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||
|
||||
let response = reqwest::Client::new()
|
||||
.post(format!("{gateway_url}/v1/videos/task-local-123/remix"))
|
||||
.header(http::header::CONTENT_TYPE, "application/json")
|
||||
.bearer_auth("client-openai-video-remix-local-key")
|
||||
.header(TRACE_ID_HEADER, "trace-openai-video-remix-local-123")
|
||||
.body("{\"prompt\":\"remix this\",\"model\":\"sora-2\"}")
|
||||
.send()
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
use aether_crypto::{encrypt_python_fernet_plaintext, DEVELOPMENT_ENCRYPTION_KEY};
|
||||
use aether_crypto::DEVELOPMENT_ENCRYPTION_KEY;
|
||||
use aether_data::repository::auth::{
|
||||
InMemoryAuthApiKeySnapshotRepository, StoredAuthApiKeySnapshot,
|
||||
};
|
||||
use aether_data::repository::candidates::InMemoryRequestCandidateRepository;
|
||||
use aether_data::repository::provider_catalog::InMemoryProviderCatalogReadRepository;
|
||||
use aether_data::repository::video_tasks::InMemoryVideoTaskRepository;
|
||||
use aether_data_contracts::repository::candidates::{
|
||||
RequestCandidateReadRepository, RequestCandidateStatus,
|
||||
};
|
||||
use aether_data_contracts::repository::provider_catalog::{
|
||||
StoredProviderCatalogEndpoint, StoredProviderCatalogKey, StoredProviderCatalogProvider,
|
||||
};
|
||||
use aether_data_contracts::repository::video_tasks::{
|
||||
UpsertVideoTask, VideoTaskStatus, VideoTaskWriteRepository,
|
||||
};
|
||||
@@ -18,13 +17,14 @@ use axum::{extract::Request, Json, Router};
|
||||
use http::header::{HeaderName, HeaderValue};
|
||||
use http::StatusCode;
|
||||
use serde_json::json;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::constants::{CONTROL_EXECUTED_HEADER, CONTROL_EXECUTE_FALLBACK_HEADER, TRACE_ID_HEADER};
|
||||
|
||||
use super::{
|
||||
build_router_with_state, build_state_with_execution_runtime_override, start_server,
|
||||
VideoTaskTruthSourceMode,
|
||||
video_provider_catalog_repository, VideoTaskTruthSourceMode,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
@@ -37,73 +37,37 @@ async fn gateway_executes_openai_video_delete_via_reconstructed_data_backed_loca
|
||||
authorization: String,
|
||||
}
|
||||
|
||||
fn sample_provider() -> StoredProviderCatalogProvider {
|
||||
StoredProviderCatalogProvider::new(
|
||||
"provider-openai-video-followup-1".to_string(),
|
||||
"openai".to_string(),
|
||||
Some("https://example.com".to_string()),
|
||||
"custom".to_string(),
|
||||
)
|
||||
.expect("provider should build")
|
||||
.with_transport_fields(
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
Some(2),
|
||||
None,
|
||||
Some(20.0),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
fn hash_api_key(value: &str) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(value.as_bytes());
|
||||
format!("{:x}", hasher.finalize())
|
||||
}
|
||||
|
||||
fn sample_endpoint() -> StoredProviderCatalogEndpoint {
|
||||
StoredProviderCatalogEndpoint::new(
|
||||
"endpoint-openai-video-followup-1".to_string(),
|
||||
"provider-openai-video-followup-1".to_string(),
|
||||
"openai:video".to_string(),
|
||||
Some("openai".to_string()),
|
||||
Some("video".to_string()),
|
||||
fn sample_auth_snapshot(api_key_id: &str, user_id: &str) -> StoredAuthApiKeySnapshot {
|
||||
StoredAuthApiKeySnapshot::new(
|
||||
user_id.to_string(),
|
||||
"video-user".to_string(),
|
||||
Some("[email protected]".to_string()),
|
||||
"user".to_string(),
|
||||
"local".to_string(),
|
||||
true,
|
||||
)
|
||||
.expect("endpoint should build")
|
||||
.with_transport_fields(
|
||||
"https://api.openai.example/v1".to_string(),
|
||||
None,
|
||||
None,
|
||||
Some(2),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.expect("endpoint transport should build")
|
||||
}
|
||||
|
||||
fn sample_key() -> StoredProviderCatalogKey {
|
||||
StoredProviderCatalogKey::new(
|
||||
"key-openai-video-followup-1".to_string(),
|
||||
"provider-openai-video-followup-1".to_string(),
|
||||
"prod".to_string(),
|
||||
"api_key".to_string(),
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.expect("key should build")
|
||||
.with_transport_fields(
|
||||
false,
|
||||
Some(json!(["openai"])),
|
||||
Some(json!(["openai:video"])),
|
||||
encrypt_python_fernet_plaintext(DEVELOPMENT_ENCRYPTION_KEY, "sk-upstream-openai-video")
|
||||
.expect("api key should encrypt"),
|
||||
None,
|
||||
None,
|
||||
Some(json!({"openai:video": 1})),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(json!(["sora-2"])),
|
||||
api_key_id.to_string(),
|
||||
Some("default".to_string()),
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
Some(60),
|
||||
Some(5),
|
||||
Some(4_102_444_800),
|
||||
Some(json!(["openai"])),
|
||||
Some(json!(["openai:video"])),
|
||||
Some(json!(["sora-2"])),
|
||||
)
|
||||
.expect("key transport should build")
|
||||
.expect("auth snapshot should build")
|
||||
}
|
||||
|
||||
let decision_hits = Arc::new(Mutex::new(0usize));
|
||||
@@ -116,7 +80,6 @@ async fn gateway_executes_openai_video_delete_via_reconstructed_data_backed_loca
|
||||
let report_hits_clone = Arc::clone(&report_hits);
|
||||
let seen_execution_runtime = Arc::new(Mutex::new(None::<SeenExecutionRuntimeSyncRequest>));
|
||||
let seen_execution_runtime_clone = Arc::clone(&seen_execution_runtime);
|
||||
|
||||
let upstream = Router::new()
|
||||
.route(
|
||||
"/api/internal/gateway/resolve",
|
||||
@@ -128,11 +91,6 @@ async fn gateway_executes_openai_video_delete_via_reconstructed_data_backed_loca
|
||||
"route_kind": "video",
|
||||
"auth_endpoint_signature": "openai:video",
|
||||
"execution_runtime_candidate": true,
|
||||
"auth_context": {
|
||||
"user_id": "user-openai-video-delete-local-123",
|
||||
"api_key_id": "key-openai-video-delete-local-123",
|
||||
"access_allowed": true
|
||||
},
|
||||
"public_path": "/v1/videos/task-local-followup-123"
|
||||
}))
|
||||
}),
|
||||
@@ -279,12 +237,32 @@ async fn gateway_executes_openai_video_delete_via_reconstructed_data_backed_loca
|
||||
})
|
||||
.await
|
||||
.expect("upsert should succeed");
|
||||
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
|
||||
vec![sample_provider()],
|
||||
vec![sample_endpoint()],
|
||||
vec![sample_key()],
|
||||
));
|
||||
let provider_catalog_repository = video_provider_catalog_repository(
|
||||
"provider-openai-video-followup-1",
|
||||
"openai",
|
||||
"endpoint-openai-video-followup-1",
|
||||
"openai:video",
|
||||
"https://api.openai.example/v1",
|
||||
"key-openai-video-followup-1",
|
||||
"sk-upstream-openai-video",
|
||||
);
|
||||
let request_candidate_repository = Arc::new(InMemoryRequestCandidateRepository::default());
|
||||
let auth_repository = Arc::new(InMemoryAuthApiKeySnapshotRepository::seed(vec![
|
||||
(
|
||||
Some(hash_api_key("client-video-delete-foreign-key")),
|
||||
sample_auth_snapshot(
|
||||
"key-openai-video-delete-foreign-123",
|
||||
"user-openai-video-delete-foreign-123",
|
||||
),
|
||||
),
|
||||
(
|
||||
Some(hash_api_key("client-video-delete-owner-key")),
|
||||
sample_auth_snapshot(
|
||||
"key-openai-video-delete-rotated-local-123",
|
||||
"user-openai-video-delete-local-123",
|
||||
),
|
||||
),
|
||||
]));
|
||||
|
||||
let gateway = build_router_with_state(
|
||||
build_state_with_execution_runtime_override(execution_runtime_url)
|
||||
@@ -295,14 +273,44 @@ async fn gateway_executes_openai_video_delete_via_reconstructed_data_backed_loca
|
||||
provider_catalog_repository,
|
||||
Arc::clone(&request_candidate_repository),
|
||||
DEVELOPMENT_ENCRYPTION_KEY,
|
||||
),
|
||||
)
|
||||
.with_auth_api_key_reader(auth_repository),
|
||||
),
|
||||
);
|
||||
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||
|
||||
let response = reqwest::Client::new()
|
||||
let client = reqwest::Client::new();
|
||||
let foreign_response = client
|
||||
.delete(format!("{gateway_url}/v1/videos/task-local-followup-123"))
|
||||
.header(CONTROL_EXECUTE_FALLBACK_HEADER, "true")
|
||||
.bearer_auth("client-video-delete-foreign-key")
|
||||
.header(
|
||||
TRACE_ID_HEADER,
|
||||
"trace-openai-video-delete-foreign-local-123",
|
||||
)
|
||||
.send()
|
||||
.await
|
||||
.expect("foreign delete request should complete");
|
||||
let foreign_status = foreign_response.status();
|
||||
let foreign_body = foreign_response.text().await.expect("body should read");
|
||||
assert_eq!(
|
||||
foreign_status,
|
||||
StatusCode::NOT_FOUND,
|
||||
"unexpected foreign response body: {foreign_body}"
|
||||
);
|
||||
assert!(seen_execution_runtime
|
||||
.lock()
|
||||
.expect("mutex should lock")
|
||||
.is_none());
|
||||
assert_eq!(*decision_hits.lock().expect("mutex should lock"), 0);
|
||||
assert_eq!(*execute_hits.lock().expect("mutex should lock"), 0);
|
||||
assert_eq!(*report_hits.lock().expect("mutex should lock"), 0);
|
||||
assert_eq!(*public_hits.lock().expect("mutex should lock"), 0);
|
||||
|
||||
let response = client
|
||||
.delete(format!("{gateway_url}/v1/videos/task-local-followup-123"))
|
||||
.header(CONTROL_EXECUTE_FALLBACK_HEADER, "true")
|
||||
.bearer_auth("client-video-delete-owner-key")
|
||||
.header(TRACE_ID_HEADER, "trace-openai-video-delete-local-123")
|
||||
.send()
|
||||
.await
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use aether_crypto::DEVELOPMENT_ENCRYPTION_KEY;
|
||||
use aether_data::repository::video_tasks::InMemoryVideoTaskRepository;
|
||||
use aether_data_contracts::repository::video_tasks::{
|
||||
UpsertVideoTask, VideoTaskLookupKey, VideoTaskReadRepository, VideoTaskStatus,
|
||||
@@ -11,7 +12,8 @@ use axum::{extract::Request, Json, Router};
|
||||
use serde_json::json;
|
||||
|
||||
use super::{
|
||||
build_state_with_execution_runtime_override, start_server, AppState, VideoTaskTruthSourceMode,
|
||||
build_state_with_execution_runtime_override, start_server, video_provider_catalog_repository,
|
||||
AppState, VideoTaskTruthSourceMode,
|
||||
};
|
||||
|
||||
fn sample_due_openai_task(upstream_base_url: &str) -> UpsertVideoTask {
|
||||
@@ -168,9 +170,24 @@ async fn gateway_background_video_task_poller_refreshes_due_openai_task_from_rep
|
||||
.upsert(sample_due_openai_task("https://api.openai.example/v1"))
|
||||
.await
|
||||
.expect("task upsert should succeed");
|
||||
let provider_catalog_repository = video_provider_catalog_repository(
|
||||
"provider-openai-video-local-1",
|
||||
"openai",
|
||||
"endpoint-openai-video-local-1",
|
||||
"openai:video",
|
||||
"https://api.openai.example/v1",
|
||||
"key-openai-video-local-1",
|
||||
"sk-upstream-openai-video",
|
||||
);
|
||||
|
||||
let gateway_state = build_state_with_execution_runtime_override(execution_runtime_url)
|
||||
.with_video_task_data_repository_for_tests(Arc::clone(&repository))
|
||||
.with_data_state_for_tests(
|
||||
crate::data::GatewayDataState::with_video_task_repository_and_provider_transport_for_tests(
|
||||
Arc::clone(&repository),
|
||||
provider_catalog_repository,
|
||||
DEVELOPMENT_ENCRYPTION_KEY,
|
||||
),
|
||||
)
|
||||
.with_video_task_truth_source_mode(VideoTaskTruthSourceMode::RustAuthoritative)
|
||||
.with_video_task_poller_config(std::time::Duration::from_millis(25), 8);
|
||||
let background_tasks = gateway_state.spawn_background_tasks();
|
||||
@@ -202,23 +219,10 @@ async fn gateway_background_video_task_poller_refreshes_due_openai_task_from_rep
|
||||
stored.next_poll_at_unix_secs.is_some_and(|value| value > 0),
|
||||
"poller should push next poll into the future"
|
||||
);
|
||||
assert_eq!(
|
||||
stored
|
||||
.request_metadata
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("rust_owner"))
|
||||
.and_then(serde_json::Value::as_str),
|
||||
Some("async_task")
|
||||
);
|
||||
assert_eq!(
|
||||
stored
|
||||
.request_metadata
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("poll_raw_response"))
|
||||
.and_then(|value| value.get("status"))
|
||||
.and_then(serde_json::Value::as_str),
|
||||
Some("processing")
|
||||
);
|
||||
assert!(stored.original_request_body.is_none());
|
||||
assert!(stored.progress_message.is_none());
|
||||
assert!(stored.error_message.is_none());
|
||||
assert!(stored.request_metadata.is_none());
|
||||
|
||||
assert_eq!(
|
||||
seen_execution_runtime_requests
|
||||
@@ -274,10 +278,25 @@ async fn gateway_background_video_task_poller_refreshes_due_openai_task_from_rep
|
||||
.upsert(sample_due_openai_task(&upstream_api_root))
|
||||
.await
|
||||
.expect("task upsert should succeed");
|
||||
let provider_catalog_repository = video_provider_catalog_repository(
|
||||
"provider-openai-video-local-1",
|
||||
"openai",
|
||||
"endpoint-openai-video-local-1",
|
||||
"openai:video",
|
||||
&upstream_api_root,
|
||||
"key-openai-video-local-1",
|
||||
"sk-upstream-openai-video",
|
||||
);
|
||||
|
||||
let gateway_state = AppState::new()
|
||||
.expect("gateway state should build")
|
||||
.with_video_task_data_repository_for_tests(Arc::clone(&repository))
|
||||
.with_data_state_for_tests(
|
||||
crate::data::GatewayDataState::with_video_task_repository_and_provider_transport_for_tests(
|
||||
Arc::clone(&repository),
|
||||
provider_catalog_repository,
|
||||
DEVELOPMENT_ENCRYPTION_KEY,
|
||||
),
|
||||
)
|
||||
.with_video_task_truth_source_mode(VideoTaskTruthSourceMode::RustAuthoritative)
|
||||
.with_video_task_poller_config(std::time::Duration::from_millis(25), 8);
|
||||
let background_tasks = gateway_state.spawn_background_tasks();
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use aether_contracts::{StreamFrame, StreamFramePayload, StreamFrameType};
|
||||
use aether_crypto::{encrypt_python_fernet_plaintext, DEVELOPMENT_ENCRYPTION_KEY};
|
||||
use aether_data::repository::provider_catalog::InMemoryProviderCatalogReadRepository;
|
||||
use aether_data::repository::video_tasks::InMemoryVideoTaskRepository;
|
||||
use aether_data_contracts::repository::provider_catalog::{
|
||||
StoredProviderCatalogEndpoint, StoredProviderCatalogKey, StoredProviderCatalogProvider,
|
||||
use aether_crypto::DEVELOPMENT_ENCRYPTION_KEY;
|
||||
use aether_data::repository::auth::{
|
||||
InMemoryAuthApiKeySnapshotRepository, StoredAuthApiKeySnapshot,
|
||||
};
|
||||
use aether_data::repository::video_tasks::InMemoryVideoTaskRepository;
|
||||
use aether_data_contracts::repository::video_tasks::{
|
||||
UpsertVideoTask, VideoTaskStatus, VideoTaskWriteRepository,
|
||||
};
|
||||
@@ -16,13 +15,14 @@ use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
|
||||
use http::header::{HeaderName, HeaderValue};
|
||||
use http::StatusCode;
|
||||
use serde_json::json;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::constants::{CONTROL_EXECUTED_HEADER, CONTROL_EXECUTE_FALLBACK_HEADER, TRACE_ID_HEADER};
|
||||
|
||||
use super::{
|
||||
build_router_with_state, build_state_with_execution_runtime_override, start_server,
|
||||
VideoTaskTruthSourceMode,
|
||||
video_provider_catalog_repository, VideoTaskTruthSourceMode,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
@@ -34,73 +34,37 @@ async fn gateway_executes_openai_video_content_from_reconstructed_data_task_with
|
||||
url: String,
|
||||
}
|
||||
|
||||
fn sample_provider() -> StoredProviderCatalogProvider {
|
||||
StoredProviderCatalogProvider::new(
|
||||
"provider-openai-video-content-followup-1".to_string(),
|
||||
"openai".to_string(),
|
||||
Some("https://example.com".to_string()),
|
||||
"custom".to_string(),
|
||||
)
|
||||
.expect("provider should build")
|
||||
.with_transport_fields(
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
Some(2),
|
||||
None,
|
||||
Some(20.0),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
fn hash_api_key(value: &str) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(value.as_bytes());
|
||||
format!("{:x}", hasher.finalize())
|
||||
}
|
||||
|
||||
fn sample_endpoint() -> StoredProviderCatalogEndpoint {
|
||||
StoredProviderCatalogEndpoint::new(
|
||||
"endpoint-openai-video-content-followup-1".to_string(),
|
||||
"provider-openai-video-content-followup-1".to_string(),
|
||||
"openai:video".to_string(),
|
||||
Some("openai".to_string()),
|
||||
Some("video".to_string()),
|
||||
fn sample_auth_snapshot(api_key_id: &str, user_id: &str) -> StoredAuthApiKeySnapshot {
|
||||
StoredAuthApiKeySnapshot::new(
|
||||
user_id.to_string(),
|
||||
"video-user".to_string(),
|
||||
Some("[email protected]".to_string()),
|
||||
"user".to_string(),
|
||||
"local".to_string(),
|
||||
true,
|
||||
)
|
||||
.expect("endpoint should build")
|
||||
.with_transport_fields(
|
||||
"https://api.openai.example/v1".to_string(),
|
||||
None,
|
||||
None,
|
||||
Some(2),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.expect("endpoint transport should build")
|
||||
}
|
||||
|
||||
fn sample_key() -> StoredProviderCatalogKey {
|
||||
StoredProviderCatalogKey::new(
|
||||
"key-openai-video-content-followup-1".to_string(),
|
||||
"provider-openai-video-content-followup-1".to_string(),
|
||||
"prod".to_string(),
|
||||
"api_key".to_string(),
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.expect("key should build")
|
||||
.with_transport_fields(
|
||||
false,
|
||||
Some(json!(["openai"])),
|
||||
Some(json!(["openai:video"])),
|
||||
encrypt_python_fernet_plaintext(DEVELOPMENT_ENCRYPTION_KEY, "sk-upstream-openai-video")
|
||||
.expect("api key should encrypt"),
|
||||
None,
|
||||
None,
|
||||
Some(json!({"openai:video": 1})),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(json!(["sora-2"])),
|
||||
api_key_id.to_string(),
|
||||
Some("default".to_string()),
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
Some(60),
|
||||
Some(5),
|
||||
Some(4_102_444_800),
|
||||
Some(json!(["openai"])),
|
||||
Some(json!(["openai:video"])),
|
||||
Some(json!(["sora-2"])),
|
||||
)
|
||||
.expect("key transport should build")
|
||||
.expect("auth snapshot should build")
|
||||
}
|
||||
|
||||
let decision_stream_hits = Arc::new(Mutex::new(0usize));
|
||||
@@ -112,7 +76,6 @@ async fn gateway_executes_openai_video_content_from_reconstructed_data_task_with
|
||||
let seen_execution_runtime_stream =
|
||||
Arc::new(Mutex::new(None::<SeenExecutionRuntimeStreamRequest>));
|
||||
let seen_execution_runtime_stream_clone = Arc::clone(&seen_execution_runtime_stream);
|
||||
|
||||
let upstream = Router::new()
|
||||
.route(
|
||||
"/api/internal/gateway/resolve",
|
||||
@@ -124,11 +87,6 @@ async fn gateway_executes_openai_video_content_from_reconstructed_data_task_with
|
||||
"route_kind": "video",
|
||||
"auth_endpoint_signature": "openai:video",
|
||||
"execution_runtime_candidate": true,
|
||||
"auth_context": {
|
||||
"user_id": "user-video-content-local-123",
|
||||
"api_key_id": "key-video-content-local-123",
|
||||
"access_allowed": true
|
||||
},
|
||||
"public_path": request.uri().path()
|
||||
}))
|
||||
}),
|
||||
@@ -299,28 +257,81 @@ async fn gateway_executes_openai_video_content_from_reconstructed_data_task_with
|
||||
})
|
||||
.await
|
||||
.expect("upsert should succeed");
|
||||
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
|
||||
vec![sample_provider()],
|
||||
vec![sample_endpoint()],
|
||||
vec![sample_key()],
|
||||
));
|
||||
let provider_catalog_repository = video_provider_catalog_repository(
|
||||
"provider-openai-video-content-followup-1",
|
||||
"openai",
|
||||
"endpoint-openai-video-content-followup-1",
|
||||
"openai:video",
|
||||
"https://api.openai.example/v1",
|
||||
"key-openai-video-content-followup-1",
|
||||
"sk-upstream-openai-video",
|
||||
);
|
||||
let auth_repository = Arc::new(InMemoryAuthApiKeySnapshotRepository::seed(vec![
|
||||
(
|
||||
Some(hash_api_key("client-video-content-foreign-key")),
|
||||
sample_auth_snapshot(
|
||||
"key-video-content-foreign-123",
|
||||
"user-video-content-foreign-123",
|
||||
),
|
||||
),
|
||||
(
|
||||
Some(hash_api_key("client-video-content-owner-key")),
|
||||
sample_auth_snapshot(
|
||||
"key-video-content-local-rotated-123",
|
||||
"user-video-content-local-123",
|
||||
),
|
||||
),
|
||||
]));
|
||||
|
||||
let gateway = build_router_with_state(
|
||||
build_state_with_execution_runtime_override(execution_runtime_url)
|
||||
.with_video_task_truth_source_mode(VideoTaskTruthSourceMode::RustAuthoritative)
|
||||
.with_video_task_repository_and_provider_transport_for_tests(
|
||||
repository,
|
||||
provider_catalog_repository,
|
||||
DEVELOPMENT_ENCRYPTION_KEY,
|
||||
),
|
||||
.with_data_state_for_tests(
|
||||
crate::data::GatewayDataState::with_video_task_repository_and_provider_transport_for_tests(
|
||||
repository,
|
||||
provider_catalog_repository,
|
||||
DEVELOPMENT_ENCRYPTION_KEY,
|
||||
)
|
||||
.with_auth_api_key_reader(auth_repository),
|
||||
)
|
||||
);
|
||||
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||
|
||||
let response = reqwest::Client::new()
|
||||
let client = reqwest::Client::new();
|
||||
let foreign_response = client
|
||||
.get(format!(
|
||||
"{gateway_url}/v1/videos/task-content-local-123/content?variant=video"
|
||||
))
|
||||
.header(CONTROL_EXECUTE_FALLBACK_HEADER, "true")
|
||||
.bearer_auth("client-video-content-foreign-key")
|
||||
.header(
|
||||
TRACE_ID_HEADER,
|
||||
"trace-openai-video-content-foreign-local-123",
|
||||
)
|
||||
.send()
|
||||
.await
|
||||
.expect("foreign content request should complete");
|
||||
let foreign_status = foreign_response.status();
|
||||
let foreign_body = foreign_response.text().await.expect("body should read");
|
||||
assert_eq!(
|
||||
foreign_status,
|
||||
StatusCode::NOT_FOUND,
|
||||
"unexpected foreign response body: {foreign_body}"
|
||||
);
|
||||
assert!(seen_execution_runtime_stream
|
||||
.lock()
|
||||
.expect("mutex should lock")
|
||||
.is_none());
|
||||
assert_eq!(*decision_stream_hits.lock().expect("mutex should lock"), 0);
|
||||
assert_eq!(*execute_stream_hits.lock().expect("mutex should lock"), 0);
|
||||
assert_eq!(*public_hits.lock().expect("mutex should lock"), 0);
|
||||
|
||||
let response = client
|
||||
.get(format!(
|
||||
"{gateway_url}/v1/videos/task-content-local-123/content?variant=video"
|
||||
))
|
||||
.header(CONTROL_EXECUTE_FALLBACK_HEADER, "true")
|
||||
.bearer_auth("client-video-content-owner-key")
|
||||
.header(TRACE_ID_HEADER, "trace-openai-video-content-local-123")
|
||||
.send()
|
||||
.await
|
||||
@@ -347,7 +358,7 @@ async fn gateway_executes_openai_video_content_from_reconstructed_data_task_with
|
||||
assert_eq!(seen_stream_request.method, "GET");
|
||||
assert_eq!(
|
||||
seen_stream_request.url,
|
||||
"https://cdn.example.com/video-content.mp4"
|
||||
"https://api.openai.example/v1/videos/ext-video-content-followup-123/content"
|
||||
);
|
||||
assert_eq!(*decision_stream_hits.lock().expect("mutex should lock"), 0);
|
||||
assert_eq!(*execute_stream_hits.lock().expect("mutex should lock"), 0);
|
||||
|
||||
Reference in New Issue
Block a user