Files
wifi-densepose/examples/vibecast-7sense/tests/integration/api_test.rs
ruv d803bfe2b1 Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector
git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
2026-02-28 14:39:40 -05:00

754 lines
23 KiB
Rust

//! Integration tests for API Context
//!
//! Tests for REST endpoints, GraphQL queries/mutations, rate limiting,
//! and error responses.
use vibecast_tests::fixtures::*;
use vibecast_tests::mocks::*;
use std::collections::HashMap;
use std::time::{Duration, Instant};
// ============================================================================
// REST Endpoint Tests
// ============================================================================
mod rest_endpoints {
use super::*;
// Mock API paths
const RECORDINGS_PATH: &str = "/api/v1/recordings";
const SEGMENTS_PATH: &str = "/api/v1/segments";
const EMBEDDINGS_PATH: &str = "/api/v1/embeddings";
const CLUSTERS_PATH: &str = "/api/v1/clusters";
const INTERPRETATIONS_PATH: &str = "/api/v1/interpretations";
const SEARCH_PATH: &str = "/api/v1/search";
const HEALTH_PATH: &str = "/api/v1/health";
#[test]
fn test_recordings_list_endpoint() {
let client = MockApiClient::new();
client.queue_response(
200,
r#"{"recordings": [{"id": "uuid1", "duration_ms": 60000}]}"#,
);
let response = client.get(RECORDINGS_PATH).unwrap();
assert_eq!(response.status, 200);
assert!(response.body.contains("recordings"));
}
#[test]
fn test_recordings_create_endpoint() {
let client = MockApiClient::new();
client.queue_response(
201,
r#"{"id": "new-uuid", "status": "created"}"#,
);
let body = r#"{"source": "upload", "metadata": {}}"#;
let response = client.post(RECORDINGS_PATH, body).unwrap();
assert_eq!(response.status, 201);
assert!(response.body.contains("id"));
}
#[test]
fn test_segments_by_recording_endpoint() {
let client = MockApiClient::new();
client.queue_response(
200,
r#"{"segments": [{"id": "seg1", "start_ms": 0, "end_ms": 5000}]}"#,
);
let path = format!("{}/recording123/segments", RECORDINGS_PATH);
let response = client.get(&path).unwrap();
assert_eq!(response.status, 200);
assert!(response.body.contains("segments"));
}
#[test]
fn test_embedding_generation_endpoint() {
let client = MockApiClient::new();
client.queue_response(
202,
r#"{"job_id": "job123", "status": "processing"}"#,
);
let body = r#"{"segment_ids": ["seg1", "seg2"], "model": "perch2"}"#;
let response = client.post(EMBEDDINGS_PATH, body).unwrap();
assert_eq!(response.status, 202); // Accepted for async processing
assert!(response.body.contains("job_id"));
}
#[test]
fn test_similarity_search_endpoint() {
let client = MockApiClient::new();
client.queue_response(
200,
r#"{"results": [{"segment_id": "seg1", "distance": 0.1}], "count": 1}"#,
);
let body = r#"{"query_segment_id": "query1", "k": 10}"#;
let response = client.post(SEARCH_PATH, body).unwrap();
assert_eq!(response.status, 200);
assert!(response.body.contains("results"));
}
#[test]
fn test_interpretation_endpoint() {
let client = MockApiClient::new();
client.queue_response(
200,
r#"{"interpretation": {"statements": ["Similar to alarm calls"], "confidence": 0.85}}"#,
);
let body = r#"{"segment_id": "seg1", "include_citations": true}"#;
let response = client.post(INTERPRETATIONS_PATH, body).unwrap();
assert_eq!(response.status, 200);
assert!(response.body.contains("interpretation"));
assert!(response.body.contains("confidence"));
}
#[test]
fn test_health_check_endpoint() {
let client = MockApiClient::new();
client.queue_response(
200,
r#"{"status": "healthy", "version": "1.0.0", "components": {"database": "ok", "index": "ok"}}"#,
);
let response = client.get(HEALTH_PATH).unwrap();
assert_eq!(response.status, 200);
assert!(response.body.contains("healthy"));
}
#[test]
fn test_cluster_list_endpoint() {
let client = MockApiClient::new();
client.queue_response(
200,
r#"{"clusters": [{"id": "c1", "member_count": 50}], "total": 1}"#,
);
let response = client.get(CLUSTERS_PATH).unwrap();
assert_eq!(response.status, 200);
assert!(response.body.contains("clusters"));
}
}
// ============================================================================
// GraphQL Tests
// ============================================================================
mod graphql {
use super::*;
const GRAPHQL_PATH: &str = "/graphql";
fn create_graphql_query(query: &str) -> String {
format!(r#"{{"query": "{}"}}"#, query.replace('"', "\\\""))
}
#[test]
fn test_graphql_recordings_query() {
let client = MockApiClient::new();
client.queue_response(
200,
r#"{"data": {"recordings": [{"id": "rec1", "duration_ms": 60000}]}}"#,
);
let query = create_graphql_query("{ recordings { id duration_ms } }");
let response = client.post(GRAPHQL_PATH, &query).unwrap();
assert_eq!(response.status, 200);
assert!(response.body.contains("data"));
assert!(response.body.contains("recordings"));
}
#[test]
fn test_graphql_recording_with_segments() {
let client = MockApiClient::new();
client.queue_response(
200,
r#"{"data": {"recording": {"id": "rec1", "segments": [{"id": "seg1"}]}}}"#,
);
let query = create_graphql_query(
"{ recording(id: \\\"rec1\\\") { id segments { id } } }",
);
let response = client.post(GRAPHQL_PATH, &query).unwrap();
assert_eq!(response.status, 200);
assert!(response.body.contains("segments"));
}
#[test]
fn test_graphql_segment_with_embedding() {
let client = MockApiClient::new();
client.queue_response(
200,
r#"{"data": {"segment": {"id": "seg1", "embedding": {"id": "emb1", "norm": 1.0}}}}"#,
);
let query = create_graphql_query(
"{ segment(id: \\\"seg1\\\") { id embedding { id norm } } }",
);
let response = client.post(GRAPHQL_PATH, &query).unwrap();
assert_eq!(response.status, 200);
assert!(response.body.contains("embedding"));
}
#[test]
fn test_graphql_similarity_search() {
let client = MockApiClient::new();
client.queue_response(
200,
r#"{"data": {"similarSegments": [{"segment": {"id": "s1"}, "distance": 0.1}]}}"#,
);
let query = create_graphql_query(
"{ similarSegments(segmentId: \\\"seg1\\\", k: 10) { segment { id } distance } }",
);
let response = client.post(GRAPHQL_PATH, &query).unwrap();
assert_eq!(response.status, 200);
assert!(response.body.contains("similarSegments"));
}
#[test]
fn test_graphql_create_recording_mutation() {
let client = MockApiClient::new();
client.queue_response(
200,
r#"{"data": {"createRecording": {"id": "new-rec", "status": "INGESTED"}}}"#,
);
let mutation = create_graphql_query(
"mutation { createRecording(input: {source: \\\"upload\\\"}) { id status } }",
);
let response = client.post(GRAPHQL_PATH, &mutation).unwrap();
assert_eq!(response.status, 200);
assert!(response.body.contains("createRecording"));
}
#[test]
fn test_graphql_generate_embeddings_mutation() {
let client = MockApiClient::new();
client.queue_response(
200,
r#"{"data": {"generateEmbeddings": {"jobId": "job1", "status": "PROCESSING"}}}"#,
);
let mutation = create_graphql_query(
"mutation { generateEmbeddings(segmentIds: [\\\"s1\\\", \\\"s2\\\"]) { jobId status } }",
);
let response = client.post(GRAPHQL_PATH, &mutation).unwrap();
assert_eq!(response.status, 200);
assert!(response.body.contains("generateEmbeddings"));
}
#[test]
fn test_graphql_run_clustering_mutation() {
let client = MockApiClient::new();
client.queue_response(
200,
r#"{"data": {"runClustering": {"sessionId": "sess1", "clusterCount": 15}}}"#,
);
let mutation = create_graphql_query(
"mutation { runClustering(method: HDBSCAN, params: {minClusterSize: 5}) { sessionId clusterCount } }",
);
let response = client.post(GRAPHQL_PATH, &mutation).unwrap();
assert_eq!(response.status, 200);
assert!(response.body.contains("runClustering"));
}
#[test]
fn test_graphql_error_response() {
let client = MockApiClient::new();
client.queue_response(
200,
r#"{"data": null, "errors": [{"message": "Segment not found", "path": ["segment"]}]}"#,
);
let query = create_graphql_query("{ segment(id: \\\"nonexistent\\\") { id } }");
let response = client.post(GRAPHQL_PATH, &query).unwrap();
assert_eq!(response.status, 200); // GraphQL returns 200 even for errors
assert!(response.body.contains("errors"));
}
#[test]
fn test_graphql_nested_query() {
let client = MockApiClient::new();
client.queue_response(
200,
r#"{"data": {"recording": {"segments": [{"embedding": {"cluster": {"id": "c1"}}}]}}}"#,
);
let query = create_graphql_query(
"{ recording(id: \\\"r1\\\") { segments { embedding { cluster { id } } } } }",
);
let response = client.post(GRAPHQL_PATH, &query).unwrap();
assert_eq!(response.status, 200);
assert!(response.body.contains("cluster"));
}
}
// ============================================================================
// Rate Limiting Tests
// ============================================================================
mod rate_limiting {
use super::*;
#[test]
fn test_rate_limiter_allows_under_limit() {
let limiter = MockRateLimiter::new(100); // 100 requests/second
// Should allow first requests
for _ in 0..50 {
assert!(limiter.check(), "Should allow requests under limit");
}
}
#[test]
fn test_rate_limiter_blocks_over_limit() {
let limiter = MockRateLimiter::new(10); // 10 requests/second
// Exhaust limit
for _ in 0..10 {
limiter.check();
}
// Next request should be blocked
assert!(!limiter.check(), "Should block requests over limit");
}
#[test]
fn test_rate_limiter_sliding_window() {
let limiter = MockRateLimiter::new(5);
// Make 5 requests
for _ in 0..5 {
assert!(limiter.check());
}
// 6th should be blocked
assert!(!limiter.check());
// After window slides (simulated by new check), requests should be allowed
// In real implementation, would wait for time to pass
}
#[test]
fn test_rate_limit_response_code() {
// When rate limited, API should return 429
let client = MockApiClient::new();
client.queue_response(
429,
r#"{"error": "Too Many Requests", "retry_after": 60}"#,
);
let response = client.get("/api/v1/recordings").unwrap();
assert_eq!(response.status, 429);
assert!(response.body.contains("Too Many Requests"));
}
#[test]
fn test_rate_limit_headers() {
let mut response = MockResponse {
status: 200,
body: "{}".to_string(),
headers: HashMap::new(),
};
response.headers.insert("X-RateLimit-Limit".to_string(), "100".to_string());
response.headers.insert("X-RateLimit-Remaining".to_string(), "95".to_string());
response.headers.insert("X-RateLimit-Reset".to_string(), "1609459200".to_string());
assert_eq!(response.headers.get("X-RateLimit-Limit").unwrap(), "100");
assert_eq!(response.headers.get("X-RateLimit-Remaining").unwrap(), "95");
}
#[test]
fn test_different_rate_limits_per_endpoint() {
// Heavy operations should have lower limits
let search_limiter = MockRateLimiter::new(10); // 10/sec for search
let read_limiter = MockRateLimiter::new(100); // 100/sec for reads
let write_limiter = MockRateLimiter::new(20); // 20/sec for writes
// Reads should be most permissive
for _ in 0..50 {
assert!(read_limiter.check());
}
// Search should be more restrictive
for _ in 0..10 {
assert!(search_limiter.check());
}
assert!(!search_limiter.check());
}
}
// ============================================================================
// Error Response Tests
// ============================================================================
mod error_responses {
use super::*;
#[test]
fn test_404_not_found() {
let client = MockApiClient::new();
client.queue_response(
404,
r#"{"error": "Not Found", "message": "Recording not found", "code": "RECORDING_NOT_FOUND"}"#,
);
let response = client.get("/api/v1/recordings/nonexistent").unwrap();
assert_eq!(response.status, 404);
assert!(response.body.contains("Not Found"));
}
#[test]
fn test_400_bad_request() {
let client = MockApiClient::new();
client.queue_response(
400,
r#"{"error": "Bad Request", "message": "Invalid segment_id format", "field": "segment_id"}"#,
);
let response = client.post("/api/v1/embeddings", r#"{"segment_id": "invalid"}"#).unwrap();
assert_eq!(response.status, 400);
assert!(response.body.contains("Bad Request"));
}
#[test]
fn test_422_validation_error() {
let client = MockApiClient::new();
client.queue_response(
422,
r#"{"error": "Validation Error", "errors": [{"field": "k", "message": "must be between 1 and 100"}]}"#,
);
let response = client.post("/api/v1/search", r#"{"k": 1000}"#).unwrap();
assert_eq!(response.status, 422);
assert!(response.body.contains("Validation Error"));
}
#[test]
fn test_500_internal_error() {
let client = MockApiClient::new();
client.queue_response(
500,
r#"{"error": "Internal Server Error", "message": "An unexpected error occurred", "request_id": "req-123"}"#,
);
let response = client.get("/api/v1/recordings").unwrap();
assert_eq!(response.status, 500);
assert!(response.body.contains("Internal Server Error"));
assert!(response.body.contains("request_id"));
}
#[test]
fn test_503_service_unavailable() {
let client = MockApiClient::new();
client.queue_response(
503,
r#"{"error": "Service Unavailable", "message": "Index is rebuilding", "retry_after": 300}"#,
);
let response = client.get("/api/v1/search").unwrap();
assert_eq!(response.status, 503);
assert!(response.body.contains("Service Unavailable"));
}
#[test]
fn test_error_response_format() {
// All errors should have consistent format
let error_bodies = vec![
r#"{"error": "Not Found", "message": "Resource not found", "code": "NOT_FOUND"}"#,
r#"{"error": "Bad Request", "message": "Invalid input", "code": "INVALID_INPUT"}"#,
r#"{"error": "Internal Server Error", "message": "Server error", "code": "INTERNAL_ERROR"}"#,
];
for body in error_bodies {
assert!(body.contains("error"));
assert!(body.contains("message"));
assert!(body.contains("code"));
}
}
#[test]
fn test_error_with_details() {
let client = MockApiClient::new();
client.queue_response(
400,
r#"{
"error": "Bad Request",
"message": "Multiple validation errors",
"details": [
{"field": "sample_rate", "error": "must be 32000"},
{"field": "channels", "error": "must be 1 (mono)"}
]
}"#,
);
let response = client.post("/api/v1/recordings", "{}").unwrap();
assert_eq!(response.status, 400);
assert!(response.body.contains("details"));
}
}
// ============================================================================
// Authentication Tests
// ============================================================================
mod authentication {
use super::*;
#[test]
fn test_unauthorized_without_token() {
let client = MockApiClient::new();
client.queue_response(
401,
r#"{"error": "Unauthorized", "message": "Missing or invalid authentication token"}"#,
);
let response = client.get("/api/v1/recordings").unwrap();
assert_eq!(response.status, 401);
assert!(response.body.contains("Unauthorized"));
}
#[test]
fn test_forbidden_insufficient_permissions() {
let client = MockApiClient::new();
client.queue_response(
403,
r#"{"error": "Forbidden", "message": "Insufficient permissions to access this resource"}"#,
);
let response = client.get("/api/v1/admin/settings").unwrap();
assert_eq!(response.status, 403);
assert!(response.body.contains("Forbidden"));
}
#[test]
fn test_token_expired() {
let client = MockApiClient::new();
client.queue_response(
401,
r#"{"error": "Unauthorized", "message": "Token expired", "code": "TOKEN_EXPIRED"}"#,
);
let response = client.get("/api/v1/recordings").unwrap();
assert_eq!(response.status, 401);
assert!(response.body.contains("TOKEN_EXPIRED"));
}
}
// ============================================================================
// Pagination Tests
// ============================================================================
mod pagination {
use super::*;
#[test]
fn test_paginated_response() {
let client = MockApiClient::new();
client.queue_response(
200,
r#"{
"data": [{"id": "rec1"}, {"id": "rec2"}],
"pagination": {
"page": 1,
"per_page": 20,
"total": 100,
"total_pages": 5
}
}"#,
);
let response = client.get("/api/v1/recordings?page=1&per_page=20").unwrap();
assert_eq!(response.status, 200);
assert!(response.body.contains("pagination"));
assert!(response.body.contains("total_pages"));
}
#[test]
fn test_cursor_based_pagination() {
let client = MockApiClient::new();
client.queue_response(
200,
r#"{
"data": [{"id": "rec1"}, {"id": "rec2"}],
"cursors": {
"next": "eyJpZCI6InJlYzIifQ==",
"previous": null
},
"has_more": true
}"#,
);
let response = client.get("/api/v1/recordings?limit=20").unwrap();
assert_eq!(response.status, 200);
assert!(response.body.contains("cursors"));
assert!(response.body.contains("has_more"));
}
#[test]
fn test_invalid_page_parameter() {
let client = MockApiClient::new();
client.queue_response(
400,
r#"{"error": "Bad Request", "message": "Page must be a positive integer"}"#,
);
let response = client.get("/api/v1/recordings?page=-1").unwrap();
assert_eq!(response.status, 400);
}
}
// ============================================================================
// Content Negotiation Tests
// ============================================================================
mod content_negotiation {
use super::*;
#[test]
fn test_json_response() {
let response = MockResponse {
status: 200,
body: r#"{"data": []}"#.to_string(),
headers: [("Content-Type".to_string(), "application/json".to_string())]
.into_iter()
.collect(),
};
assert_eq!(
response.headers.get("Content-Type").unwrap(),
"application/json"
);
}
#[test]
fn test_unsupported_media_type() {
let client = MockApiClient::new();
client.queue_response(
415,
r#"{"error": "Unsupported Media Type", "message": "Only application/json is supported"}"#,
);
let response = client.post("/api/v1/recordings", "<xml></xml>").unwrap();
// Assuming XML was sent
assert_eq!(response.status, 415);
}
}
// ============================================================================
// API Request Tracking Tests
// ============================================================================
mod request_tracking {
use super::*;
#[test]
fn test_request_count_tracking() {
let client = MockApiClient::new();
assert_eq!(client.request_count(), 0);
client.get("/path1").unwrap();
assert_eq!(client.request_count(), 1);
client.post("/path2", "{}").unwrap();
assert_eq!(client.request_count(), 2);
client.get("/path3").unwrap();
assert_eq!(client.request_count(), 3);
}
#[test]
fn test_response_queuing() {
let client = MockApiClient::new();
client.queue_response(200, "first");
client.queue_response(201, "second");
client.queue_response(202, "third");
let r1 = client.get("/").unwrap();
let r2 = client.get("/").unwrap();
let r3 = client.get("/").unwrap();
assert_eq!(r1.status, 200);
assert_eq!(r2.status, 201);
assert_eq!(r3.status, 202);
}
#[test]
fn test_default_response_when_queue_empty() {
let client = MockApiClient::new();
let response = client.get("/").unwrap();
assert_eq!(response.status, 200);
assert_eq!(response.body, "{}");
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_api_integration_smoke_test() {
let client = MockApiClient::new();
// List recordings
client.queue_response(200, r#"{"recordings": []}"#);
let list_response = client.get("/api/v1/recordings").unwrap();
assert_eq!(list_response.status, 200);
// Create recording
client.queue_response(201, r#"{"id": "new-rec"}"#);
let create_response = client.post("/api/v1/recordings", "{}").unwrap();
assert_eq!(create_response.status, 201);
// Search
client.queue_response(200, r#"{"results": []}"#);
let search_response = client.post("/api/v1/search", r#"{"k": 10}"#).unwrap();
assert_eq!(search_response.status, 200);
// Track all requests
assert_eq!(client.request_count(), 3);
}
}