use exonum::runtime::SUPERVISOR_INSTANCE_ID;
use exonum_api as api;
use exonum_rust_runtime::{RustRuntime, ServiceFactory};
use exonum_testkit::{ApiKind, Spec, TestKit, TestKitApi, TestKitBuilder};
use pretty_assertions::assert_eq;
use crate::{
api_service::{ApiInterface, ApiService, ApiServiceV2, PingQuery, SERVICE_ID, SERVICE_NAME},
supervisor::{StartMigration, Supervisor, SupervisorInterface},
};
mod api_service;
mod supervisor;
fn init_testkit() -> (TestKit, TestKitApi) {
let mut testkit = TestKitBuilder::validator()
.with(Spec::new(Supervisor).with_default_instance())
.with(Spec::new(ApiService).with_default_instance())
.with(Spec::migrating(ApiServiceV2))
.build();
let api = testkit.api();
(testkit, api)
}
#[tokio::test]
async fn ping_pong() {
let (_testkit, api) = init_testkit();
let ping = PingQuery { value: 64 };
let pong: u64 = api
.public(ApiKind::Service("api-service"))
.query(&ping)
.get("ping-pong")
.await
.expect("Request to the valid endpoint failed");
assert_eq!(ping.value, pong);
}
#[tokio::test]
async fn submit_tx() {
let (mut testkit, api) = init_testkit();
let ping = PingQuery { value: 64 };
api.public(ApiKind::Service("api-service"))
.query(&ping)
.post::<()>("submit-tx")
.await
.expect("Request to the valid endpoint failed");
let block = testkit.create_block();
assert_eq!(block.len(), 1);
let expected_tx = testkit
.us()
.service_keypair()
.do_nothing(SERVICE_ID, ping.value);
assert_eq!(*block[0].message(), expected_tx);
}
#[tokio::test]
async fn deprecated() {
let (_testkit, api) = init_testkit();
let ping = PingQuery { value: 64 };
const UNBOUND_WARNING: &str = "299 - \"Deprecated API: This endpoint is deprecated, \
see the service documentation to find an alternative. \
Currently there is no specific date for disabling this endpoint.\"";
const WARNING_WITH_DEADLINE: &str = "299 - \"Deprecated API: This endpoint is deprecated, \
see the service documentation to find an alternative. \
The old API is maintained until Fri, 31 Dec 2055 23:59:59 GMT.\"";
let pong: u64 = api
.public(ApiKind::Service("api-service"))
.query(&ping)
.expect_header("Warning", UNBOUND_WARNING)
.get("ping-pong-deprecated")
.await
.expect("Request to the valid endpoint failed");
assert_eq!(ping.value, pong);
let pong: u64 = api
.public(ApiKind::Service("api-service"))
.query(&ping)
.expect_header("Warning", WARNING_WITH_DEADLINE)
.get("ping-pong-deprecated-with-deadline")
.await
.expect("Request to the valid endpoint failed");
assert_eq!(ping.value, pong);
let pong: u64 = api
.public(ApiKind::Service("api-service"))
.query(&ping)
.expect_header("Warning", UNBOUND_WARNING)
.post("ping-pong-deprecated-mut")
.await
.expect("Request to the valid endpoint failed");
assert_eq!(ping.value, pong);
}
#[tokio::test]
async fn gone() {
let (_testkit, api) = init_testkit();
let ping = PingQuery { value: 64 };
let pong_error: api::Error = api
.public(ApiKind::Service("api-service"))
.query(&ping)
.get::<u64>("gone-immutable")
.await
.expect_err("Request to the `Gone` endpoint succeed");
assert_eq!(pong_error.http_code, api::HttpStatusCode::GONE);
assert_eq!(
pong_error.body.source,
format!("{}:{}", SERVICE_ID, SERVICE_NAME)
);
let pong_error: api::Error = api
.public(ApiKind::Service("api-service"))
.query(&ping)
.post::<u64>("gone-mutable")
.await
.expect_err("Request to the `Gone` endpoint succeed");
assert_eq!(pong_error.http_code, api::HttpStatusCode::GONE);
assert_eq!(
pong_error.body.source,
format!("{}:{}", SERVICE_ID, SERVICE_NAME)
);
}
#[tokio::test]
async fn moved() {
let (_testkit, api) = init_testkit();
let ping = PingQuery { value: 64 };
let pong_error: api::Error = api
.public(ApiKind::Service("api-service"))
.query(&ping)
.expect_header("Location", "../ping-pong?value=64")
.get::<u64>("moved-immutable")
.await
.expect_err("Request to the `MovedPermanently` endpoint succeed");
assert_eq!(pong_error.http_code, api::HttpStatusCode::MOVED_PERMANENTLY);
assert_eq!(
pong_error.body.source,
format!("{}:{}", SERVICE_ID, SERVICE_NAME)
);
let pong_error: api::Error = api
.public(ApiKind::Service("api-service"))
.query(&ping)
.expect_header("Location", "../ping-pong-deprecated-mut")
.post::<u64>("moved-mutable")
.await
.expect_err("Request to the `MovedPermanently` endpoint succeed");
assert_eq!(pong_error.http_code, api::HttpStatusCode::MOVED_PERMANENTLY);
assert_eq!(
pong_error.body.source,
format!("{}:{}", SERVICE_ID, SERVICE_NAME)
);
}
#[tokio::test]
async fn endpoint_with_new_error_type() {
let (_testkit, api) = init_testkit();
let ok_query = PingQuery { value: 64 };
let response: u64 = api
.public(ApiKind::Service("api-service"))
.query(&ok_query)
.get("error")
.await
.expect("This request should be successful");
assert_eq!(ok_query.value, response);
let err_query = PingQuery { value: 63 };
let error: api::Error = api
.public(ApiKind::Service("api-service"))
.query(&err_query)
.get::<u64>("error")
.await
.expect_err("Should return error.");
assert_eq!(error.http_code, api::HttpStatusCode::BAD_REQUEST);
assert_eq!(error.body.docs_uri, "http://some-docs.com");
assert_eq!(error.body.title, "Test endpoint error");
assert_eq!(
error.body.detail,
format!("Test endpoint failed with query: {}", err_query.value)
);
assert_eq!(
error.body.source,
format!("{}:{}", SERVICE_ID, SERVICE_NAME)
);
assert_eq!(error.body.error_code, Some(42));
}
#[tokio::test]
async fn submit_tx_when_service_is_stopped() {
let (mut testkit, api) = init_testkit();
let keys = testkit.us().service_keypair();
let tx = keys.stop_service(SUPERVISOR_INSTANCE_ID, SERVICE_ID);
let block = testkit.create_block_with_transaction(tx);
block[0].status().expect("Cannot stop service");
let ping = PingQuery { value: 64 };
let err = api
.public(ApiKind::Service("api-service"))
.query(&ping)
.post::<()>("submit-tx")
.await
.expect_err("Request to the valid endpoint should fail");
assert_eq!(err.http_code, api::HttpStatusCode::SERVICE_UNAVAILABLE);
assert_eq!(err.body.title, "Service is not active");
let block = testkit.create_block();
assert!(block.is_empty());
}
#[tokio::test]
async fn submit_tx_when_service_is_frozen() {
let (mut testkit, api) = init_testkit();
let keys = testkit.us().service_keypair();
let tx = keys.freeze_service(SUPERVISOR_INSTANCE_ID, SERVICE_ID);
let block = testkit.create_block_with_transaction(tx);
block[0].status().expect("Cannot freeze service");
let ping = PingQuery { value: 64 };
let err = api
.public(ApiKind::Service("api-service"))
.query(&ping)
.post::<()>("submit-tx")
.await
.expect_err("Request to the valid endpoint should fail");
assert_eq!(err.http_code, api::HttpStatusCode::SERVICE_UNAVAILABLE);
assert_eq!(err.body.title, "Service is not active");
let block = testkit.create_block();
assert!(block.is_empty());
}
#[tokio::test]
async fn error_after_migration() {
let (mut testkit, api) = init_testkit();
let keys = testkit.us().service_keypair();
let tx = keys.freeze_service(SUPERVISOR_INSTANCE_ID, SERVICE_ID);
let block = testkit.create_block_with_transaction(tx);
block[0].status().unwrap();
let pong: u64 = api
.public(ApiKind::Service(SERVICE_NAME))
.query(&PingQuery { value: 10 })
.get("ping-pong")
.await
.expect("API should work fine after restart");
assert_eq!(pong, 10);
let tx = keys.start_migration(
SUPERVISOR_INSTANCE_ID,
StartMigration {
instance_id: SERVICE_ID,
new_artifact: ApiServiceV2.artifact_id(),
migration_len: 0,
},
);
let block = testkit.create_block_with_transaction(tx);
block[0].status().unwrap();
let error = api
.public(ApiKind::Service(SERVICE_NAME))
.query(&PingQuery { value: 10 })
.get::<u64>("ping-pong")
.await
.expect_err("API should return errors now");
assert_eq!(error.http_code, api::HttpStatusCode::SERVICE_UNAVAILABLE);
assert_eq!(
error.body.title,
"Service has been upgraded, but its HTTP handlers are not rebooted yet"
);
assert!(error
.body
.detail
.contains("Service `3:api-service` was upgraded to version 2.0.0"));
let new_api = testkit.api();
let pong: u64 = new_api
.public(ApiKind::Service(SERVICE_NAME))
.query(&PingQuery { value: 10 })
.get("ping-pong")
.await
.expect("API should work fine after restart");
assert_eq!(pong, 11);
}
async fn test_no_old_artifact_after_unload(unload: bool) {
let (mut testkit, _) = init_testkit();
let keys = testkit.us().service_keypair();
let tx = keys.freeze_service(SUPERVISOR_INSTANCE_ID, SERVICE_ID);
let block = testkit.create_block_with_transaction(tx);
block[0].status().unwrap();
let tx = keys.start_migration(
SUPERVISOR_INSTANCE_ID,
StartMigration {
instance_id: SERVICE_ID,
new_artifact: ApiServiceV2.artifact_id(),
migration_len: 0,
},
);
let block = testkit.create_block_with_transaction(tx);
block[0].status().unwrap();
if unload {
let tx = keys.unload_artifact(SUPERVISOR_INSTANCE_ID, ApiService.artifact_id());
let block = testkit.create_block_with_transaction(tx);
block[0].status().unwrap();
}
let stopped = testkit.stop();
let runtime = RustRuntime::builder()
.with_factory(Supervisor)
.with_factory(ApiServiceV2); let mut testkit = stopped.resume(runtime);
let api = testkit.api();
let pong: u64 = api
.public(ApiKind::Service(SERVICE_NAME))
.query(&PingQuery { value: 10 })
.get("ping-pong")
.await
.expect("API should work fine after testkit restart");
assert_eq!(pong, 11);
}
#[tokio::test]
async fn no_old_artifact_after_unload() {
test_no_old_artifact_after_unload(true).await;
}
#[tokio::test]
#[should_panic(expected = "artifact `0:api-service:1.0.0` failed to deploy")]
async fn no_old_artifact_without_unload() {
test_no_old_artifact_after_unload(false).await;
}