mirror of
https://github.com/Shu-Ying/farm_service.git
synced 2026-11-02 10:39:59 +08:00
257 lines
8.7 KiB
Rust
257 lines
8.7 KiB
Rust
use axum::{
|
|
body::{Body, to_bytes},
|
|
http::{Request, StatusCode},
|
|
};
|
|
use farm_service::{
|
|
assets::digest,
|
|
catalogue::{self, CatalogueStore},
|
|
};
|
|
use serde_json::{Value, json};
|
|
use std::{collections::BTreeMap, path::Path};
|
|
use tower::ServiceExt;
|
|
|
|
fn publish(root: &Path, version: &str, plants: &[u8]) -> Value {
|
|
let content = BTreeMap::from([
|
|
("plants.json", plants),
|
|
("metadata.json", b"{\"schema\":1}".as_slice()),
|
|
("assets.zip", b"PK\x03\x04pictures".as_slice()),
|
|
("land-soils.zip", b"PK\x03\x04soils".as_slice()),
|
|
]);
|
|
let files: BTreeMap<_, _> = content
|
|
.iter()
|
|
.map(|(name, bytes)| {
|
|
(
|
|
*name,
|
|
json!({"sha256": digest(bytes), "size_bytes": bytes.len()}),
|
|
)
|
|
})
|
|
.collect();
|
|
let release = digest(&serde_json::to_vec(&files).unwrap());
|
|
let manifest = json!({"schema": 1, "version": version, "release": release,
|
|
"files": files});
|
|
let directory = root.join("releases").join(&release);
|
|
std::fs::create_dir_all(&directory).unwrap();
|
|
for (name, bytes) in content {
|
|
std::fs::write(directory.join(name), bytes).unwrap();
|
|
}
|
|
let bytes = serde_json::to_vec(&manifest).unwrap();
|
|
std::fs::write(directory.join("manifest.json"), &bytes).unwrap();
|
|
std::fs::write(root.join("manifest.json"), bytes).unwrap();
|
|
manifest
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn serves_latest_history_etags_and_verified_memory_snapshot() {
|
|
let directory = tempfile::tempdir().unwrap();
|
|
let old = publish(directory.path(), "one", b"{\"schema\":1,\"plants\":[1]}");
|
|
let latest = publish(directory.path(), "two", b"{\"schema\":1,\"plants\":[2]}");
|
|
let store = CatalogueStore::load(directory.path()).unwrap();
|
|
let app = catalogue::router(Some(store));
|
|
let response = app
|
|
.clone()
|
|
.oneshot(
|
|
Request::builder()
|
|
.uri("/v1/catalogue/manifest.json")
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(response.status(), StatusCode::OK);
|
|
let etag = response.headers()["etag"].clone();
|
|
assert_eq!(
|
|
serde_json::from_slice::<Value>(&to_bytes(response.into_body(), 16384).await.unwrap())
|
|
.unwrap(),
|
|
latest
|
|
);
|
|
let response = app
|
|
.clone()
|
|
.oneshot(
|
|
Request::builder()
|
|
.uri("/v1/catalogue/manifest.json")
|
|
.header("if-none-match", etag)
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(response.status(), StatusCode::NOT_MODIFIED);
|
|
let release = old["release"].as_str().unwrap();
|
|
std::fs::write(
|
|
directory
|
|
.path()
|
|
.join("releases")
|
|
.join(release)
|
|
.join("plants.json"),
|
|
b"changed",
|
|
)
|
|
.unwrap();
|
|
let uri = format!("/v1/catalogue/releases/{release}/plants.json");
|
|
let response = app
|
|
.clone()
|
|
.oneshot(Request::builder().uri(&uri).body(Body::empty()).unwrap())
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(response.status(), StatusCode::OK);
|
|
assert_eq!(
|
|
response.headers()["cache-control"],
|
|
"public, max-age=31536000, immutable"
|
|
);
|
|
let etag = response.headers()["etag"].clone();
|
|
assert_eq!(
|
|
to_bytes(response.into_body(), 4096).await.unwrap(),
|
|
b"{\"schema\":1,\"plants\":[1]}".as_slice()
|
|
);
|
|
let response = app
|
|
.clone()
|
|
.oneshot(
|
|
Request::builder()
|
|
.uri(uri)
|
|
.header("if-none-match", etag)
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(response.status(), StatusCode::NOT_MODIFIED);
|
|
for file in [
|
|
"metadata.json",
|
|
"assets.zip",
|
|
"land-soils.zip",
|
|
"manifest.json",
|
|
] {
|
|
let uri = format!("/v1/catalogue/releases/{release}/{file}");
|
|
let response = app
|
|
.clone()
|
|
.oneshot(Request::builder().uri(uri).body(Body::empty()).unwrap())
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(response.status(), StatusCode::OK);
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn disabled_unknown_paths_and_profile_authentication_remain_separate() {
|
|
let response = catalogue::router(None)
|
|
.oneshot(
|
|
Request::builder()
|
|
.uri("/v1/catalogue/manifest.json")
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
|
|
let directory = tempfile::tempdir().unwrap();
|
|
let manifest = publish(directory.path(), "test", b"{}");
|
|
let store = CatalogueStore::load(directory.path()).unwrap();
|
|
let pool = farm_service::db::open_database(&directory.path().join("test.sqlite3"))
|
|
.await
|
|
.unwrap();
|
|
let app = farm_service::api::router(pool.clone(), "test-only-token".into())
|
|
.merge(catalogue::router(Some(store)));
|
|
let release = manifest["release"].as_str().unwrap();
|
|
for (uri, expected) in [
|
|
(
|
|
format!("/v1/catalogue/releases/{release}/secret.txt"),
|
|
StatusCode::NOT_FOUND,
|
|
),
|
|
(
|
|
"/v1/catalogue/releases/unknown/plants.json".into(),
|
|
StatusCode::NOT_FOUND,
|
|
),
|
|
("/v1/catalogue/manifest.json".into(), StatusCode::OK),
|
|
("/v1/users/qq/123456".into(), StatusCode::UNAUTHORIZED),
|
|
] {
|
|
let response = app
|
|
.clone()
|
|
.oneshot(Request::builder().uri(uri).body(Body::empty()).unwrap())
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(response.status(), expected);
|
|
}
|
|
pool.close().await;
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_missing_corrupt_oversized_or_unbound_release_files() {
|
|
let directory = tempfile::tempdir().unwrap();
|
|
let manifest = publish(directory.path(), "test", b"{}");
|
|
let release = manifest["release"].as_str().unwrap();
|
|
let file = directory
|
|
.path()
|
|
.join("releases")
|
|
.join(release)
|
|
.join("plants.json");
|
|
std::fs::write(&file, b"bad").unwrap();
|
|
assert!(CatalogueStore::load(directory.path()).is_err());
|
|
std::fs::write(&file, b"[]").unwrap();
|
|
assert!(CatalogueStore::load(directory.path()).is_err());
|
|
std::fs::remove_file(&file).unwrap();
|
|
assert!(CatalogueStore::load(directory.path()).is_err());
|
|
std::fs::write(&file, b"{}").unwrap();
|
|
let mut invalid = manifest.clone();
|
|
invalid["files"]["plants.json"]["size_bytes"] = json!(2 * 1024 * 1024 + 1);
|
|
std::fs::write(
|
|
directory.path().join("manifest.json"),
|
|
serde_json::to_vec(&invalid).unwrap(),
|
|
)
|
|
.unwrap();
|
|
assert!(CatalogueStore::load(directory.path()).is_err());
|
|
invalid = manifest.clone();
|
|
invalid["release"] = json!("a".repeat(64));
|
|
std::fs::write(
|
|
directory.path().join("manifest.json"),
|
|
serde_json::to_vec(&invalid).unwrap(),
|
|
)
|
|
.unwrap();
|
|
assert!(CatalogueStore::load(directory.path()).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_unknown_schema_fields_missing_latest_history_and_bad_historical_manifest() {
|
|
for kind in ["schema", "extra", "missing", "label", "history"] {
|
|
let directory = tempfile::tempdir().unwrap();
|
|
let mut manifest = publish(directory.path(), "test", b"{}");
|
|
let release = manifest["release"].as_str().unwrap().to_string();
|
|
match kind {
|
|
"schema" => manifest["schema"] = json!(2),
|
|
"extra" => {
|
|
manifest["files"]["private.txt"] =
|
|
json!({"sha256": "a".repeat(64), "size_bytes": 1})
|
|
}
|
|
"missing" => {
|
|
std::fs::remove_dir_all(directory.path().join("releases").join(&release)).unwrap();
|
|
}
|
|
"label" => manifest["version"] = json!("different-label"),
|
|
"history" => {
|
|
let old = publish(directory.path(), "old", b"[]");
|
|
std::fs::write(
|
|
directory
|
|
.path()
|
|
.join("releases")
|
|
.join(old["release"].as_str().unwrap())
|
|
.join("manifest.json"),
|
|
b"{}",
|
|
)
|
|
.unwrap();
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
std::fs::write(
|
|
directory.path().join("manifest.json"),
|
|
serde_json::to_vec(&manifest).unwrap(),
|
|
)
|
|
.unwrap();
|
|
assert!(
|
|
CatalogueStore::load(directory.path()).is_err(),
|
|
"accepted {kind}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn independently_published_repository_release_is_accepted_by_rust() {
|
|
CatalogueStore::load(&Path::new(env!("CARGO_MANIFEST_DIR")).join("catalogue")).unwrap();
|
|
}
|