first commit

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-28 09:16:54 +01:00
commit 32a7ba8e8f
168 files changed
+123856

No files matched your search

+228
View File
@@ -0,0 +1,228 @@
//! Published documentation must match live routes and remain local to this server.
use axum::{
Router,
body::Body,
http::{Request, StatusCode},
};
use http_body_util::BodyExt;
use mikrotik_backup::{AppState, api, config::Config, db::Db};
use serde_json::Value;
use std::collections::{HashMap, HashSet};
use tower::ServiceExt;
fn fixture(prefix: &str) -> (tempfile::TempDir, Router) {
let dir = tempfile::tempdir().unwrap();
let config = Config::from_values(&HashMap::from([
("DATA_DIR".into(), dir.path().display().to_string()),
("DEFAULT_ADMIN_PASSWORD".into(), "docs-test-password".into()),
("API_PREFIX".into(), prefix.into()),
("APP_NAME".into(), "MikroTik API contract test".into()),
]))
.unwrap();
let db = Db::open(&config).unwrap();
(dir, api::router(AppState::new(config, db)))
}
fn check_references(value: &Value, document: &Value) {
match value {
Value::Object(object) => {
if let Some(reference) = object.get("$ref").and_then(Value::as_str) {
assert!(reference.starts_with("#/components/"));
assert!(
document.pointer(&reference[1..]).is_some(),
"Broken ref {reference}"
);
}
for child in object.values() {
check_references(child, document);
}
}
Value::Array(items) => {
for child in items {
check_references(child, document);
}
}
_ => {}
}
}
#[tokio::test]
async fn openapi_covers_live_routes_and_honors_custom_prefix() {
let (_dir, app) = fixture("/v2/api");
let response = app
.clone()
.oneshot(Request::get("/openapi.json").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(response.headers()["content-type"], "application/json");
let bytes = response.into_body().collect().await.unwrap().to_bytes();
let document: Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(document["openapi"], "3.1.0");
assert_eq!(document["info"]["title"], "MikroTik API contract test");
assert_eq!(document["info"]["version"], env!("CARGO_PKG_VERSION"));
assert_eq!(document["servers"][0]["url"], "/v2/api");
assert_eq!(
document["tags"],
serde_json::json!([
{"name": "Account"},
{"name": "Devices"},
{"name": "Backups"},
{"name": "Settings"},
{"name": "Logs"},
{"name": "System"}
])
);
assert!(
document["info"]["description"]
.as_str()
.unwrap()
.starts_with("Documentation for the live API.")
);
assert_eq!(
document["paths"]["/auth/login"]["post"]["summary"],
"Log in"
);
assert_eq!(
document["paths"]["/routers"]["get"]["tags"],
serde_json::json!(["Devices"])
);
assert_eq!(
document["components"]["securitySchemes"]["OAuth2PasswordBearer"]["flows"]["password"]["tokenUrl"],
"/v2/api/auth/login"
);
check_references(&document, &document);
let mut operation_ids = HashSet::new();
let mut operation_count = 0;
for (path, methods) in document["paths"].as_object().unwrap() {
let concrete = ["id", "backup_id", "left", "right", "days"]
.into_iter()
.fold(path.clone(), |path, name| {
path.replace(&format!("{{{name}}}"), "1")
});
for (method, operation) in methods.as_object().unwrap() {
operation_count += 1;
assert!(operation_ids.insert(operation["operationId"].as_str().unwrap()));
let public = ["/health", "/auth/login", "/auth/register"].contains(&path.as_str());
assert_eq!(operation.get("security").is_some(), public);
let response = app
.clone()
.oneshot(
Request::builder()
.method(method.to_ascii_uppercase().as_str())
.uri(format!("/v2/api{concrete}"))
.header("content-type", "application/json")
.body(Body::from("{}"))
.unwrap(),
)
.await
.unwrap();
assert_ne!(
response.status(),
StatusCode::NOT_FOUND,
"Undocumented route mismatch: {method} {path}"
);
assert_ne!(
response.status(),
StatusCode::METHOD_NOT_ALLOWED,
"Undocumented method mismatch: {method} {path}"
);
if !public {
assert_eq!(
response.status(),
StatusCode::UNAUTHORIZED,
"Protected route {method} {path}"
);
}
}
}
assert_eq!(operation_count, 36);
assert_eq!(
document["components"]["schemas"]["Device"]["properties"]["ssh_password"]["type"],
"null"
);
assert!(
document["components"]["schemas"]["DeviceUpdate"]["properties"]
.get("use_global_switchos_credentials")
.is_some()
);
}
#[tokio::test]
async fn docs_and_embedded_assets_work_without_a_cdn() {
let (_dir, app) = fixture("/api");
let mut documentation_html = None;
for path in ["/docs", "/docs/"] {
let response = app
.clone()
.oneshot(Request::get(path).body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
assert!(
response.headers()["content-security-policy"]
.to_str()
.unwrap()
.contains("script-src 'self'")
);
let bytes = response.into_body().collect().await.unwrap().to_bytes();
let html = std::str::from_utf8(&bytes).unwrap();
assert!(html.contains("<html lang=\"en\">"));
assert!(html.contains("aria-label=\"API documentation\""));
assert!(html.contains("Back to the application"));
assert!(html.contains(env!("DOCS_JS_ROUTE")));
assert!(html.contains(env!("DOCS_CSS_ROUTE")));
assert!(!html.contains("?v="));
assert_eq!(html.matches("<script ").count(), 1);
assert!(!html.contains("https://"));
if let Some(previous) = &documentation_html {
assert_eq!(html, previous);
} else {
documentation_html = Some(html.to_owned());
}
}
let documentation_html = documentation_html.unwrap();
for (path, mime) in [
(env!("DOCS_JS_ROUTE"), "text/javascript"),
(env!("DOCS_CSS_ROUTE"), "text/css"),
] {
let response = app
.clone()
.oneshot(Request::get(path).body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
assert!(
response.headers()["content-type"]
.to_str()
.unwrap()
.starts_with(mime)
);
assert_eq!(
response.headers()["cache-control"].to_str().unwrap(),
"public, max-age=31536000, immutable"
);
let bytes = response.into_body().collect().await.unwrap().to_bytes();
assert!(bytes.len() > 1000);
assert!(documentation_html.contains(&format!("\"{path}\"")));
if path.ends_with(".js") {
let javascript = std::str::from_utf8(&bytes).unwrap();
assert!(javascript.contains("validatorUrl: null"));
assert!(javascript.contains("queryConfigEnabled: false"));
assert!(javascript.contains("persistAuthorization: false"));
assert!(
javascript.contains("API documentation can send requests only to this instance.")
);
assert!(javascript.ends_with(include_str!("../docs/api-ui/init.js")));
} else {
let stylesheet = std::str::from_utf8(&bytes).unwrap();
assert!(stylesheet.ends_with(include_str!("../docs/api-ui/style.css")));
}
}
let response = app
.oneshot(Request::get("/redoc").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::PERMANENT_REDIRECT);
assert_eq!(response.headers()["location"], "/docs");
}