mod common;
use common::*;
use serde_json::Value;
use std::time::Duration;
macro_rules! require_server {
() => {
if !is_server_running().await {
eprintln!("SKIP: server not running at {}", TEST_API_URL);
return;
}
let _c = test_client();
if login_as_admin(&_c).await.is_err() {
eprintln!("SKIP: admin login failed (run AxonML_DB_Init.sh)");
return;
}
};
}
#[tokio::test]
async fn test_complete_registration_login_flow() {
require_server!();
let client = test_client();
let unique_id = chrono::Utc::now().timestamp_millis();
let email = format!("e2e_user_{}@test.local", unique_id);
let password =
std::env::var("AXONML_TEST_PASSWORD").unwrap_or_else(|_| format!("TestPw_{}!", unique_id));
let register_response = client
.post(format!("{}/api/auth/register", TEST_API_URL))
.json(&serde_json::json!({
"name": "E2E Test User",
"email": email,
"password": &password
}))
.send()
.await
.expect("Registration request failed");
let status = register_response.status().as_u16();
assert!(
status == 200 || status == 201,
"Registration should succeed, got {}",
status
);
let login_result = login(&client, &email, &password).await;
if login_result.is_err() {
eprintln!("Note: New user login requires email verification (expected behavior)");
return; }
let token = login_result.unwrap();
let me_response = auth_get(&client, "/api/auth/me", &token)
.await
.expect("Me request failed");
assert!(me_response.status().is_success(), "Should get user info");
let user_info: Value = me_response.json().await.expect("Failed to parse JSON");
assert_eq!(user_info["email"], email);
assert_eq!(user_info["name"], "E2E Test User");
let logout_response = auth_post(&client, "/api/auth/logout", &token, serde_json::json!({}))
.await
.expect("Logout request failed");
let status = logout_response.status().as_u16();
assert!(status == 200 || status == 204, "Logout should succeed");
tokio::time::sleep(Duration::from_millis(100)).await;
let post_logout_response = auth_get(&client, "/api/auth/me", &token)
.await
.expect("Post-logout request failed");
let status = post_logout_response.status().as_u16();
assert!(
status == 200 || status == 401,
"Token should be invalid or still valid briefly, got {}",
status
);
}
#[tokio::test]
async fn test_model_lifecycle_flow() {
require_server!();
let client = test_client();
let token = login_as_admin(&client).await.expect("Login failed");
let list_response = auth_get(&client, "/api/models", &token)
.await
.expect("List models failed");
assert!(list_response.status().is_success(), "Should list models");
let _initial_models: Vec<Value> = list_response.json().await.expect("Failed to parse");
let unique_name = format!("e2e_model_{}", chrono::Utc::now().timestamp_millis());
let create_response = auth_post(
&client,
"/api/models",
&token,
serde_json::json!({
"name": unique_name,
"description": "E2E test model",
"architecture": "MLP",
"framework": "axonml"
}),
)
.await
.expect("Create model failed");
if create_response.status().is_success() {
let model: Value = create_response.json().await.expect("Failed to parse");
let model_id = model["id"].as_str().expect("Model should have ID");
let get_response = auth_get(&client, &format!("/api/models/{}", model_id), &token)
.await
.expect("Get model failed");
assert!(
get_response.status().is_success(),
"Should get model details"
);
let model_details: Value = get_response.json().await.expect("Failed to parse");
assert_eq!(model_details["name"], unique_name);
let update_response = auth_put(
&client,
&format!("/api/models/{}", model_id),
&token,
serde_json::json!({
"description": "Updated E2E test model"
}),
)
.await
.expect("Update model failed");
assert!(update_response.status().is_success(), "Should update model");
let versions_response = auth_get(
&client,
&format!("/api/models/{}/versions", model_id),
&token,
)
.await
.expect("List versions failed");
assert!(
versions_response.status().is_success(),
"Should list versions"
);
let delete_response = auth_delete(&client, &format!("/api/models/{}", model_id), &token)
.await
.expect("Delete model failed");
assert!(
delete_response.status().is_success() || delete_response.status().as_u16() == 204,
"Should delete model"
);
let verify_response = auth_get(&client, &format!("/api/models/{}", model_id), &token)
.await
.expect("Verify delete failed");
assert_eq!(
verify_response.status().as_u16(),
404,
"Model should be deleted"
);
}
}
#[tokio::test]
async fn test_training_run_lifecycle() {
require_server!();
let client = test_client();
let token = login_as_admin(&client).await.expect("Login failed");
let list_response = auth_get(&client, "/api/training/runs", &token)
.await
.expect("List runs failed");
assert!(list_response.status().is_success(), "Should list runs");
let unique_name = format!("e2e_run_{}", chrono::Utc::now().timestamp_millis());
let create_response = auth_post(
&client,
"/api/training/runs",
&token,
serde_json::json!({
"name": unique_name,
"model_type": "MLP",
"config": {
"epochs": 1,
"batch_size": 32,
"learning_rate": 0.001,
"optimizer": "adam"
}
}),
)
.await
.expect("Create run failed");
if create_response.status().is_success() || create_response.status().as_u16() == 201 {
let run: Value = create_response.json().await.expect("Failed to parse");
let run_id = run["id"].as_str().expect("Run should have ID");
let get_response = auth_get(&client, &format!("/api/training/runs/{}", run_id), &token)
.await
.expect("Get run failed");
assert!(get_response.status().is_success(), "Should get run details");
let run_details: Value = get_response.json().await.expect("Failed to parse");
assert_eq!(run_details["name"], unique_name);
let metrics_response = auth_post(
&client,
&format!("/api/training/runs/{}/metrics", run_id),
&token,
serde_json::json!({
"epoch": 0,
"step": 10,
"loss": 0.5,
"accuracy": 0.75
}),
)
.await
.expect("Record metrics failed");
let status = metrics_response.status().as_u16();
if status == 500 {
eprintln!("Note: Metrics recording returned 500 - time series may not be configured");
} else {
assert!(
status == 200 || status == 201 || status == 204,
"Should record metrics, got {}",
status
);
}
let get_metrics_response = auth_get(
&client,
&format!("/api/training/runs/{}/metrics", run_id),
&token,
)
.await
.expect("Get metrics failed");
assert!(
get_metrics_response.status().is_success(),
"Should get metrics"
);
let stop_response = auth_post(
&client,
&format!("/api/training/runs/{}/stop", run_id),
&token,
serde_json::json!({}),
)
.await
.expect("Stop run failed");
assert!(stop_response.status().is_success(), "Should stop run");
let delete_response =
auth_delete(&client, &format!("/api/training/runs/{}", run_id), &token)
.await
.expect("Delete run failed");
let status = delete_response.status().as_u16();
assert!(
status == 200 || status == 204,
"Should delete run, got {}",
status
);
}
}
#[tokio::test]
async fn test_inference_endpoint_lifecycle() {
require_server!();
let client = test_client();
let token = login_as_admin(&client).await.expect("Login failed");
let list_response = auth_get(&client, "/api/inference/endpoints", &token)
.await
.expect("List endpoints failed");
assert!(list_response.status().is_success(), "Should list endpoints");
let unique_name = format!("e2e_endpoint_{}", chrono::Utc::now().timestamp_millis());
let create_response = auth_post(
&client,
"/api/inference/endpoints",
&token,
serde_json::json!({
"name": unique_name,
"model_id": "test-model-id",
"version_id": "v1"
}),
)
.await
.expect("Create endpoint failed");
if create_response.status().is_success() || create_response.status().as_u16() == 201 {
let endpoint: Value = create_response.json().await.expect("Failed to parse");
let endpoint_id = endpoint["id"].as_str().expect("Endpoint should have ID");
let get_response = auth_get(
&client,
&format!("/api/inference/endpoints/{}", endpoint_id),
&token,
)
.await
.expect("Get endpoint failed");
assert!(
get_response.status().is_success(),
"Should get endpoint details"
);
let start_response = auth_post(
&client,
&format!("/api/inference/endpoints/{}/start", endpoint_id),
&token,
serde_json::json!({}),
)
.await
.expect("Start endpoint failed");
let status = start_response.status().as_u16();
assert!(
status == 200 || status == 400 || status == 404 || status == 500,
"Start should return expected status, got {}",
status
);
let stop_response = auth_post(
&client,
&format!("/api/inference/endpoints/{}/stop", endpoint_id),
&token,
serde_json::json!({}),
)
.await
.expect("Stop endpoint failed");
assert!(
stop_response.status().is_success() || stop_response.status().as_u16() == 400,
"Stop should succeed or return error if not started"
);
let delete_response = auth_delete(
&client,
&format!("/api/inference/endpoints/{}", endpoint_id),
&token,
)
.await
.expect("Delete endpoint failed");
let status = delete_response.status().as_u16();
assert!(
status == 200 || status == 204,
"Should delete endpoint, got {}",
status
);
}
}
#[tokio::test]
async fn test_hub_browse_flow() {
require_server!();
let client = test_client();
let token = login_as_admin(&client).await.expect("Login failed");
let list_response = auth_get(&client, "/api/hub/models", &token)
.await
.expect("List hub models failed");
assert!(
list_response.status().is_success(),
"Should list hub models"
);
let models: Vec<Value> = list_response.json().await.expect("Failed to parse");
if !models.is_empty() {
let model_name = models[0]["name"].as_str().expect("Model should have name");
let info_response = auth_get(&client, &format!("/api/hub/models/{}", model_name), &token)
.await
.expect("Get model info failed");
assert!(info_response.status().is_success(), "Should get model info");
let model_info: Value = info_response.json().await.expect("Failed to parse");
assert_eq!(model_info["name"], model_name);
}
let cache_response = auth_get(&client, "/api/hub/cache", &token)
.await
.expect("Get cache info failed");
assert!(
cache_response.status().is_success(),
"Should get cache info"
);
}
#[tokio::test]
async fn test_system_info_flow() {
require_server!();
let client = test_client();
let token = login_as_admin(&client).await.expect("Login failed");
let info_response = auth_get(&client, "/api/system/info", &token)
.await
.expect("Get system info failed");
assert!(
info_response.status().is_success(),
"Should get system info"
);
let info: Value = info_response.json().await.expect("Failed to parse");
assert!(
info.get("cpu_count").is_some() || info.get("cpus").is_some(),
"Should have CPU info"
);
let gpus_response = auth_get(&client, "/api/system/gpus", &token)
.await
.expect("List GPUs failed");
assert!(gpus_response.status().is_success(), "Should list GPUs");
}
#[tokio::test]
async fn test_admin_operations_flow() {
require_server!();
let client = test_client();
let token = login_as_admin(&client).await.expect("Login failed");
let users_response = auth_get(&client, "/api/admin/users", &token)
.await
.expect("List users failed");
assert!(
users_response.status().is_success(),
"Admin should list users"
);
let users: Vec<Value> = users_response.json().await.expect("Failed to parse");
assert!(!users.is_empty(), "Should have at least admin user");
let stats_response = auth_get(&client, "/api/admin/stats", &token)
.await
.expect("Get stats failed");
assert!(
stats_response.status().is_success(),
"Admin should get stats"
);
}
#[tokio::test]
async fn test_dataset_operations_flow() {
require_server!();
let client = test_client();
let token = login_as_admin(&client).await.expect("Login failed");
let list_response = auth_get(&client, "/api/datasets", &token)
.await
.expect("List datasets failed");
assert!(list_response.status().is_success(), "Should list datasets");
let datasets: Vec<Value> = list_response.json().await.expect("Failed to parse");
if !datasets.is_empty() {
let dataset_id = datasets[0]["id"].as_str().expect("Dataset should have ID");
let get_response = auth_get(&client, &format!("/api/datasets/{}", dataset_id), &token)
.await
.expect("Get dataset failed");
assert!(
get_response.status().is_success(),
"Should get dataset details"
);
let analyze_response = auth_post(
&client,
&format!("/api/data/{}/analyze", dataset_id),
&token,
serde_json::json!({}),
)
.await
.expect("Analyze dataset failed");
let status = analyze_response.status().as_u16();
assert!(
status == 200 || status == 400 || status == 404 || status == 500,
"Analyze should return expected status, got {}",
status
);
}
}
#[tokio::test]
async fn test_complete_ml_pipeline_flow() {
require_server!();
let client = test_client();
let token = login_as_admin(&client).await.expect("Login failed");
let datasets_response = match auth_get(&client, "/api/builtin-datasets", &token).await {
Ok(resp) => resp,
Err(e) => {
eprintln!(
"Note: Builtin datasets request failed (network issue): {}",
e
);
return; }
};
assert!(
datasets_response.status().is_success(),
"Should list builtin datasets"
);
let model_name = format!("pipeline_model_{}", chrono::Utc::now().timestamp_millis());
let model_response = auth_post(
&client,
"/api/models",
&token,
serde_json::json!({
"name": model_name,
"description": "Pipeline test model",
"architecture": "CNN",
"framework": "axonml"
}),
)
.await
.expect("Create model failed");
if model_response.status().is_success() {
let model: Value = model_response.json().await.expect("Failed to parse");
let model_id = model["id"].as_str().unwrap_or("unknown");
let run_response = auth_post(
&client,
"/api/training/runs",
&token,
serde_json::json!({
"name": format!("pipeline_run_{}", chrono::Utc::now().timestamp_millis()),
"model_type": "CNN",
"config": {
"epochs": 1,
"batch_size": 32,
"learning_rate": 0.001,
"optimizer": "adam"
}
}),
)
.await
.expect("Create run failed");
if run_response.status().is_success() || run_response.status().as_u16() == 201 {
let run: Value = run_response.json().await.expect("Failed to parse");
let run_id = run["id"].as_str().unwrap_or("unknown");
let status_response =
auth_get(&client, &format!("/api/training/runs/{}", run_id), &token)
.await
.expect("Get run status failed");
assert!(
status_response.status().is_success(),
"Should get run status"
);
let _ = auth_post(
&client,
&format!("/api/training/runs/{}/stop", run_id),
&token,
serde_json::json!({}),
)
.await;
let _ = auth_delete(&client, &format!("/api/training/runs/{}", run_id), &token).await;
}
let _ = auth_delete(&client, &format!("/api/models/{}", model_id), &token).await;
}
}
#[tokio::test]
async fn test_concurrent_api_requests() {
require_server!();
let client = test_client();
let token = login_as_admin(&client).await.expect("Login failed");
let futures = vec![
auth_get(&client, "/api/models", &token),
auth_get(&client, "/api/training/runs", &token),
auth_get(&client, "/api/datasets", &token),
auth_get(&client, "/api/inference/endpoints", &token),
auth_get(&client, "/api/hub/models", &token),
auth_get(&client, "/api/system/info", &token),
auth_get(&client, "/api/builtin-datasets", &token),
auth_get(&client, "/api/kaggle/status", &token),
];
let results = futures::future::join_all(futures).await;
let mut success_count = 0;
let mut network_errors = 0;
for (i, result) in results.into_iter().enumerate() {
match result {
Ok(response) => {
if response.status().is_success() {
success_count += 1;
} else {
eprintln!("Request {} returned {}", i, response.status());
}
}
Err(e) => {
eprintln!("Request {} had network error: {}", i, e);
network_errors += 1;
}
}
}
assert!(
success_count >= 4,
"At least 4 requests should succeed, got {} (network errors: {})",
success_count,
network_errors
);
}
#[tokio::test]
async fn test_invalid_json_handling() {
require_server!();
let client = test_client();
let token = login_as_admin(&client).await.expect("Login failed");
let response = client
.post(format!("{}/api/models", TEST_API_URL))
.header("Authorization", format!("Bearer {}", token))
.header("Content-Type", "application/json")
.body("{ invalid json }")
.send()
.await
.expect("Request failed");
assert!(
response.status().is_client_error(),
"Should reject invalid JSON"
);
}
#[tokio::test]
async fn test_missing_required_fields() {
require_server!();
let client = test_client();
let token = login_as_admin(&client).await.expect("Login failed");
let response = auth_post(
&client,
"/api/models",
&token,
serde_json::json!({
"description": "Test"
}),
)
.await
.expect("Request failed");
assert!(
response.status().is_client_error(),
"Should reject missing required fields"
);
}
#[tokio::test]
async fn test_404_handling() {
require_server!();
let client = test_client();
let token = login_as_admin(&client).await.expect("Login failed");
let endpoints = vec![
"/api/models/nonexistent-id-12345",
"/api/training/runs/nonexistent-id-12345",
"/api/datasets/nonexistent-id-12345",
"/api/inference/endpoints/nonexistent-id-12345",
];
for endpoint in endpoints {
let response = auth_get(&client, endpoint, &token)
.await
.expect("Request failed");
assert_eq!(
response.status().as_u16(),
404,
"Endpoint {} should return 404",
endpoint
);
}
}