1use std::sync::Arc;
18
19use axum::extract::{Path, State};
20use axum::http::header;
21use axum::http::StatusCode;
22use axum::response::{IntoResponse, Response};
23use axum::routing::{get, post};
24use axum::Json;
25use mongreldb_core::schema::{Schema, TypeId};
26use mongreldb_core::{Database, Value};
27use mongreldb_query::{ExternalTableModule, MongrelSession};
28use serde::Deserialize;
29use serde_json::json;
30
31mod kit;
32mod procedure;
33mod trigger;
34
35fn status_for_error(e: &mongreldb_core::MongrelError) -> StatusCode {
40 use mongreldb_core::MongrelError;
41 match e {
42 MongrelError::AuthRequired | MongrelError::InvalidCredentials { .. } => {
43 StatusCode::UNAUTHORIZED
44 }
45 MongrelError::AuthNotRequired => StatusCode::BAD_REQUEST,
46 MongrelError::PermissionDenied { .. } => StatusCode::FORBIDDEN,
47 _ => StatusCode::INTERNAL_SERVER_ERROR,
48 }
49}
50
51fn status_for_query_error(e: &mongreldb_query::MongrelQueryError) -> StatusCode {
54 use mongreldb_query::MongrelQueryError;
55 match e {
56 MongrelQueryError::Core(core) => status_for_error(core),
57 _ => StatusCode::INTERNAL_SERVER_ERROR,
58 }
59}
60
61struct AppState {
62 db: Arc<Database>,
63 idem: kit::IdempotencyStore,
64 external_modules: Vec<Arc<dyn ExternalTableModule>>,
65 auth_token: Option<String>,
66 user_auth: bool,
68}
69
70pub fn build_app(db: Arc<Database>) -> axum::Router {
71 build_app_with_config(db, std::iter::empty::<Arc<dyn ExternalTableModule>>(), None, None)
72}
73
74pub fn build_app_with_external_modules(
75 db: Arc<Database>,
76 external_modules: impl IntoIterator<Item = Arc<dyn ExternalTableModule>>,
77) -> axum::Router {
78 build_app_with_config(db, external_modules, None, None)
79}
80
81pub fn build_app_with_config(
83 db: Arc<Database>,
84 external_modules: impl IntoIterator<Item = Arc<dyn ExternalTableModule>>,
85 auth_token: Option<String>,
86 max_connections: Option<usize>,
87) -> axum::Router {
88 build_app_full(db, external_modules, auth_token, max_connections, false)
89}
90
91pub fn build_app_full(
93 db: Arc<Database>,
94 external_modules: impl IntoIterator<Item = Arc<dyn ExternalTableModule>>,
95 auth_token: Option<String>,
96 max_connections: Option<usize>,
97 user_auth: bool,
98) -> axum::Router {
99 let state = Arc::new(AppState {
100 idem: kit::IdempotencyStore::new(db.root()),
101 db,
102 external_modules: external_modules.into_iter().collect(),
103 auth_token,
104 user_auth,
105 });
106 let router = axum::Router::new()
107 .route("/health", get(health))
108 .route("/tables", get(list_tables).post(create_table))
109 .route("/tables/{name}", axum::routing::delete(drop_table))
110 .route("/tables/{name}/put", post(put_row))
111 .route("/tables/{name}/count", get(count))
112 .route("/tables/{name}/commit", post(commit))
113 .route("/sql", post(sql))
114 .route("/txn", post(txn))
115 .route("/procedures", get(procedure::list).post(procedure::create))
116 .route(
117 "/procedures/{name}",
118 get(procedure::describe)
119 .put(procedure::replace)
120 .delete(procedure::drop_procedure),
121 )
122 .route("/procedures/{name}/call", post(procedure::call))
123 .route("/triggers", get(trigger::list).post(trigger::create))
124 .route(
125 "/triggers/{name}",
126 get(trigger::describe)
127 .put(trigger::replace)
128 .delete(trigger::drop_trigger),
129 )
130 .route("/kit/schema", get(kit::schema_all))
132 .route("/kit/schema/{table}", get(kit::schema_one))
133 .route("/kit/txn", post(kit::kit_txn))
134 .route("/kit/query", post(kit::kit_query))
135 .route("/kit/create_table", post(kit::kit_create_table))
136 .route("/kit/procedures/{name}/call", post(procedure::kit_call))
137 .route("/compact", post(compact_all))
138 .route("/tables/{name}/compact", post(compact_table))
139 .route("/wal/stream", get(wal_stream))
140 .route("/events", get(events_stream))
141 .with_state(state.clone());
142
143 let router = if state.auth_token.is_some() || state.user_auth {
145 router.layer(axum::middleware::from_fn_with_state(
146 state.clone(),
147 auth_middleware,
148 ))
149 } else {
150 router
151 };
152
153 if let Some(max) = max_connections {
155 router.layer(tower::limit::ConcurrencyLimitLayer::new(max))
156 } else {
157 router
158 }
159}
160
161async fn auth_middleware(
168 axum::extract::State(state): axum::extract::State<Arc<AppState>>,
169 mut req: axum::extract::Request,
170 next: axum::middleware::Next,
171) -> Result<axum::response::Response, axum::http::StatusCode> {
172 let header = req
173 .headers()
174 .get("authorization")
175 .and_then(|v| v.to_str().ok())
176 .unwrap_or("");
177
178 if let Some(token) = &state.auth_token {
180 if let Some(provided) = header.strip_prefix("Bearer ") {
181 if provided == token {
182 return Ok(next.run(req).await);
183 }
184 }
185 }
186
187 if state.user_auth {
189 if let Some(encoded) = header.strip_prefix("Basic ") {
190 if let Ok(decoded) = base64_decode(encoded) {
191 if let Ok(creds) = std::str::from_utf8(&decoded) {
192 if let Some((username, password)) = creds.split_once(':') {
193 if let Ok(Some(_user)) = state.db.verify_user(username, password) {
194 if let Some(principal) = state.db.resolve_principal(username) {
196 req.extensions_mut().insert(principal);
197 }
198 return Ok(next.run(req).await);
199 }
200 }
201 }
202 }
203 }
204 }
205
206 Err(axum::http::StatusCode::UNAUTHORIZED)
207}
208
209fn base64_decode(input: &str) -> Result<Vec<u8>, ()> {
211 const TABLE: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
212 let input: Vec<u8> = input.bytes().filter(|&b| b != b'\n' && b != b'\r' && b != b' ').collect();
213 let mut out = Vec::with_capacity(input.len() * 3 / 4);
214 let mut buf = 0u32;
215 let mut bits = 0u32;
216 for &b in &input {
217 if b == b'=' {
218 break;
219 }
220 let val = TABLE.iter().position(|&t| t == b).ok_or(())? as u32;
221 buf = (buf << 6) | val;
222 bits += 6;
223 if bits >= 8 {
224 bits -= 8;
225 out.push((buf >> bits) as u8);
226 }
227 }
228 Ok(out)
229}
230
231async fn wal_stream(
235 axum::extract::State(state): axum::extract::State<Arc<AppState>>,
236 axum::extract::Query(params): axum::extract::Query<WalStreamParams>,
237) -> Result<Response, StatusCode> {
238 let db_root = state.db.root().to_path_buf();
239 let since = params.since.unwrap_or(0);
240
241 let body = tokio::task::spawn_blocking(move || -> Result<String, String> {
243 let records = mongreldb_core::wal::SharedWal::replay(&db_root).map_err(|e| e.to_string())?;
244 let mut out = String::new();
245 for record in records.iter().filter(|r| r.seq.0 > since) {
246 if let Ok(json) = serde_json::to_string(record) {
247 out.push_str(&json);
248 out.push('\n');
249 }
250 }
251 Ok(out)
252 })
253 .await
254 .map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?;
255
256 let body = body.map_err(|e| {
257 eprintln!("wal_stream error: {e}");
258 StatusCode::INTERNAL_SERVER_ERROR
259 })?;
260
261 Ok((
262 [
263 (header::CONTENT_TYPE, "application/x-ndjson".to_string()),
264 (header::CACHE_CONTROL, "no-cache".to_string()),
265 ],
266 body,
267 )
268 .into_response())
269}
270
271#[derive(serde::Deserialize)]
272struct WalStreamParams {
273 since: Option<u64>,
274}
275
276async fn events_stream(
280 axum::extract::State(state): axum::extract::State<Arc<AppState>>,
281) -> Result<Response, StatusCode> {
282 let mut rx = state.db.subscribe_changes();
283 let body = tokio::task::spawn_blocking(move || {
284 let mut out = String::new();
286 while let Ok(event) = rx.try_recv() {
287 if let Ok(json) = serde_json::to_string(&event) {
288 out.push_str(&json);
289 out.push('\n');
290 }
291 }
292 out
293 })
294 .await
295 .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
296
297 Ok((
298 [
299 (header::CONTENT_TYPE, "application/x-ndjson".to_string()),
300 (header::CACHE_CONTROL, "no-cache".to_string()),
301 ],
302 body,
303 )
304 .into_response())
305}
306
307pub fn spawn_auto_compactor(db: Arc<Database>) {
312 std::thread::Builder::new()
313 .name("mongreldb-auto-compact".into())
314 .spawn(move || loop {
315 std::thread::sleep(std::time::Duration::from_secs(30));
316 for name in db.table_names() {
317 let Ok(handle) = db.table(&name) else {
318 continue;
319 };
320 let mut t = handle.lock();
321 let before = t.run_count();
322 match t.maybe_compact() {
323 Ok(true) => {
324 eprintln!(
325 "[auto-compact] {name}: {} runs -> {}",
326 before,
327 t.run_count()
328 );
329 }
330 Ok(false) => {}
331 Err(e) => {
332 eprintln!("[auto-compact] {name}: compaction failed: {e}");
333 }
334 }
335 }
336 })
337 .expect("spawn auto-compact thread");
338}
339
340async fn health() -> StatusCode {
341 StatusCode::OK
342}
343
344async fn compact_all(State(state): State<Arc<AppState>>) -> (StatusCode, Json<serde_json::Value>) {
346 match state.db.compact() {
347 Ok((compacted, skipped)) => (
348 StatusCode::OK,
349 Json(json!({
350 "status": "ok",
351 "compacted": compacted,
352 "skipped": skipped,
353 })),
354 ),
355 Err(e) => (
356 StatusCode::INTERNAL_SERVER_ERROR,
357 Json(json!({ "status": "error", "message": format!("{e}") })),
358 ),
359 }
360}
361
362async fn compact_table(
364 State(state): State<Arc<AppState>>,
365 Path(name): Path<String>,
366) -> (StatusCode, Json<serde_json::Value>) {
367 match state.db.compact_table(&name) {
368 Ok(true) => (
369 StatusCode::OK,
370 Json(json!({ "status": "compacted", "table": name })),
371 ),
372 Ok(false) => (
373 StatusCode::OK,
374 Json(json!({ "status": "skipped", "table": name, "reason": "fewer than 2 runs" })),
375 ),
376 Err(e) => (
377 StatusCode::INTERNAL_SERVER_ERROR,
378 Json(json!({ "status": "error", "table": name, "message": format!("{e}") })),
379 ),
380 }
381}
382
383#[derive(Deserialize)]
384struct CreateTableRequest {
385 name: String,
386 columns: Vec<ColumnDefJson>,
387}
388
389#[derive(Deserialize)]
390struct ColumnDefJson {
391 id: u16,
392 name: String,
393 ty: String,
394 primary_key: bool,
395 #[serde(default)]
396 nullable: bool,
397}
398
399async fn create_table(
400 State(state): State<Arc<AppState>>,
401 Json(req): Json<CreateTableRequest>,
402) -> Response {
403 let mut columns = Vec::new();
404 for c in &req.columns {
405 let ty = match c.ty.as_str() {
406 "int64" | "bigint" => TypeId::Int64,
407 "float64" | "double" => TypeId::Float64,
408 "bytes" | "varchar" | "text" => TypeId::Bytes,
409 "bool" => TypeId::Bool,
410 other => {
411 return (StatusCode::BAD_REQUEST, format!("unknown type: {other}")).into_response()
412 }
413 };
414 let mut flags = mongreldb_core::schema::ColumnFlags::empty();
415 if c.primary_key {
416 flags = flags.with(mongreldb_core::schema::ColumnFlags::PRIMARY_KEY);
417 }
418 if c.nullable {
419 flags = flags.with(mongreldb_core::schema::ColumnFlags::NULLABLE);
420 }
421 columns.push(mongreldb_core::schema::ColumnDef {
422 id: c.id,
423 name: c.name.clone(),
424 ty,
425 flags, default_value: None,
426 });
427 }
428 let schema = Schema {
429 schema_id: 0,
430 columns,
431 indexes: vec![],
432 colocation: vec![],
433 constraints: Default::default(),
434 clustered: false,
435 };
436 if let Err(msg) = validate_table_name(&req.name) {
437 return (StatusCode::BAD_REQUEST, msg).into_response();
438 }
439 match state.db.create_table(&req.name, schema) {
440 Ok(id) => Json(json!({ "table_id": id })).into_response(),
441 Err(e) => (status_for_error(&e), e.to_string()).into_response(),
442 }
443}
444
445async fn list_tables(State(state): State<Arc<AppState>>) -> Json<Vec<String>> {
446 Json(state.db.table_names())
447}
448
449async fn drop_table(State(state): State<Arc<AppState>>, Path(name): Path<String>) -> Response {
450 match state.db.drop_table(&name) {
451 Ok(_) => {
452 state.idem.clear();
456 StatusCode::OK.into_response()
457 }
458 Err(e) => (status_for_error(&e), e.to_string()).into_response(),
459 }
460}
461
462#[derive(Deserialize)]
463struct PutRequest {
464 row: Vec<serde_json::Value>,
465}
466
467pub(crate) fn json_to_value(v: &serde_json::Value, expected: &TypeId) -> Value {
468 match (v, expected) {
469 (serde_json::Value::Number(n), TypeId::Float64) => {
470 n.as_f64().map(Value::Float64).unwrap_or(Value::Null)
471 }
472 (serde_json::Value::Number(n), TypeId::Int64) => {
473 n.as_i64().map(Value::Int64).unwrap_or(Value::Null)
474 }
475 (serde_json::Value::String(s), TypeId::Bytes) => Value::Bytes(s.as_bytes().to_vec()),
476 (serde_json::Value::String(s), TypeId::Enum { variants }) => {
477 if variants.iter().any(|v| v == s) {
478 Value::Bytes(s.as_bytes().to_vec())
479 } else {
480 Value::Null
481 }
482 }
483 (serde_json::Value::Bool(b), TypeId::Bool) => Value::Bool(*b),
484 (serde_json::Value::Array(arr), TypeId::Embedding { dim }) => {
487 if arr.len() as u32 != *dim {
488 return Value::Null;
489 }
490 let vec: Option<Vec<f32>> = arr
491 .iter()
492 .map(|el| el.as_f64().map(|f| f as f32))
493 .collect();
494 vec.map(Value::Embedding).unwrap_or(Value::Null)
495 }
496 (serde_json::Value::Null, _) => Value::Null,
497 (serde_json::Value::Number(n), _) => {
499 if let Some(i) = n.as_i64() {
500 Value::Int64(i)
501 } else if let Some(f) = n.as_f64() {
502 Value::Float64(f)
503 } else {
504 Value::Null
505 }
506 }
507 (serde_json::Value::String(s), _) => Value::Bytes(s.as_bytes().to_vec()),
508 (serde_json::Value::Bool(b), _) => Value::Bool(*b),
509 _ => Value::Null,
510 }
511}
512
513fn parse_cells(
516 row: &[serde_json::Value],
517 schema: &mongreldb_core::schema::Schema,
518) -> Result<Vec<(u16, Value)>, String> {
519 if row.len() & 1 != 0 {
520 return Err("row must be an even-length array of [col_id, value] pairs".into());
521 }
522 let mut out = Vec::with_capacity(row.len() / 2);
523 for chunk in row.chunks(2) {
524 let col_id = chunk[0]
525 .as_u64()
526 .ok_or("column id must be a non-negative integer")? as u16;
527 let expected = schema
528 .columns
529 .iter()
530 .find(|c| c.id == col_id)
531 .map(|c| c.ty.clone())
532 .ok_or_else(|| format!("unknown column id {col_id}"))?;
533 let val = json_to_value(&chunk[1], &expected);
534 out.push((col_id, val));
535 }
536 Ok(out)
537}
538
539pub(crate) fn validate_table_name(name: &str) -> Result<(), String> {
541 if name.is_empty() {
542 return Err("table name must not be empty".into());
543 }
544 if name.contains('/') || name.contains('\\') || name.contains('\0') {
545 return Err("table name contains invalid characters".into());
546 }
547 Ok(())
548}
549
550async fn put_row(
551 State(state): State<Arc<AppState>>,
552 Path(name): Path<String>,
553 Json(req): Json<PutRequest>,
554) -> Response {
555 let handle = match state.db.table(&name) {
556 Ok(h) => h,
557 Err(e) => return (StatusCode::NOT_FOUND, e.to_string()).into_response(),
558 };
559 let mut g = handle.lock();
560 let schema = g.schema().clone();
561 let row = match parse_cells(&req.row, &schema) {
562 Ok(r) => r,
563 Err(msg) => return (StatusCode::BAD_REQUEST, msg).into_response(),
564 };
565 match g.put(row) {
566 Ok(rid) => Json(json!({ "row_id": rid.0.to_string() })).into_response(),
567 Err(e) => (status_for_error(&e), e.to_string()).into_response(),
568 }
569}
570
571async fn count(State(state): State<Arc<AppState>>, Path(name): Path<String>) -> Response {
572 let handle = match state.db.table(&name) {
573 Ok(h) => h,
574 Err(e) => return (StatusCode::NOT_FOUND, e.to_string()).into_response(),
575 };
576 Json(json!({ "count": handle.lock().count() })).into_response()
577}
578
579async fn commit(State(state): State<Arc<AppState>>, Path(name): Path<String>) -> Response {
580 let handle = match state.db.table(&name) {
581 Ok(h) => h,
582 Err(e) => return (StatusCode::NOT_FOUND, e.to_string()).into_response(),
583 };
584 let mut g = handle.lock();
585 match g.commit() {
586 Ok(epoch) => Json(json!({ "epoch": epoch.0 })).into_response(),
587 Err(e) => (status_for_error(&e), e.to_string()).into_response(),
588 }
589}
590
591#[derive(Deserialize)]
592struct SqlRequest {
593 sql: String,
594 #[serde(default)]
597 format: Option<String>,
598}
599
600async fn sql(State(state): State<Arc<AppState>>, Json(req): Json<SqlRequest>) -> Response {
601 let session = match MongrelSession::open_with_external_modules(
602 Arc::clone(&state.db),
603 state.external_modules.iter().cloned(),
604 ) {
605 Ok(s) => s,
606 Err(e) => return (status_for_query_error(&e), e.to_string()).into_response(),
607 };
608 match session.run(&req.sql).await {
609 Ok(batches) => {
610 if req.format.as_deref() == Some("arrow") {
613 return sql_arrow_response(&batches);
614 }
615
616 sql_json_response(&batches)
618 }
619 Err(e) => (status_for_query_error(&e), e.to_string()).into_response(),
620 }
621}
622
623fn sql_arrow_response(batches: &[arrow::record_batch::RecordBatch]) -> Response {
626 if batches.is_empty() {
627 return StatusCode::OK.into_response();
628 }
629 let schema = batches[0].schema();
630 let mut buf = Vec::new();
631 let mut writer =
632 arrow::ipc::writer::FileWriter::try_new(&mut buf, schema.as_ref()).unwrap();
633 for b in batches {
634 let _ = writer.write(b);
635 }
636 let _ = writer.finish();
637 drop(writer);
638 (
639 [(header::CONTENT_TYPE, "application/vnd.apache.arrow.file")],
640 buf,
641 )
642 .into_response()
643}
644
645fn sql_json_response(batches: &[arrow::record_batch::RecordBatch]) -> Response {
651 if batches.is_empty() {
652 return (
653 [(header::CONTENT_TYPE, "application/json")],
654 b"[]" as &[u8],
655 )
656 .into_response();
657 }
658
659 let mut buf = Vec::new();
660 {
661 let mut writer = arrow::json::writer::ArrayWriter::new(&mut buf);
662 let refs: Vec<&_> = batches.iter().collect();
663 if let Err(e) = writer.write_batches(&refs) {
664 return (
665 StatusCode::INTERNAL_SERVER_ERROR,
666 format!("JSON serialization error: {e}"),
667 )
668 .into_response();
669 }
670 let _ = writer.finish();
671 }
672
673 (
674 [(header::CONTENT_TYPE, "application/json")],
675 buf,
676 )
677 .into_response()
678}
679
680#[derive(Deserialize)]
681struct TxnOp {
682 table: String,
683 op: String,
684 cells: Option<Vec<serde_json::Value>>,
685 row_id: Option<u64>,
686}
687
688#[derive(Deserialize)]
689struct TxnRequest {
690 ops: Vec<TxnOp>,
691}
692
693async fn txn(State(state): State<Arc<AppState>>, Json(req): Json<TxnRequest>) -> Response {
694 let mut parsed: Vec<(String, TxnAction)> = Vec::with_capacity(req.ops.len());
698 for op in &req.ops {
699 match op.op.as_str() {
700 "put" => {
701 let cells_json = match op.cells.as_ref() {
702 Some(c) if !c.is_empty() => c,
703 _ => {
704 return (StatusCode::BAD_REQUEST, "put op requires non-empty cells")
705 .into_response()
706 }
707 };
708 let handle = match state.db.table(&op.table) {
709 Ok(h) => h,
710 Err(e) => return (StatusCode::NOT_FOUND, e.to_string()).into_response(),
711 };
712 let schema = handle.lock().schema().clone();
713 let cells = match parse_cells(cells_json, &schema) {
714 Ok(c) => c,
715 Err(msg) => return (StatusCode::BAD_REQUEST, msg).into_response(),
716 };
717 parsed.push((op.table.clone(), TxnAction::Put(cells)));
718 }
719 "delete" => {
720 let rid = match op.row_id {
721 Some(r) => r,
722 None => {
723 return (StatusCode::BAD_REQUEST, "delete op requires row_id")
724 .into_response()
725 }
726 };
727 parsed.push((op.table.clone(), TxnAction::Delete(rid)));
728 }
729 other => {
730 return (StatusCode::BAD_REQUEST, format!("unknown op: {other}")).into_response()
731 }
732 }
733 }
734
735 let result = state.db.transaction(|t| {
736 for (table, action) in &parsed {
737 match action {
738 TxnAction::Put(cells) => {
739 t.put(table, cells.clone())?;
740 }
741 TxnAction::Delete(rid) => {
742 t.delete(table, mongreldb_core::RowId(*rid))?;
743 }
744 }
745 }
746 Ok(())
747 });
748 match result {
749 Ok(_) => Json(json!({ "status": "committed" })).into_response(),
750 Err(e) => (status_for_error(&e), e.to_string()).into_response(),
751 }
752}
753
754enum TxnAction {
755 Put(Vec<(u16, Value)>),
756 Delete(u64),
757}
758
759#[cfg(test)]
760mod auth_tests {
761 use super::*;
762 use mongreldb_core::Database;
763 use tempfile::tempdir;
764
765 #[tokio::test]
766 async fn auth_rejects_missing_token() {
767 let dir = tempdir().unwrap();
768 let db = Arc::new(Database::create(dir.path()).unwrap());
769 let app = build_app_with_config(db, std::iter::empty(), Some("secret".into()), None);
770 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
771 let addr = listener.local_addr().unwrap();
772 tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
773 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
775
776 let client = reqwest::Client::new();
777 let resp = client
778 .get(format!("http://{addr}/health"))
779 .send()
780 .await
781 .unwrap();
782 assert_eq!(resp.status(), 401);
783 }
784
785 #[tokio::test]
786 async fn auth_accepts_valid_token() {
787 let dir = tempdir().unwrap();
788 let db = Arc::new(Database::create(dir.path()).unwrap());
789 let app = build_app_with_config(db, std::iter::empty(), Some("secret".into()), None);
790 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
791 let addr = listener.local_addr().unwrap();
792 tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
793 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
794
795 let client = reqwest::Client::new();
796 let resp = client
797 .get(format!("http://{addr}/health"))
798 .header("Authorization", "Bearer secret")
799 .send()
800 .await
801 .unwrap();
802 assert_eq!(resp.status(), 200);
803 }
804
805 #[tokio::test]
806 async fn no_auth_when_token_unset() {
807 let dir = tempdir().unwrap();
808 let db = Arc::new(Database::create(dir.path()).unwrap());
809 let app = build_app_with_config(db, std::iter::empty(), None, None);
810 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
811 let addr = listener.local_addr().unwrap();
812 tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
813 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
814
815 let client = reqwest::Client::new();
816 let resp = client
817 .get(format!("http://{addr}/health"))
818 .send()
819 .await
820 .unwrap();
821 assert_eq!(resp.status(), 200);
822 }
823}
824
825#[cfg(test)]
826mod wal_stream_tests {
827 use super::*;
828 use mongreldb_core::Database;
829 use tempfile::tempdir;
830
831 #[tokio::test]
832 async fn wal_stream_returns_records_after_commit() {
833 let dir = tempdir().unwrap();
834 let db = Arc::new(Database::create(dir.path()).unwrap());
835 let table_schema = mongreldb_core::schema::Schema {
836 schema_id: 1,
837 columns: vec![mongreldb_core::schema::ColumnDef {
838 id: 1,
839 name: "id".into(),
840 ty: TypeId::Int64,
841 flags: mongreldb_core::schema::ColumnFlags::empty()
842 .with(mongreldb_core::schema::ColumnFlags::PRIMARY_KEY), default_value: None,
843 }],
844 indexes: vec![],
845 colocation: vec![],
846 constraints: Default::default(),
847 clustered: false,
848 };
849 db.create_table("items", table_schema).unwrap();
850 let handle = db.table("items").unwrap();
852 handle.lock().put(vec![(1, Value::Int64(1))]).unwrap();
853 handle.lock().flush().unwrap();
854
855 let app = build_app(db);
856 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
857 let addr = listener.local_addr().unwrap();
858 tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
859 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
860
861 let resp = reqwest::get(format!("http://{addr}/wal/stream")).await.unwrap();
862 assert_eq!(resp.status(), 200);
863 let body = resp.text().await.unwrap();
864 assert!(!body.is_empty(), "wal_stream should return records");
866 assert!(body.contains("seq"), "response should contain seq field");
867 }
868}