convertor/api-server/tests/graphql_tests.rs
Your Name e083e5d11d feat: Add Rust API Server with REST and GraphQL support
- Implement JWT authentication layer
- Add Engine Registry with 20+ conversion engines
- Implement Conversion Service with background job processing
- REST API endpoints for file conversion operations
- GraphQL API with queries and mutations
- Comprehensive error handling with conversion suggestions
- Integration tests for both REST and GraphQL APIs
- Complete API documentation

Features:
- REST API: /api/v1/* endpoints
- GraphQL API: /graphql endpoint with playground
- JWT Bearer token authentication
- Engine validation with alternative suggestions
- File download via API (not exposing file paths)
- Support for FFmpeg, ImageMagick, LibreOffice, Pandoc, and more
2026-01-21 11:40:03 +08:00

413 lines
13 KiB
Rust

//! GraphQL integration tests
use std::sync::Arc;
use axum::http::header;
use axum_test::TestServer;
use serde_json::{json, Value};
use convertx_api::{build_router, config::Config, AppState};
fn create_test_server() -> TestServer {
let config = Config::test_config();
let state = AppState::new(config);
let app = build_router(state);
TestServer::new(app).unwrap()
}
fn generate_test_token() -> String {
use jsonwebtoken::{encode, EncodingKey, Header};
use chrono::Utc;
#[derive(serde::Serialize)]
struct Claims {
sub: String,
exp: i64,
iat: i64,
}
let now = Utc::now().timestamp();
let claims = Claims {
sub: "test-user-123".to_string(),
exp: now + 3600,
iat: now,
};
encode(
&Header::default(),
&claims,
&EncodingKey::from_secret("test-secret-key".as_bytes()),
)
.unwrap()
}
mod graphql_query_tests {
use super::*;
#[tokio::test]
async fn test_health_query() {
let server = create_test_server();
let query = json!({
"query": "{ health { status version timestamp } }"
});
let response = server
.post("/graphql")
.content_type("application/json")
.json(&query)
.await;
response.assert_status_ok();
let body: Value = response.json();
assert_eq!(body["data"]["health"]["status"], "healthy");
assert!(body["data"]["health"]["version"].is_string());
}
#[tokio::test]
async fn test_engines_query_unauthorized() {
let server = create_test_server();
let query = json!({
"query": "{ engines { id name description } }"
});
let response = server
.post("/graphql")
.content_type("application/json")
.json(&query)
.await;
response.assert_status_ok();
let body: Value = response.json();
// Should have errors due to missing auth
assert!(body["errors"].is_array());
}
#[tokio::test]
async fn test_engines_query() {
let server = create_test_server();
let token = generate_test_token();
let query = json!({
"query": "{ engines { id name description supportedInputFormats supportedOutputFormats } }"
});
let response = server
.post("/graphql")
.add_header(header::AUTHORIZATION, format!("Bearer {}", token))
.content_type("application/json")
.json(&query)
.await;
response.assert_status_ok();
let body: Value = response.json();
assert!(body["data"]["engines"].is_array());
let engines = body["data"]["engines"].as_array().unwrap();
assert!(!engines.is_empty());
// Verify structure
let first_engine = &engines[0];
assert!(first_engine["id"].is_string());
assert!(first_engine["name"].is_string());
assert!(first_engine["supportedInputFormats"].is_array());
assert!(first_engine["supportedOutputFormats"].is_array());
}
#[tokio::test]
async fn test_engine_query() {
let server = create_test_server();
let token = generate_test_token();
let query = json!({
"query": "{ engine(id: \"ffmpeg\") { id name description } }"
});
let response = server
.post("/graphql")
.add_header(header::AUTHORIZATION, format!("Bearer {}", token))
.content_type("application/json")
.json(&query)
.await;
response.assert_status_ok();
let body: Value = response.json();
assert_eq!(body["data"]["engine"]["id"], "ffmpeg");
assert_eq!(body["data"]["engine"]["name"], "FFmpeg");
}
#[tokio::test]
async fn test_jobs_query_empty() {
let server = create_test_server();
let token = generate_test_token();
let query = json!({
"query": "{ jobs { id status originalFilename } }"
});
let response = server
.post("/graphql")
.add_header(header::AUTHORIZATION, format!("Bearer {}", token))
.content_type("application/json")
.json(&query)
.await;
response.assert_status_ok();
let body: Value = response.json();
assert!(body["data"]["jobs"].is_array());
assert!(body["data"]["jobs"].as_array().unwrap().is_empty());
}
#[tokio::test]
async fn test_validate_conversion_supported() {
let server = create_test_server();
let token = generate_test_token();
let query = json!({
"query": "{ validateConversion(engine: \"ffmpeg\", from: \"mp4\", to: \"webm\") { success error { code message } } }"
});
let response = server
.post("/graphql")
.add_header(header::AUTHORIZATION, format!("Bearer {}", token))
.content_type("application/json")
.json(&query)
.await;
response.assert_status_ok();
let body: Value = response.json();
assert_eq!(body["data"]["validateConversion"]["success"], true);
assert!(body["data"]["validateConversion"]["error"].is_null());
}
#[tokio::test]
async fn test_validate_conversion_unsupported() {
let server = create_test_server();
let token = generate_test_token();
let query = json!({
"query": "{ validateConversion(engine: \"ffmpeg\", from: \"pdf\", to: \"mp4\") { success error { code message suggestions { engine from to } } } }"
});
let response = server
.post("/graphql")
.add_header(header::AUTHORIZATION, format!("Bearer {}", token))
.content_type("application/json")
.json(&query)
.await;
response.assert_status_ok();
let body: Value = response.json();
assert_eq!(body["data"]["validateConversion"]["success"], false);
assert!(body["data"]["validateConversion"]["error"].is_object());
assert_eq!(
body["data"]["validateConversion"]["error"]["code"],
"UNSUPPORTED_CONVERSION"
);
}
#[tokio::test]
async fn test_suggestions_query() {
let server = create_test_server();
let token = generate_test_token();
let query = json!({
"query": "{ suggestions(from: \"png\", to: \"jpg\") { engine from to } }"
});
let response = server
.post("/graphql")
.add_header(header::AUTHORIZATION, format!("Bearer {}", token))
.content_type("application/json")
.json(&query)
.await;
response.assert_status_ok();
let body: Value = response.json();
assert!(body["data"]["suggestions"].is_array());
let suggestions = body["data"]["suggestions"].as_array().unwrap();
// Should have suggestions for png to jpg conversion
assert!(!suggestions.is_empty());
// Verify at least one suggestion uses imagemagick or vips
let has_valid_engine = suggestions.iter().any(|s| {
s["engine"] == "imagemagick" || s["engine"] == "vips"
});
assert!(has_valid_engine);
}
}
mod graphql_mutation_tests {
use super::*;
use base64::{Engine as _, engine::general_purpose::STANDARD};
#[tokio::test]
async fn test_create_job_mutation() {
let server = create_test_server();
let token = generate_test_token();
// Create a simple test file content (PNG header)
let file_content = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
let file_base64 = STANDARD.encode(&file_content);
let query = json!({
"query": r#"
mutation($filename: String!, $fileBase64: String!, $input: CreateJobInput!) {
createJob(filename: $filename, fileBase64: $fileBase64, input: $input) {
success
job {
id
status
originalFilename
sourceFormat
targetFormat
engine
}
error {
code
message
suggestions {
engine
from
to
}
}
}
}
"#,
"variables": {
"filename": "test.png",
"fileBase64": file_base64,
"input": {
"engine": "imagemagick",
"targetFormat": "jpg"
}
}
});
let response = server
.post("/graphql")
.add_header(header::AUTHORIZATION, format!("Bearer {}", token))
.content_type("application/json")
.json(&query)
.await;
response.assert_status_ok();
let body: Value = response.json();
// Job creation should succeed
if body["data"]["createJob"]["success"] == true {
assert!(body["data"]["createJob"]["job"]["id"].is_string());
assert_eq!(body["data"]["createJob"]["job"]["originalFilename"], "test.png");
assert_eq!(body["data"]["createJob"]["job"]["engine"], "imagemagick");
}
}
#[tokio::test]
async fn test_create_job_unsupported_conversion() {
let server = create_test_server();
let token = generate_test_token();
// Try to convert PDF with FFmpeg (unsupported)
let file_content = b"%PDF-1.4";
let file_base64 = STANDARD.encode(file_content);
let query = json!({
"query": r#"
mutation($filename: String!, $fileBase64: String!, $input: CreateJobInput!) {
createJob(filename: $filename, fileBase64: $fileBase64, input: $input) {
success
error {
code
message
suggestions {
engine
from
to
}
}
}
}
"#,
"variables": {
"filename": "document.pdf",
"fileBase64": file_base64,
"input": {
"engine": "ffmpeg",
"targetFormat": "mp4"
}
}
});
let response = server
.post("/graphql")
.add_header(header::AUTHORIZATION, format!("Bearer {}", token))
.content_type("application/json")
.json(&query)
.await;
response.assert_status_ok();
let body: Value = response.json();
// Should fail with suggestions
assert_eq!(body["data"]["createJob"]["success"], false);
assert!(body["data"]["createJob"]["error"].is_object());
assert_eq!(body["data"]["createJob"]["error"]["code"], "UNSUPPORTED_CONVERSION");
// Should have suggestions for alternative conversions
let suggestions = &body["data"]["createJob"]["error"]["suggestions"];
assert!(suggestions.is_array());
}
#[tokio::test]
async fn test_delete_job_not_found() {
let server = create_test_server();
let token = generate_test_token();
let query = json!({
"query": r#"
mutation {
deleteJob(id: "00000000-0000-0000-0000-000000000000")
}
"#
});
let response = server
.post("/graphql")
.add_header(header::AUTHORIZATION, format!("Bearer {}", token))
.content_type("application/json")
.json(&query)
.await;
response.assert_status_ok();
let body: Value = response.json();
// Should have an error
assert!(body["errors"].is_array());
}
}
mod graphql_playground_test {
use super::*;
#[tokio::test]
async fn test_playground_available() {
let server = create_test_server();
let response = server.get("/graphql").await;
response.assert_status_ok();
let body = response.text();
// Should contain GraphQL Playground HTML
assert!(body.contains("GraphQL"));
}
}