sofie-http 0.1.0

The HTTP server, areas, routes and the handler model.
Documentation
#![cfg(feature = "jobs")]

use http::StatusCode;
use serde_json::json;
use sofie_core::{Result, SqlValue};
use sofie_db::{Db, Step};
use sofie_http::{App, Area, Routes, Web};
use sofie_testing::{FakeDriver, TestServer};

struct Account;

impl sofie_http::AreaPrincipal for Account {
    const ACCESS: sofie_http::Access = sofie_http::Access::Owned;
    type Record = Self;
}

struct Members;

impl Area for Members {
    const NAME: &'static str = "members";
    const PREFIX: &'static str = "/members";
    const SIGN_IN: &'static str = "/sign-in";
    type Principal = Account;
}

async fn export() -> Result<()> {
    Ok(())
}

fn queued_values(transactions: &[Vec<Step>]) -> Vec<SqlValue> {
    transactions
        .iter()
        .flatten()
        .flat_map(|step| match step {
            Step::Statement { params, .. } => params.clone(),
            Step::Script(_) => Vec::new(),
        })
        .flat_map(|value| match value {
            SqlValue::Array(values) => values,
            other => vec![other],
        })
        .collect()
}

#[tokio::test]
async fn a_route_queues_its_job_with_the_input_and_path_parameters() {
    let driver = FakeDriver::default();
    let db = Db::new(driver.clone());
    sofie_db::name_job(&db, "reports.actions.export", export);
    let app = App::new(db).area("reports", |routes: &mut Routes<Web>| {
        routes.queue("/reports/{report}/export", export);
    });
    let server = TestServer::start(app).await;

    let answered = server
        .post("/reports/r1/export")
        .header("sec-fetch-site", "same-origin")
        .json(&json!({ "format": "csv", "report": "posted" }))
        .await;

    assert_eq!(answered.status, StatusCode::NO_CONTENT, "{}", answered.body);
    let values = queued_values(&driver.take_transactions());
    assert!(
        values.contains(&SqlValue::Text("reports.actions.export".to_owned())),
        "{values:?}"
    );
    let payload = values
        .iter()
        .find_map(|value| match value {
            SqlValue::Json(payload) => Some(payload.clone()),
            SqlValue::Text(text) => serde_json::from_str(text).ok(),
            _ => None,
        })
        .expect("the job's payload is written");
    assert_eq!(payload, json!({ "format": "csv", "report": "r1" }));
}

#[tokio::test]
async fn an_area_whose_principals_own_their_records_queues_no_job() {
    let app =
        App::new(Db::new(FakeDriver::default())).area("reports", |routes: &mut Routes<Members>| {
            routes.queue("/reports/export", export);
        });
    let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();

    let refused = app.serve(listener, async {}).await.unwrap_err();

    assert!(
        refused.to_string().contains("reaches every record"),
        "{refused}"
    );
}