feat: complete Epic 1 production foundation
This commit is contained in:
@@ -10,12 +10,12 @@ use std::{
|
||||
use async_trait::async_trait;
|
||||
use axum::{Json, Router, routing::post};
|
||||
use crank_core::{
|
||||
ExecutionConfig, HttpMethod, MembershipRole, OperationSecurityLevel, Protocol,
|
||||
AuditSink, ExecutionConfig, HttpMethod, MembershipRole, OperationSecurityLevel, Protocol,
|
||||
ResponseCachePolicy, RestTarget, SecretKind, Target, ToolDescription, WorkspaceId,
|
||||
};
|
||||
use crank_core::{IdentityError, IdentityProvider, IdentityProviderKind, LoginOutcome};
|
||||
use crank_mapping::{MappingRule, MappingSet};
|
||||
use crank_registry::PostgresRegistry;
|
||||
use crank_registry::{MASTER_KEY_CIPHER_CONTRACT, MasterKeyIdentityCandidate, PostgresRegistry};
|
||||
use crank_runtime::SecretCrypto;
|
||||
use crank_schema::{Schema, SchemaKind};
|
||||
use serde_json::{Value, json};
|
||||
@@ -34,7 +34,7 @@ const TEST_AUTH_EMAIL: &str = "owner@crank.local";
|
||||
const TEST_AUTH_PASSWORD: &str = "test-password";
|
||||
const TEST_PASSWORD_PEPPER: &str = "test-password-pepper";
|
||||
const TEST_SESSION_SECRET: &str = "test-session-secret";
|
||||
const TEST_MASTER_KEY: &str = "test-master-key";
|
||||
const TEST_MASTER_KEY: &str = "test-master-key-00000000000000000000000000000000";
|
||||
|
||||
pub(super) struct TestServer {
|
||||
base_url: String,
|
||||
@@ -104,7 +104,32 @@ pub(super) fn build_test_app(
|
||||
api_rate_limiter: crank_runtime::RequestRateLimiter::new(
|
||||
crank_runtime::RequestRateLimitConfig::new(10_000, 10_000).unwrap(),
|
||||
),
|
||||
trust_forwarded_headers: false,
|
||||
trusted_proxy_ips: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn build_test_app_with_audit_sink(
|
||||
registry: PostgresRegistry,
|
||||
storage_root: std::path::PathBuf,
|
||||
audit_sink: Arc<dyn AuditSink>,
|
||||
) -> Router {
|
||||
let outbound_policy = crank_runtime::OutboundHttpPolicy::allowing_hosts(["127.0.0.1"]);
|
||||
let runtime = crank_runtime::community_with_outbound_policy(outbound_policy.clone()).build();
|
||||
build_app(AppState {
|
||||
service: AdminServiceBuilder::new(
|
||||
registry,
|
||||
storage_root,
|
||||
test_auth_settings(),
|
||||
test_secret_crypto(),
|
||||
runtime,
|
||||
)
|
||||
.with_outbound_http_policy(outbound_policy)
|
||||
.with_audit_sink(audit_sink)
|
||||
.build(),
|
||||
api_rate_limiter: crank_runtime::RequestRateLimiter::new(
|
||||
crank_runtime::RequestRateLimitConfig::new(10_000, 10_000).unwrap(),
|
||||
),
|
||||
trusted_proxy_ips: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -169,12 +194,13 @@ pub(super) async fn authorized_client(workspace_base_url: impl AsRef<str>) -> re
|
||||
.split("/api/admin/workspaces/")
|
||||
.next()
|
||||
.unwrap();
|
||||
let client = reqwest::Client::builder()
|
||||
.cookie_store(true)
|
||||
let cookie_jar = Arc::new(reqwest::cookie::Jar::default());
|
||||
let login_client = reqwest::Client::builder()
|
||||
.cookie_provider(cookie_jar.clone())
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
client
|
||||
let login = login_client
|
||||
.post(format!("{root_url}/api/auth/login"))
|
||||
.json(&json!({
|
||||
"email": TEST_AUTH_EMAIL,
|
||||
@@ -184,9 +210,67 @@ pub(super) async fn authorized_client(workspace_base_url: impl AsRef<str>) -> re
|
||||
.await
|
||||
.unwrap()
|
||||
.error_for_status()
|
||||
.unwrap()
|
||||
.json::<Value>()
|
||||
.await
|
||||
.unwrap();
|
||||
let csrf_token = login["csrf_token"]
|
||||
.as_str()
|
||||
.expect("login response must include csrf_token");
|
||||
let mut default_headers = reqwest::header::HeaderMap::new();
|
||||
default_headers.insert(
|
||||
"x-csrf-token",
|
||||
reqwest::header::HeaderValue::from_str(csrf_token).unwrap(),
|
||||
);
|
||||
|
||||
client
|
||||
reqwest::Client::builder()
|
||||
.cookie_provider(cookie_jar)
|
||||
.default_headers(default_headers)
|
||||
.build()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub(super) async fn operation_etag(
|
||||
client: &reqwest::Client,
|
||||
workspace_base_url: impl AsRef<str>,
|
||||
operation_id: &str,
|
||||
) -> String {
|
||||
let response = client
|
||||
.get(format!(
|
||||
"{}/operations/{operation_id}",
|
||||
workspace_base_url.as_ref()
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), reqwest::StatusCode::OK);
|
||||
response
|
||||
.headers()
|
||||
.get(reqwest::header::ETAG)
|
||||
.expect("operation detail must expose an ETag")
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
pub(super) async fn agent_etag(
|
||||
client: &reqwest::Client,
|
||||
workspace_base_url: impl AsRef<str>,
|
||||
agent_id: &str,
|
||||
) -> String {
|
||||
let response = client
|
||||
.get(format!("{}/agents/{agent_id}", workspace_base_url.as_ref()))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), reqwest::StatusCode::OK);
|
||||
response
|
||||
.headers()
|
||||
.get(reqwest::header::ETAG)
|
||||
.expect("agent detail must expose an ETag")
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
pub(super) async fn assert_success_json(response: reqwest::Response) -> Value {
|
||||
@@ -216,6 +300,16 @@ pub(super) async fn test_registry() -> PostgresRegistry {
|
||||
.await
|
||||
.unwrap();
|
||||
let registry = PostgresRegistry::connect(&database_url).await.unwrap();
|
||||
let secret_crypto = test_secret_crypto();
|
||||
registry
|
||||
.verify_or_register_master_key_identity(MasterKeyIdentityCandidate {
|
||||
epoch: secret_crypto.master_key_epoch(),
|
||||
fingerprint: secret_crypto.master_key_fingerprint(),
|
||||
cipher_contract: MASTER_KEY_CIPHER_CONTRACT,
|
||||
observed_at: &time::OffsetDateTime::now_utc(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let password_hash = hash_password(TEST_AUTH_PASSWORD, TEST_PASSWORD_PEPPER).unwrap();
|
||||
let user_id = registry
|
||||
.upsert_bootstrap_user(TEST_AUTH_EMAIL, "Test Owner", &password_hash)
|
||||
|
||||
Reference in New Issue
Block a user