fix: cover api edge cases
This commit is contained in:
213
tests/app.rs
213
tests/app.rs
@@ -80,9 +80,11 @@ async fn translated_endpoint_returns_translated_description() {
|
||||
mount_species(&pokeapi, "pikachu", false, "forest", "Electric cheeks.").await;
|
||||
Mock::given(matchers::method("POST"))
|
||||
.and(matchers::path("/translate/shakespeare.json"))
|
||||
.and(matchers::body_string_contains("Electric+cheeks"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
|
||||
"contents": { "translated": "Electric cheeks, prithee." }
|
||||
})))
|
||||
.expect(1)
|
||||
.mount(&translations)
|
||||
.await;
|
||||
let app = create_app(state_for(&pokeapi, &translations));
|
||||
@@ -94,8 +96,52 @@ async fn translated_endpoint_returns_translated_description() {
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
json_body(response).await["description"],
|
||||
"Electric cheeks, prithee."
|
||||
json_body(response).await,
|
||||
serde_json::json!({
|
||||
"name": "pikachu",
|
||||
"description": "Electric cheeks, prithee.",
|
||||
"habitat": "forest",
|
||||
"isLegendary": false
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn translated_endpoint_uses_yoda_for_legendary_pokemon() {
|
||||
let pokeapi = MockServer::start().await;
|
||||
let translations = MockServer::start().await;
|
||||
mount_species(&pokeapi, "mewtwo", true, "rare", "Created by science.").await;
|
||||
Mock::given(matchers::method("POST"))
|
||||
.and(matchers::path("/translate/yoda.json"))
|
||||
.and(matchers::body_string_contains("Created+by+science"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
|
||||
"contents": { "translated": "Created by science, it was." }
|
||||
})))
|
||||
.expect(1)
|
||||
.mount(&translations)
|
||||
.await;
|
||||
Mock::given(matchers::method("POST"))
|
||||
.and(matchers::path("/translate/shakespeare.json"))
|
||||
.respond_with(ResponseTemplate::new(200))
|
||||
.expect(0)
|
||||
.mount(&translations)
|
||||
.await;
|
||||
let app = create_app(state_for(&pokeapi, &translations));
|
||||
|
||||
let response = app
|
||||
.oneshot(request("/pokemon/translated/mewtwo"))
|
||||
.await
|
||||
.expect("request should be handled");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
json_body(response).await,
|
||||
serde_json::json!({
|
||||
"name": "mewtwo",
|
||||
"description": "Created by science, it was.",
|
||||
"habitat": "rare",
|
||||
"isLegendary": true
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -117,7 +163,47 @@ async fn translated_endpoint_falls_back_to_original_description_when_translation
|
||||
.expect("request should be handled");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(json_body(response).await["description"], "It has no eyes.");
|
||||
assert_eq!(
|
||||
json_body(response).await,
|
||||
serde_json::json!({
|
||||
"name": "zubat",
|
||||
"description": "It has no eyes.",
|
||||
"habitat": "cave",
|
||||
"isLegendary": false
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn translated_endpoint_falls_back_when_translation_is_empty() {
|
||||
let pokeapi = MockServer::start().await;
|
||||
let translations = MockServer::start().await;
|
||||
mount_species(&pokeapi, "zubat", false, "cave", "It has no eyes.").await;
|
||||
Mock::given(matchers::method("POST"))
|
||||
.and(matchers::path("/translate/yoda.json"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
|
||||
"contents": { "translated": " " }
|
||||
})))
|
||||
.expect(1)
|
||||
.mount(&translations)
|
||||
.await;
|
||||
let app = create_app(state_for(&pokeapi, &translations));
|
||||
|
||||
let response = app
|
||||
.oneshot(request("/pokemon/translated/zubat"))
|
||||
.await
|
||||
.expect("request should be handled");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
json_body(response).await,
|
||||
serde_json::json!({
|
||||
"name": "zubat",
|
||||
"description": "It has no eyes.",
|
||||
"habitat": "cave",
|
||||
"isLegendary": false
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -180,6 +266,7 @@ async fn metrics_endpoint_exposes_http_and_upstream_otel_metrics() {
|
||||
async fn rate_limit_rejects_requests_over_the_configured_burst() {
|
||||
let pokeapi = MockServer::start().await;
|
||||
let translations = MockServer::start().await;
|
||||
mount_species(&pokeapi, "mewtwo", true, "rare", "Created by science.").await;
|
||||
let telemetry = Telemetry::new("test-service");
|
||||
let app = create_app(AppState::new(
|
||||
PokedexService::new(
|
||||
@@ -194,11 +281,11 @@ async fn rate_limit_rejects_requests_over_the_configured_burst() {
|
||||
|
||||
let first = app
|
||||
.clone()
|
||||
.oneshot(request("/health"))
|
||||
.oneshot(request("/pokemon/mewtwo"))
|
||||
.await
|
||||
.expect("request should be handled");
|
||||
let second = app
|
||||
.oneshot(request("/health"))
|
||||
.oneshot(request("/pokemon/mewtwo"))
|
||||
.await
|
||||
.expect("request should be handled");
|
||||
|
||||
@@ -207,6 +294,43 @@ async fn rate_limit_rejects_requests_over_the_configured_burst() {
|
||||
assert_eq!(json_body(second).await["error"], "rate limit exceeded");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn health_and_metrics_are_not_rate_limited() {
|
||||
let pokeapi = MockServer::start().await;
|
||||
let translations = MockServer::start().await;
|
||||
mount_species(&pokeapi, "mewtwo", true, "rare", "Created by science.").await;
|
||||
let telemetry = Telemetry::new("test-service");
|
||||
let app = create_app(AppState::new(
|
||||
PokedexService::new(
|
||||
PokeApiClient::new(pokeapi.uri(), Duration::from_secs(1)).expect("client should build"),
|
||||
TranslationClient::new(translations.uri(), Duration::from_secs(1))
|
||||
.expect("client should build"),
|
||||
telemetry.metrics(),
|
||||
),
|
||||
telemetry,
|
||||
RateLimiter::new(0.01, 1).expect("rate limiter should build"),
|
||||
));
|
||||
|
||||
let pokemon = app
|
||||
.clone()
|
||||
.oneshot(request("/pokemon/mewtwo"))
|
||||
.await
|
||||
.expect("request should be handled");
|
||||
let health = app
|
||||
.clone()
|
||||
.oneshot(request("/health"))
|
||||
.await
|
||||
.expect("request should be handled");
|
||||
let metrics = app
|
||||
.oneshot(request("/metrics"))
|
||||
.await
|
||||
.expect("request should be handled");
|
||||
|
||||
assert_eq!(pokemon.status(), StatusCode::OK);
|
||||
assert_eq!(health.status(), StatusCode::OK);
|
||||
assert_eq!(metrics.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pokemon_endpoint_maps_missing_pokemon_to_404() {
|
||||
let pokeapi = MockServer::start().await;
|
||||
@@ -219,12 +343,91 @@ async fn pokemon_endpoint_maps_missing_pokemon_to_404() {
|
||||
let app = create_app(state_for(&pokeapi, &translations));
|
||||
|
||||
let response = app
|
||||
.clone()
|
||||
.oneshot(request("/pokemon/nope"))
|
||||
.await
|
||||
.expect("request should be handled");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||
assert_eq!(json_body(response).await["error"], "pokemon not found");
|
||||
|
||||
let metrics = app
|
||||
.oneshot(request("/metrics"))
|
||||
.await
|
||||
.expect("request should be handled");
|
||||
let body = text_body(metrics).await;
|
||||
assert!(body.contains("status=\"not_found\""));
|
||||
assert!(!body.contains("upstream_client_errors"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pokemon_endpoint_maps_pokeapi_500_to_502() {
|
||||
let pokeapi = MockServer::start().await;
|
||||
let translations = MockServer::start().await;
|
||||
Mock::given(matchers::method("GET"))
|
||||
.and(matchers::path("/pokemon-species/mewtwo"))
|
||||
.respond_with(ResponseTemplate::new(500))
|
||||
.mount(&pokeapi)
|
||||
.await;
|
||||
let app = create_app(state_for(&pokeapi, &translations));
|
||||
|
||||
let response = app
|
||||
.oneshot(request("/pokemon/mewtwo"))
|
||||
.await
|
||||
.expect("request should be handled");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::BAD_GATEWAY);
|
||||
assert_eq!(
|
||||
json_body(response).await["error"],
|
||||
"upstream service failed"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pokemon_endpoint_maps_invalid_pokeapi_json_to_502() {
|
||||
let pokeapi = MockServer::start().await;
|
||||
let translations = MockServer::start().await;
|
||||
Mock::given(matchers::method("GET"))
|
||||
.and(matchers::path("/pokemon-species/mewtwo"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_string("not-json"))
|
||||
.mount(&pokeapi)
|
||||
.await;
|
||||
let app = create_app(state_for(&pokeapi, &translations));
|
||||
|
||||
let response = app
|
||||
.oneshot(request("/pokemon/mewtwo"))
|
||||
.await
|
||||
.expect("request should be handled");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::BAD_GATEWAY);
|
||||
assert_eq!(
|
||||
json_body(response).await["error"],
|
||||
"upstream service returned invalid data"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn translated_endpoint_does_not_call_funtranslations_when_pokeapi_fails() {
|
||||
let pokeapi = MockServer::start().await;
|
||||
let translations = MockServer::start().await;
|
||||
Mock::given(matchers::method("GET"))
|
||||
.and(matchers::path("/pokemon-species/mewtwo"))
|
||||
.respond_with(ResponseTemplate::new(500))
|
||||
.mount(&pokeapi)
|
||||
.await;
|
||||
Mock::given(matchers::method("POST"))
|
||||
.respond_with(ResponseTemplate::new(200))
|
||||
.expect(0)
|
||||
.mount(&translations)
|
||||
.await;
|
||||
let app = create_app(state_for(&pokeapi, &translations));
|
||||
|
||||
let response = app
|
||||
.oneshot(request("/pokemon/translated/mewtwo"))
|
||||
.await
|
||||
.expect("request should be handled");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::BAD_GATEWAY);
|
||||
}
|
||||
|
||||
async fn mount_species(
|
||||
|
||||
Reference in New Issue
Block a user