use std::error::Error;
use std::num::NonZeroU32;
use std::sync::Arc;
use std::time::Duration;
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use ed25519_dalek::Signer;
use haematite::db::DatabaseConfig;
use haematite::store::NodeCacheBudget;
use serde_json::{Value, json};
use super::tests::{ISSUER, KID, role, signer};
use super::{AUDIENCE, Gate, Unreachable, now};
use crate::serve::protocol::{BranchPage, Operation, Version};
use crate::serve::service::Service;
use crate::serve::storage::{Failure, Storage};
type TestResult = Result<(), Box<dyn Error>>;
const PAGE: NonZeroU32 = NonZeroU32::MIN.saturating_add(9);
pub(super) fn pass(holder: &str, rights: &Value) -> String {
let issued = now();
let header =
URL_SAFE_NO_PAD.encode(json!({"alg": "EdDSA", "typ": "JWT", "kid": KID}).to_string());
let claims = URL_SAFE_NO_PAD.encode(
json!({
"iss": ISSUER, "sub": holder, "aud": AUDIENCE, "iat": issued - 10, "exp": issued + 600,
"holder": {"id": holder, "kind": "person", "responsible": null},
"rights": rights,
})
.to_string(),
);
let signature = URL_SAFE_NO_PAD.encode(
signer()
.sign(format!("{header}.{claims}").as_bytes())
.to_bytes(),
);
format!("{header}.{claims}.{signature}")
}
fn head(service: &Service) -> Result<Version, Box<dyn Error>> {
let answer = service.dispatch(Operation::Branches(BranchPage {
after: None,
limit: PAGE,
}))?;
let row = answer["entries"]
.as_array()
.ok_or("branches has no entries")?
.iter()
.find(|row| row["branch"] == "main")
.ok_or("main missing")?;
Ok(serde_json::from_value(row["version"].clone())?)
}
fn change(service: &Service, kind: &str, value: &Value) -> Result<Operation, Box<dyn Error>> {
Ok(serde_json::from_value(json!({
"method": "mutation.apply",
"params": {"request_id": format!("{kind} {value}"), "branch": "main",
"expected": head(service)?, "change": {"kind": kind, "value": value}},
}))?)
}
fn put(service: &Service, collection: &str) -> Result<Operation, Box<dyn Error>> {
change(
service,
"document.put",
&json!({"collection": collection, "document_id": "a", "document": {}}),
)
}
fn service(directory: &std::path::Path) -> Result<Service, Box<dyn Error>> {
let storage = Storage::create(
DatabaseConfig {
data_dir: directory.join("database"),
shard_count: 1,
distributed: None,
executor_threads: Some(1),
node_cache_budget: Some(NodeCacheBudget::Unlimited),
retention: haematite::Retention::KeepAll,
},
NodeCacheBudget::Unlimited,
Duration::from_secs(1),
)?;
let x = URL_SAFE_NO_PAD.encode(signer().verifying_key().as_bytes());
let keys = json!({"keys": [{"kty": "OKP", "crv": "Ed25519", "kid": KID, "x": x, "alg": "EdDSA", "use": "sig"}]});
let service = Service::new(storage, PAGE)?.with_lys(Gate::new(
&keys.to_string(),
ISSUER,
"wards",
Arc::new(Unreachable),
)?);
for collection in ["patients", "unplaced"] {
let define = change(
&service,
"collection.put",
&json!({"collection": collection, "schema": null}),
)?;
service.dispatch(define)?;
}
Ok(service)
}
fn refused_on(answer: Result<(), Failure>, id: &str) -> TestResult {
let refused = answer.err().ok_or_else(|| format!("{id} was allowed"))?;
assert_eq!(refused.code, "permission_refused", "{id}");
let shape: Value = serde_json::from_str(&refused.message)?;
assert_eq!(shape["refused"]["needed"]["id"], id);
Ok(())
}
#[test]
fn a_writer_putting_into_an_absent_collection_is_told_it_is_missing() -> TestResult {
let directory = tempfile::tempdir()?;
let service = service(directory.path())?;
let writer = pass("writer", &role(&["read", "query", "write"], "writer-grant"));
let absent = put(&service, "nothere")?;
service.authorize(Some(&writer), &absent, None)?;
let missing = service
.dispatch(absent)
.err()
.ok_or("a put into an absent collection was applied")?;
assert_eq!(missing.code, "collection_missing");
let observer = pass("observer", &role(&["read", "query"], "observer-grant"));
refused_on(
service.authorize(Some(&observer), &put(&service, "nothere")?, None),
"wards.nothere",
)?;
refused_on(
service.authorize(Some(&writer), &put(&service, "unplaced")?, None),
"wards.unplaced",
)?;
service.authorize(Some(&writer), &put(&service, "patients")?, None)?;
let nowhere: Operation = serde_json::from_value(json!({
"method": "mutation.apply",
"params": {"request_id": "nowhere", "branch": "nowhere", "expected": head(&service)?,
"change": {"kind": "document.put",
"value": {"collection": "patients", "document_id": "a", "document": {}}}},
}))?;
service.authorize(Some(&writer), &nowhere, None)?;
let missing = service
.dispatch(nowhere)
.err()
.ok_or("a put on an absent branch was applied")?;
assert_eq!(missing.code, "branch_missing");
Ok(())
}