1use crate::Service;
5use crate::auth::{Access, Auth, Principal};
6use gonzalo_core::{
7 ContentHash, CoreError, DeleteResult, Identity, KeyPrefix, PutResult, Record, RecordKey,
8 Revision,
9};
10use gonzalo_proto::v1::{
11 DeleteBlobRequest, DeleteBlobResponse, DeleteRequest, DeleteResponse, GetBlobRequest,
12 GetBlobResponse, GetRequest, GetResponse, GraphLocatedResponse, GraphNamesResponse,
13 GraphQueryRequest, HasBlobRequest, HasBlobResponse, ListBlobsRequest, ListBlobsResponse,
14 ListRequest, ListResponse, PurgeRequest, PurgeResponse, PutBlobRequest, PutBlobResponse,
15 PutRequest, PutResponse, TicketSyncRequest, TicketSyncResponse,
16 gonzalo_server::{Gonzalo, GonzaloServer},
17};
18use serde::Serialize;
19use std::sync::Arc;
20use tonic::metadata::MetadataMap;
21use tonic::{Request, Response, Status};
22
23const BLOB_NS: &str = "_blobs";
26
27pub struct GrpcAdapter {
30 service: Service,
31 auth: Arc<Auth>,
32}
33
34impl GrpcAdapter {
35 pub fn new(service: Service) -> Self {
37 Self::with_auth(service, Arc::new(Auth::Disabled))
38 }
39
40 pub fn with_auth(service: Service, auth: Arc<Auth>) -> Self {
42 Self { service, auth }
43 }
44
45 #[allow(clippy::result_large_err)]
48 fn authorize(
49 &self,
50 metadata: &MetadataMap,
51 access: Access,
52 namespace: &str,
53 ) -> Result<Principal, Status> {
54 let principal = self.authenticate(metadata)?;
55 self.check_access(&principal, access, namespace)?;
56 Ok(principal)
57 }
58
59 #[allow(clippy::result_large_err)]
64 fn authenticate(&self, metadata: &MetadataMap) -> Result<Principal, Status> {
65 self.auth
66 .authenticate(bearer(metadata))
67 .ok_or_else(|| Status::unauthenticated("invalid or missing token"))
68 }
69
70 #[allow(clippy::result_large_err)]
72 fn check_access(
73 &self,
74 principal: &Principal,
75 access: Access,
76 namespace: &str,
77 ) -> Result<(), Status> {
78 if principal.allows(access, namespace) {
79 Ok(())
80 } else {
81 Err(Status::permission_denied(format!(
82 "principal {:?} lacks {access:?} on namespace {namespace:?}",
83 principal.name()
84 )))
85 }
86 }
87
88 #[allow(clippy::result_large_err)]
93 fn authorize_admin(
94 &self,
95 metadata: &MetadataMap,
96 operation: &str,
97 ) -> Result<Principal, Status> {
98 let principal = self.authenticate(metadata)?;
99 if principal.is_admin() {
100 Ok(principal)
101 } else {
102 Err(Status::permission_denied(format!(
103 "principal {:?} is not an admin; {operation} requires admin",
104 principal.name()
105 )))
106 }
107 }
108
109 #[allow(clippy::result_large_err)]
113 fn authorize_put(
114 &self,
115 metadata: &MetadataMap,
116 r: &PutRequest,
117 ) -> Result<(Principal, Record, Option<Revision>), Status> {
118 let principal = self.authenticate(metadata)?;
119 let record: Record = serde_json::from_slice(&r.record_json)
120 .map_err(|e| Status::invalid_argument(e.to_string()))?;
121 let expected: Option<Revision> = serde_json::from_slice(&r.expected_json)
122 .map_err(|e| Status::invalid_argument(e.to_string()))?;
123 self.check_access(&principal, Access::Write, &record.key.namespace)?;
124 Ok((principal, record, expected))
125 }
126
127 #[allow(clippy::result_large_err)]
131 fn authorize_list(&self, metadata: &MetadataMap, r: ListRequest) -> Result<KeyPrefix, Status> {
132 self.authorize(
133 metadata,
134 Access::Read,
135 r.namespace.as_deref().unwrap_or("*"),
136 )?;
137 Ok(KeyPrefix {
138 namespace: r.namespace,
139 collection: r.collection,
140 })
141 }
142}
143
144fn bearer(metadata: &MetadataMap) -> Option<&str> {
146 metadata
147 .get("authorization")?
148 .to_str()
149 .ok()?
150 .strip_prefix("Bearer ")
151}
152
153fn internal<E: std::fmt::Display>(e: E) -> Status {
158 eprintln!("gonzalod: internal error: {e}");
159 Status::internal("internal error")
160}
161
162#[tonic::async_trait]
163impl Gonzalo for GrpcAdapter {
164 async fn get(&self, req: Request<GetRequest>) -> Result<Response<GetResponse>, Status> {
165 let (metadata, _ext, r) = req.into_parts();
166 self.authorize(&metadata, Access::Read, &r.namespace)?;
167 let key = RecordKey::new(r.namespace, r.collection, r.id);
168 let rec = self.service.get(&key).await.map_err(internal)?;
169 Ok(Response::new(get_response(rec)?))
170 }
171
172 async fn put(&self, req: Request<PutRequest>) -> Result<Response<PutResponse>, Status> {
173 let (metadata, _ext, r) = req.into_parts();
174 let (principal, mut record, expected) = self.authorize_put(&metadata, &r)?;
180 if principal.is_authenticated() {
183 record.meta.author = Identity::new(principal.name());
184 }
185 let outcome = self
186 .service
187 .put(record, expected)
188 .await
189 .map_err(put_error)?;
190 Ok(Response::new(put_response(outcome)?))
191 }
192
193 async fn delete(
194 &self,
195 req: Request<DeleteRequest>,
196 ) -> Result<Response<DeleteResponse>, Status> {
197 let (metadata, _ext, r) = req.into_parts();
198 let principal = self.authenticate(&metadata)?;
210 let expected: Option<Revision> = serde_json::from_slice(&r.expected_json)
211 .map_err(|e| Status::invalid_argument(e.to_string()))?;
212 let claimed: Option<Identity> = if r.author_json.is_empty() {
213 None
214 } else {
215 Some(
216 serde_json::from_slice(&r.author_json)
217 .map_err(|e| Status::invalid_argument(e.to_string()))?,
218 )
219 };
220 self.check_access(&principal, Access::Write, &r.namespace)?;
221 let author = principal.delete_author(claimed);
222 let key = RecordKey::new(r.namespace, r.collection, r.id);
223 let outcome = self
224 .service
225 .delete_as(&key, expected, author)
226 .await
227 .map_err(internal)?;
228 let (outcome, payload_json) = delete_outcome_parts(outcome)?;
229 Ok(Response::new(DeleteResponse {
230 outcome,
231 payload_json,
232 }))
233 }
234
235 async fn list(&self, req: Request<ListRequest>) -> Result<Response<ListResponse>, Status> {
236 let (metadata, _ext, r) = req.into_parts();
237 let prefix = self.authorize_list(&metadata, r)?;
238 let keys = self.service.list(&prefix).await.map_err(internal)?;
239 Ok(Response::new(list_response(&keys)?))
240 }
241
242 async fn get_raw(&self, req: Request<GetRequest>) -> Result<Response<GetResponse>, Status> {
243 let (metadata, _ext, r) = req.into_parts();
244 self.authorize(&metadata, Access::Read, &r.namespace)?;
245 let key = RecordKey::new(r.namespace, r.collection, r.id);
246 let rec = self.service.get_raw(&key).await.map_err(internal)?;
247 Ok(Response::new(get_response(rec)?))
248 }
249
250 async fn list_raw(&self, req: Request<ListRequest>) -> Result<Response<ListResponse>, Status> {
251 let (metadata, _ext, r) = req.into_parts();
252 let prefix = self.authorize_list(&metadata, r)?;
254 let keys = self.service.list_raw(&prefix).await.map_err(internal)?;
255 Ok(Response::new(list_response(&keys)?))
256 }
257
258 async fn put_raw(&self, req: Request<PutRequest>) -> Result<Response<PutResponse>, Status> {
259 let (metadata, _ext, r) = req.into_parts();
260 self.authorize_admin(&metadata, "a raw record write")?;
269 let record: Record = serde_json::from_slice(&r.record_json)
270 .map_err(|e| Status::invalid_argument(e.to_string()))?;
271 let expected: Option<Revision> = serde_json::from_slice(&r.expected_json)
272 .map_err(|e| Status::invalid_argument(e.to_string()))?;
273 let outcome = self
274 .service
275 .put_raw(record, expected)
276 .await
277 .map_err(put_error)?;
278 Ok(Response::new(put_response(outcome)?))
279 }
280
281 async fn purge(&self, req: Request<PurgeRequest>) -> Result<Response<PurgeResponse>, Status> {
282 let (metadata, _ext, r) = req.into_parts();
283 self.authorize_admin(&metadata, "purge")?;
286 let expected: Revision = serde_json::from_slice(&r.expected_json)
287 .map_err(|e| Status::invalid_argument(e.to_string()))?;
288 let key = RecordKey::new(r.namespace, r.collection, r.id);
289 let outcome = self
290 .service
291 .purge(&key, expected)
292 .await
293 .map_err(purge_error)?;
294 let (outcome, payload_json) = delete_outcome_parts(outcome)?;
295 Ok(Response::new(PurgeResponse {
296 outcome,
297 payload_json,
298 }))
299 }
300
301 async fn ticket_sync(
302 &self,
303 req: Request<TicketSyncRequest>,
304 ) -> Result<Response<TicketSyncResponse>, Status> {
305 let (metadata, _ext, r) = req.into_parts();
306 self.authorize(&metadata, Access::Write, "tickets")?;
308 let conn: gonzalo_ticket_config::Connection = serde_json::from_slice(&r.connection_json)
309 .map_err(|e| Status::invalid_argument(e.to_string()))?;
310 let summary = self
311 .service
312 .ticket_sync(&conn, "gonzalod")
313 .await
314 .map_err(|e| match e {
315 crate::service::TicketSyncError::BadRequest(m) => Status::invalid_argument(m),
319 crate::service::TicketSyncError::Internal(m) => internal(m),
320 })?;
321 Ok(Response::new(TicketSyncResponse {
322 imported: summary.imported as u64,
323 updated: summary.updated as u64,
324 unchanged: summary.unchanged as u64,
325 }))
326 }
327
328 async fn graph_definitions(
329 &self,
330 req: Request<GraphQueryRequest>,
331 ) -> Result<Response<GraphLocatedResponse>, Status> {
332 let (metadata, _ext, r) = req.into_parts();
333 self.authorize(&metadata, Access::Read, &r.repo)?;
334 let items = self
335 .service
336 .graph_definitions(&r.repo, &r.view_id, &r.name)
337 .await
338 .map_err(internal)?;
339 Ok(Response::new(located_response(&items)?))
340 }
341
342 async fn graph_references_to(
343 &self,
344 req: Request<GraphQueryRequest>,
345 ) -> Result<Response<GraphLocatedResponse>, Status> {
346 let (metadata, _ext, r) = req.into_parts();
347 self.authorize(&metadata, Access::Read, &r.repo)?;
348 let items = self
349 .service
350 .graph_references_to(&r.repo, &r.view_id, &r.name)
351 .await
352 .map_err(internal)?;
353 Ok(Response::new(located_response(&items)?))
354 }
355
356 async fn graph_callers_of(
357 &self,
358 req: Request<GraphQueryRequest>,
359 ) -> Result<Response<GraphNamesResponse>, Status> {
360 let (metadata, _ext, r) = req.into_parts();
361 self.authorize(&metadata, Access::Read, &r.repo)?;
362 let names = self
363 .service
364 .graph_callers_of(&r.repo, &r.view_id, &r.name)
365 .await
366 .map_err(internal)?;
367 Ok(Response::new(GraphNamesResponse { names }))
368 }
369
370 async fn graph_callees(
371 &self,
372 req: Request<GraphQueryRequest>,
373 ) -> Result<Response<GraphNamesResponse>, Status> {
374 let (metadata, _ext, r) = req.into_parts();
375 self.authorize(&metadata, Access::Read, &r.repo)?;
376 let names = self
377 .service
378 .graph_callees(&r.repo, &r.view_id, &r.name)
379 .await
380 .map_err(internal)?;
381 Ok(Response::new(GraphNamesResponse { names }))
382 }
383
384 async fn graph_impact(
385 &self,
386 req: Request<GraphQueryRequest>,
387 ) -> Result<Response<GraphNamesResponse>, Status> {
388 let (metadata, _ext, r) = req.into_parts();
389 self.authorize(&metadata, Access::Read, &r.repo)?;
390 let names = self
391 .service
392 .graph_impact_names(&r.repo, &r.view_id, &r.name)
393 .await
394 .map_err(internal)?;
395 Ok(Response::new(GraphNamesResponse { names }))
396 }
397
398 async fn put_blob(
399 &self,
400 req: Request<PutBlobRequest>,
401 ) -> Result<Response<PutBlobResponse>, Status> {
402 let (metadata, _ext, r) = req.into_parts();
403 self.authorize(&metadata, Access::Write, BLOB_NS)?;
404 let computed = ContentHash::of(&r.content);
407 if computed.0 != r.hash {
408 return Err(Status::invalid_argument(
409 "blob content does not match the advertised hash",
410 ));
411 }
412 let hash = self.service.put_blob(&r.content).await.map_err(internal)?;
413 Ok(Response::new(PutBlobResponse { hash: hash.0 }))
414 }
415
416 async fn get_blob(
417 &self,
418 req: Request<GetBlobRequest>,
419 ) -> Result<Response<GetBlobResponse>, Status> {
420 let (metadata, _ext, r) = req.into_parts();
421 self.authorize(&metadata, Access::Read, BLOB_NS)?;
422 let found = self
423 .service
424 .get_blob(&ContentHash(r.hash))
425 .await
426 .map_err(internal)?;
427 let resp = match found {
428 Some(content) => GetBlobResponse {
429 found: true,
430 content,
431 },
432 None => GetBlobResponse {
433 found: false,
434 content: Vec::new(),
435 },
436 };
437 Ok(Response::new(resp))
438 }
439
440 async fn has_blob(
441 &self,
442 req: Request<HasBlobRequest>,
443 ) -> Result<Response<HasBlobResponse>, Status> {
444 let (metadata, _ext, r) = req.into_parts();
445 self.authorize(&metadata, Access::Read, BLOB_NS)?;
446 let found = self
447 .service
448 .has_blob(&ContentHash(r.hash))
449 .await
450 .map_err(internal)?;
451 Ok(Response::new(HasBlobResponse { found }))
452 }
453
454 async fn list_blobs(
455 &self,
456 req: Request<ListBlobsRequest>,
457 ) -> Result<Response<ListBlobsResponse>, Status> {
458 let (metadata, _ext, _r) = req.into_parts();
459 self.authorize(&metadata, Access::Read, BLOB_NS)?;
460 let entries = self.service.list_blobs().await.map_err(internal)?;
461 Ok(Response::new(ListBlobsResponse {
462 entries: entries
463 .into_iter()
464 .map(|e| gonzalo_proto::v1::BlobEntry {
465 hash: e.hash.0,
466 modified_unix_ms: e.modified_unix_ms,
467 })
468 .collect(),
469 }))
470 }
471
472 async fn delete_blob(
473 &self,
474 req: Request<DeleteBlobRequest>,
475 ) -> Result<Response<DeleteBlobResponse>, Status> {
476 let (metadata, _ext, r) = req.into_parts();
477 self.authorize(&metadata, Access::Write, BLOB_NS)?;
478 self.service
479 .delete_blob(&ContentHash(r.hash))
480 .await
481 .map_err(internal)?;
482 Ok(Response::new(DeleteBlobResponse {}))
483 }
484}
485
486fn put_error(e: CoreError) -> Status {
491 match e {
492 CoreError::NotFound(key) => Status::failed_precondition(format!("record not found: {key}")),
493 CoreError::Invalid(reason) => Status::invalid_argument(reason),
496 other => internal(other),
497 }
498}
499
500fn purge_error(e: CoreError) -> Status {
505 match e {
506 CoreError::Invalid(reason) => Status::invalid_argument(reason),
507 other => internal(other),
508 }
509}
510
511#[allow(clippy::result_large_err)]
513fn put_response(outcome: PutResult) -> Result<PutResponse, Status> {
514 Ok(match outcome {
515 PutResult::Committed(rev) => PutResponse {
516 outcome: "committed".into(),
517 payload_json: serde_json::to_vec(&rev).map_err(internal)?,
518 },
519 PutResult::Conflict(c) => PutResponse {
520 outcome: "conflict".into(),
521 payload_json: serde_json::to_vec(&*c).map_err(internal)?,
522 },
523 })
524}
525
526#[allow(clippy::result_large_err)]
528fn get_response(rec: Option<Record>) -> Result<GetResponse, Status> {
529 Ok(match rec {
530 Some(rec) => GetResponse {
531 found: true,
532 record_json: serde_json::to_vec(&rec).map_err(internal)?,
533 },
534 None => GetResponse {
535 found: false,
536 record_json: Vec::new(),
537 },
538 })
539}
540
541#[allow(clippy::result_large_err)]
543fn list_response(keys: &[RecordKey]) -> Result<ListResponse, Status> {
544 let keys_json = keys
545 .iter()
546 .map(serde_json::to_vec)
547 .collect::<std::result::Result<Vec<_>, _>>()
548 .map_err(internal)?;
549 Ok(ListResponse { keys_json })
550}
551
552#[allow(clippy::result_large_err)]
555fn delete_outcome_parts(outcome: DeleteResult) -> Result<(String, Vec<u8>), Status> {
556 Ok(match outcome {
557 DeleteResult::Deleted => ("deleted".into(), Vec::new()),
558 DeleteResult::Conflict(c) => (
559 "conflict".into(),
560 serde_json::to_vec(&*c).map_err(internal)?,
561 ),
562 })
563}
564
565#[allow(clippy::result_large_err)]
570fn located_response<T: Serialize>(items: &[T]) -> Result<GraphLocatedResponse, Status> {
571 let items_json = items
572 .iter()
573 .map(serde_json::to_vec)
574 .collect::<std::result::Result<Vec<_>, _>>()
575 .map_err(internal)?;
576 Ok(GraphLocatedResponse { items_json })
577}
578
579pub async fn serve_grpc(
602 listener: tokio::net::TcpListener,
603 service: Service,
604 auth: Arc<Auth>,
605) -> Result<(), tonic::transport::Error> {
606 let max_blob = service.max_blob_size();
607 let adapter = GrpcAdapter::with_auth(service, auth);
608 let incoming = tokio_stream::wrappers::TcpListenerStream::new(listener);
609 tonic::transport::Server::builder()
610 .add_service(GonzaloServer::new(adapter).max_decoding_message_size(max_blob))
612 .serve_with_incoming(incoming)
613 .await
614}
615
616#[cfg(test)]
617mod tests {
618 use super::*;
619 use gonzalo_core::{BlobStore, Identity, Manifest, Meta, RecordKind, Store};
620 use gonzalo_graph::{Located, Symbol, build_rust};
621 use gonzalo_store_fs::FsStore;
622 use std::collections::BTreeMap;
623 use std::sync::Arc;
624
625 async fn seeded_adapter() -> GrpcAdapter {
627 let fs = Arc::new(FsStore::new(tempfile::tempdir().unwrap().keep()));
628 let mut manifest = Manifest::new();
629 for (path, src) in [
630 ("lib.rs", "fn helper() {}"),
631 ("main.rs", "fn main() { helper(); }"),
632 ] {
633 let hash = fs
634 .put_blob(&build_rust(src).to_slice_bytes())
635 .await
636 .unwrap();
637 manifest.insert(path, hash);
638 }
639 let body = manifest.to_body();
640 let record = Record {
641 revision: Revision::initial(body.bytes()),
642 parent: None,
643 body,
644 kind: RecordKind::GraphManifest,
645 meta: Meta {
646 author: Identity::new("tester"),
647 origin_system: "test".into(),
648 created: 0,
649 updated: 0,
650 labels: BTreeMap::new(),
651 },
652 links: Vec::new(),
653 key: Manifest::key("r", "main"),
654 ancestors: Vec::new(),
655 deleted_at: None,
656 deleted_blob: None,
657 deleted_kind: None,
658 };
659 let outcome = fs.put(record, None).await.unwrap();
660 assert!(matches!(outcome, PutResult::Committed(_)));
661 GrpcAdapter::new(Service::new(fs.clone(), fs))
662 }
663
664 fn query(name: &str) -> Request<GraphQueryRequest> {
665 Request::new(GraphQueryRequest {
666 repo: "r".into(),
667 view_id: "main".into(),
668 name: name.into(),
669 })
670 }
671
672 #[tokio::test]
673 async fn graph_definitions_returns_located_json() {
674 let adapter = seeded_adapter().await;
675 let resp = adapter
676 .graph_definitions(query("helper"))
677 .await
678 .unwrap()
679 .into_inner();
680 assert_eq!(resp.items_json.len(), 1);
681 let located: Located<Symbol> = serde_json::from_slice(&resp.items_json[0]).unwrap();
682 assert_eq!(located.path, "lib.rs");
683 assert_eq!(located.item.name, "helper");
684 }
685
686 use std::collections::HashMap;
689
690 fn scoped_auth() -> Arc<Auth> {
692 Arc::new(Auth::Enabled(HashMap::from([
693 (
694 "wtok".to_string(),
695 Principal::new("writer", vec!["memory".into()], vec!["memory".into()]),
696 ),
697 ("atok".to_string(), Principal::admin("admin")),
698 ])))
699 }
700
701 fn with_token<T>(msg: T, token: &str) -> Request<T> {
702 let mut req = Request::new(msg);
703 req.metadata_mut()
704 .insert("authorization", format!("Bearer {token}").parse().unwrap());
705 req
706 }
707
708 fn fs_adapter(auth: Arc<Auth>) -> GrpcAdapter {
709 let fs = Arc::new(FsStore::new(tempfile::tempdir().unwrap().keep()));
710 GrpcAdapter::with_auth(Service::new(fs.clone(), fs), auth)
711 }
712
713 fn get_req(namespace: &str) -> GetRequest {
714 GetRequest {
715 namespace: namespace.into(),
716 collection: "col".into(),
717 id: "x".into(),
718 }
719 }
720
721 fn put_req(namespace: &str, author: &str) -> PutRequest {
722 let record = Record {
723 revision: Revision::initial(b"{}"),
724 parent: None,
725 body: gonzalo_core::Body::Inline(b"{}".to_vec()),
726 kind: RecordKind::MemoryTier,
727 meta: Meta {
728 author: Identity::new(author),
729 origin_system: "test".into(),
730 created: 0,
731 updated: 0,
732 labels: BTreeMap::new(),
733 },
734 links: Vec::new(),
735 key: RecordKey::new(namespace, "col", "x"),
736 ancestors: Vec::new(),
737 deleted_at: None,
738 deleted_blob: None,
739 deleted_kind: None,
740 };
741 PutRequest {
742 record_json: serde_json::to_vec(&record).unwrap(),
743 expected_json: serde_json::to_vec(&Option::<Revision>::None).unwrap(),
744 }
745 }
746
747 fn malformed_put_req() -> PutRequest {
749 PutRequest {
750 record_json: b"definitely not a record".to_vec(),
751 expected_json: serde_json::to_vec(&Option::<Revision>::None).unwrap(),
752 }
753 }
754
755 struct DownStore;
757
758 fn leaky() -> gonzalo_core::CoreError {
759 gonzalo_core::CoreError::Backend("s3://secret-bucket".into())
760 }
761
762 #[async_trait::async_trait]
763 impl Store for DownStore {
764 async fn get(&self, _key: &RecordKey) -> gonzalo_core::Result<Option<Record>> {
765 Err(gonzalo_core::CoreError::Backend(
766 "/var/lib/gonzalo/graphs/secret.sqlite unreachable".into(),
767 ))
768 }
769 async fn put(
770 &self,
771 _record: Record,
772 _expected: Option<Revision>,
773 ) -> gonzalo_core::Result<PutResult> {
774 Err(leaky())
775 }
776 async fn list(
777 &self,
778 _prefix: &gonzalo_core::KeyPrefix,
779 ) -> gonzalo_core::Result<Vec<RecordKey>> {
780 Err(leaky())
781 }
782 async fn delete_as(
783 &self,
784 _key: &RecordKey,
785 _expected: Option<Revision>,
786 _author: Option<Identity>,
787 ) -> gonzalo_core::Result<DeleteResult> {
788 Err(leaky())
789 }
790 async fn get_raw(&self, _key: &RecordKey) -> gonzalo_core::Result<Option<Record>> {
791 Err(leaky())
792 }
793 async fn list_raw(
794 &self,
795 _prefix: &gonzalo_core::KeyPrefix,
796 ) -> gonzalo_core::Result<Vec<RecordKey>> {
797 Err(leaky())
798 }
799 async fn put_raw(
800 &self,
801 _record: Record,
802 _expected: Option<Revision>,
803 ) -> gonzalo_core::Result<PutResult> {
804 Err(leaky())
805 }
806 async fn purge(
807 &self,
808 _key: &RecordKey,
809 _expected: Revision,
810 ) -> gonzalo_core::Result<DeleteResult> {
811 Err(leaky())
812 }
813 }
814
815 #[tokio::test]
816 async fn put_authenticates_before_deserializing() {
817 let adapter = fs_adapter(scoped_auth());
820 let err = adapter
821 .put(Request::new(malformed_put_req()))
822 .await
823 .unwrap_err();
824 assert_eq!(err.code(), tonic::Code::Unauthenticated);
825 }
826
827 #[tokio::test]
828 async fn put_malformed_body_is_invalid_argument_not_internal() {
829 let adapter = fs_adapter(scoped_auth());
832 let err = adapter
833 .put(with_token(malformed_put_req(), "wtok"))
834 .await
835 .unwrap_err();
836 assert_eq!(err.code(), tonic::Code::InvalidArgument);
837 }
838
839 #[test]
840 fn an_invalid_call_is_invalid_argument_not_internal() {
841 let status = put_error(CoreError::Invalid(
845 gonzalo_core::CONSUMER_TOMBSTONE_REJECTED.to_string(),
846 ));
847 assert_eq!(status.code(), tonic::Code::InvalidArgument);
848 assert!(status.message().contains("delete_as"), "{status:?}");
849 }
850
851 #[tokio::test]
852 async fn backend_error_is_opaque() {
853 let fs = Arc::new(FsStore::new(tempfile::tempdir().unwrap().keep()));
856 let adapter = GrpcAdapter::new(Service::new(Arc::new(DownStore), fs));
857 let err = adapter.get(Request::new(get_req("any"))).await.unwrap_err();
858 assert_eq!(err.code(), tonic::Code::Internal);
859 assert_eq!(err.message(), "internal error");
860 assert!(!err.message().contains("secret"));
861 }
862
863 #[tokio::test]
864 async fn missing_token_is_unauthenticated() {
865 let adapter = fs_adapter(scoped_auth());
866 let err = adapter
867 .get(Request::new(get_req("memory")))
868 .await
869 .unwrap_err();
870 assert_eq!(err.code(), tonic::Code::Unauthenticated);
871 }
872
873 #[tokio::test]
874 async fn read_is_allowed_in_scope_denied_out_of_scope() {
875 let adapter = fs_adapter(scoped_auth());
876 assert!(
878 adapter
879 .get(with_token(get_req("memory"), "wtok"))
880 .await
881 .is_ok()
882 );
883 let err = adapter
885 .get(with_token(get_req("secrets"), "wtok"))
886 .await
887 .unwrap_err();
888 assert_eq!(err.code(), tonic::Code::PermissionDenied);
889 }
890
891 #[tokio::test]
892 async fn write_is_scoped_and_author_is_stamped() {
893 let adapter = fs_adapter(scoped_auth());
894 let err = adapter
896 .put(with_token(put_req("secrets", "writer"), "wtok"))
897 .await
898 .unwrap_err();
899 assert_eq!(err.code(), tonic::Code::PermissionDenied);
900
901 adapter
904 .put(with_token(put_req("memory", "forged"), "wtok"))
905 .await
906 .unwrap();
907 let resp = adapter
908 .get(with_token(get_req("memory"), "wtok"))
909 .await
910 .unwrap()
911 .into_inner();
912 let record: Record = serde_json::from_slice(&resp.record_json).unwrap();
913 assert_eq!(record.meta.author, Identity::new("writer"));
914 }
915
916 #[tokio::test]
917 async fn list_without_namespace_requires_admin() {
918 let adapter = fs_adapter(scoped_auth());
919 let scoped = adapter
920 .list(with_token(ListRequest::default(), "wtok"))
921 .await
922 .unwrap_err();
923 assert_eq!(scoped.code(), tonic::Code::PermissionDenied);
924 assert!(
926 adapter
927 .list(with_token(ListRequest::default(), "atok"))
928 .await
929 .is_ok()
930 );
931 }
932
933 #[tokio::test]
936 async fn grpc_blob_roundtrip_open() {
937 let fs = Arc::new(FsStore::new(tempfile::tempdir().unwrap().keep()));
938 let adapter = GrpcAdapter::new(Service::new(fs.clone(), fs));
939 let content = b"grpc blob body".to_vec();
940 let hash = gonzalo_core::ContentHash::of(&content).0;
941
942 let put = adapter
943 .put_blob(Request::new(PutBlobRequest {
944 hash: hash.clone(),
945 content: content.clone(),
946 }))
947 .await
948 .unwrap()
949 .into_inner();
950 assert_eq!(put.hash, hash);
951
952 let got = adapter
953 .get_blob(Request::new(GetBlobRequest { hash: hash.clone() }))
954 .await
955 .unwrap()
956 .into_inner();
957 assert!(got.found);
958 assert_eq!(got.content, content);
959
960 let listed = adapter
961 .list_blobs(Request::new(ListBlobsRequest {}))
962 .await
963 .unwrap()
964 .into_inner();
965 assert_eq!(listed.entries.len(), 1);
966 assert_eq!(listed.entries[0].hash, hash);
967 assert_ne!(
968 listed.entries[0].modified_unix_ms, 0,
969 "a zero timestamp means the substrate never reported one"
970 );
971
972 adapter
973 .delete_blob(Request::new(DeleteBlobRequest { hash: hash.clone() }))
974 .await
975 .unwrap();
976 let gone = adapter
977 .get_blob(Request::new(GetBlobRequest { hash }))
978 .await
979 .unwrap()
980 .into_inner();
981 assert!(!gone.found);
982 }
983
984 #[tokio::test]
985 async fn grpc_put_blob_hash_mismatch_is_invalid_argument() {
986 let fs = Arc::new(FsStore::new(tempfile::tempdir().unwrap().keep()));
987 let adapter = GrpcAdapter::new(Service::new(fs.clone(), fs));
988 let err = adapter
989 .put_blob(Request::new(PutBlobRequest {
990 hash: gonzalo_core::ContentHash::of(b"not the body").0,
991 content: b"the body".to_vec(),
992 }))
993 .await
994 .unwrap_err();
995 assert_eq!(err.code(), tonic::Code::InvalidArgument);
996 }
997
998 #[tokio::test]
1002 async fn grpc_has_blob_reports_presence() {
1003 let fs = Arc::new(FsStore::new(tempfile::tempdir().unwrap().keep()));
1004 let adapter = GrpcAdapter::new(Service::new(fs.clone(), fs));
1005 let content = b"grpc presence".to_vec();
1006 let hash = gonzalo_core::ContentHash::of(&content).0;
1007
1008 let before = adapter
1009 .has_blob(Request::new(HasBlobRequest { hash: hash.clone() }))
1010 .await
1011 .unwrap()
1012 .into_inner();
1013 assert!(!before.found, "nothing stored yet");
1014
1015 adapter
1016 .put_blob(Request::new(PutBlobRequest {
1017 hash: hash.clone(),
1018 content,
1019 }))
1020 .await
1021 .unwrap();
1022
1023 let after = adapter
1024 .has_blob(Request::new(HasBlobRequest { hash }))
1025 .await
1026 .unwrap()
1027 .into_inner();
1028 assert!(after.found, "the blob just written is present");
1029 }
1030
1031 #[tokio::test]
1034 async fn grpc_has_blob_requires_blobs_scope() {
1035 let adapter = fs_adapter(scoped_auth());
1037 let content = b"scoped presence".to_vec();
1038 let hash = gonzalo_core::ContentHash::of(&content).0;
1039 adapter
1040 .put_blob(with_token(
1041 PutBlobRequest {
1042 hash: hash.clone(),
1043 content,
1044 },
1045 "atok",
1046 ))
1047 .await
1048 .unwrap();
1049
1050 let denied = adapter
1051 .has_blob(with_token(HasBlobRequest { hash: hash.clone() }, "wtok"))
1052 .await
1053 .unwrap_err();
1054 assert_eq!(denied.code(), tonic::Code::PermissionDenied);
1055
1056 let allowed = adapter
1057 .has_blob(with_token(HasBlobRequest { hash }, "atok"))
1058 .await
1059 .unwrap()
1060 .into_inner();
1061 assert!(allowed.found);
1062 }
1063
1064 #[tokio::test]
1065 async fn grpc_blob_ops_require_blobs_scope() {
1066 let adapter = fs_adapter(scoped_auth());
1068 let content = b"scoped".to_vec();
1069 let hash = gonzalo_core::ContentHash::of(&content).0;
1070
1071 let denied = adapter
1072 .put_blob(with_token(
1073 PutBlobRequest {
1074 hash: hash.clone(),
1075 content: content.clone(),
1076 },
1077 "wtok",
1078 ))
1079 .await
1080 .unwrap_err();
1081 assert_eq!(denied.code(), tonic::Code::PermissionDenied);
1082
1083 adapter
1085 .put_blob(with_token(PutBlobRequest { hash, content }, "atok"))
1086 .await
1087 .unwrap();
1088 }
1089
1090 #[tokio::test]
1091 async fn graph_name_queries_return_names() {
1092 let adapter = seeded_adapter().await;
1093 let impact = adapter
1094 .graph_impact(query("helper"))
1095 .await
1096 .unwrap()
1097 .into_inner();
1098 assert_eq!(impact.names, vec!["main".to_string()]);
1099 let callees = adapter
1100 .graph_callees(query("main"))
1101 .await
1102 .unwrap()
1103 .into_inner();
1104 assert_eq!(callees.names, vec!["helper".to_string()]);
1105 let callers = adapter
1106 .graph_callers_of(query("helper"))
1107 .await
1108 .unwrap()
1109 .into_inner();
1110 assert_eq!(callers.names, vec!["main".to_string()]);
1111 }
1112
1113 fn tomb_auth() -> Arc<Auth> {
1117 Arc::new(Auth::Enabled(HashMap::from([
1118 (
1119 "rtok".to_string(),
1120 Principal::new("reader", vec!["memory".into()], vec![]),
1121 ),
1122 (
1123 "wtok".to_string(),
1124 Principal::new("writer", vec!["memory".into()], vec!["memory".into()]),
1125 ),
1126 ("atok".to_string(), Principal::admin("admin")),
1127 ])))
1128 }
1129
1130 fn delete_req(namespace: &str) -> DeleteRequest {
1131 DeleteRequest {
1132 namespace: namespace.into(),
1133 collection: "col".into(),
1134 id: "x".into(),
1135 expected_json: serde_json::to_vec(&Option::<Revision>::None).unwrap(),
1136 author_json: Vec::new(),
1137 }
1138 }
1139
1140 fn purge_req(namespace: &str, expected: &Revision) -> PurgeRequest {
1141 PurgeRequest {
1142 namespace: namespace.into(),
1143 collection: "col".into(),
1144 id: "x".into(),
1145 expected_json: serde_json::to_vec(expected).unwrap(),
1146 }
1147 }
1148
1149 fn put_req_with(author: &str, payload: &[u8], expected: Option<Revision>) -> PutRequest {
1151 let body = gonzalo_core::Body::Inline(payload.to_vec());
1152 let record = Record {
1153 revision: Revision::initial(body.bytes()),
1154 parent: None,
1155 body,
1156 kind: RecordKind::MemoryTier,
1157 meta: Meta {
1158 author: Identity::new(author),
1159 origin_system: "test".into(),
1160 created: 0,
1161 updated: 0,
1162 labels: BTreeMap::new(),
1163 },
1164 links: Vec::new(),
1165 ancestors: Vec::new(),
1166 deleted_at: None,
1167 deleted_blob: None,
1168 key: RecordKey::new("memory", "col", "x"),
1169 deleted_kind: None,
1170 };
1171 PutRequest {
1172 record_json: serde_json::to_vec(&record).unwrap(),
1173 expected_json: serde_json::to_vec(&expected).unwrap(),
1174 }
1175 }
1176
1177 fn memory_list() -> ListRequest {
1178 ListRequest {
1179 namespace: Some("memory".into()),
1180 collection: None,
1181 }
1182 }
1183
1184 async fn raw_record(adapter: &GrpcAdapter) -> Option<Record> {
1185 let raw = adapter
1186 .get_raw(with_token(get_req("memory"), "atok"))
1187 .await
1188 .unwrap()
1189 .into_inner();
1190 raw.found
1191 .then(|| serde_json::from_slice(&raw.record_json).unwrap())
1192 }
1193
1194 async fn seed_tombstone(adapter: &GrpcAdapter, delete_token: &str) -> Record {
1197 adapter
1198 .put(with_token(put_req("memory", "writer"), "wtok"))
1199 .await
1200 .unwrap();
1201 let del = adapter
1202 .delete(with_token(delete_req("memory"), delete_token))
1203 .await
1204 .unwrap()
1205 .into_inner();
1206 assert_eq!(del.outcome, "deleted");
1207 raw_record(adapter).await.expect("tombstone")
1208 }
1209
1210 fn decode_keys(resp: ListResponse) -> Vec<RecordKey> {
1211 resp.keys_json
1212 .iter()
1213 .map(|b| serde_json::from_slice(b).unwrap())
1214 .collect()
1215 }
1216
1217 #[tokio::test]
1218 async fn grpc_delete_leaves_a_stamped_tombstone_for_raw_reads() {
1219 let adapter = fs_adapter(tomb_auth());
1220 let tomb = seed_tombstone(&adapter, "atok").await;
1222 assert_eq!(tomb.kind, RecordKind::Tombstone);
1223 assert_eq!(tomb.revision.hash, gonzalo_core::tombstone_hash());
1224 assert_eq!(tomb.meta.author, Identity::new("admin"));
1225 let key = RecordKey::new("memory", "col", "x");
1226
1227 let got = adapter
1228 .get(with_token(get_req("memory"), "rtok"))
1229 .await
1230 .unwrap()
1231 .into_inner();
1232 assert!(!got.found);
1233 let live = decode_keys(
1234 adapter
1235 .list(with_token(memory_list(), "rtok"))
1236 .await
1237 .unwrap()
1238 .into_inner(),
1239 );
1240 assert!(!live.contains(&key));
1241
1242 let raw = adapter
1243 .get_raw(with_token(get_req("memory"), "rtok"))
1244 .await
1245 .unwrap()
1246 .into_inner();
1247 assert!(raw.found);
1248 let raw_keys = decode_keys(
1249 adapter
1250 .list_raw(with_token(memory_list(), "rtok"))
1251 .await
1252 .unwrap()
1253 .into_inner(),
1254 );
1255 assert!(raw_keys.contains(&key));
1256 }
1257
1258 #[tokio::test]
1259 async fn grpc_raw_reads_are_read_scoped() {
1260 let adapter = fs_adapter(tomb_auth());
1261 let err = adapter
1262 .get_raw(with_token(get_req("secrets"), "rtok"))
1263 .await
1264 .unwrap_err();
1265 assert_eq!(err.code(), tonic::Code::PermissionDenied);
1266 let err = adapter
1267 .get_raw(Request::new(get_req("memory")))
1268 .await
1269 .unwrap_err();
1270 assert_eq!(err.code(), tonic::Code::Unauthenticated);
1271 }
1272
1273 #[tokio::test]
1274 async fn grpc_unscoped_list_raw_requires_admin() {
1275 let adapter = fs_adapter(tomb_auth());
1276 for token in ["rtok", "wtok"] {
1277 let err = adapter
1278 .list_raw(with_token(ListRequest::default(), token))
1279 .await
1280 .unwrap_err();
1281 assert_eq!(err.code(), tonic::Code::PermissionDenied, "{token}");
1282 }
1283 assert!(
1284 adapter
1285 .list_raw(with_token(ListRequest::default(), "atok"))
1286 .await
1287 .is_ok()
1288 );
1289 }
1290
1291 #[tokio::test]
1292 async fn grpc_put_raw_requires_admin() {
1293 let adapter = fs_adapter(tomb_auth());
1297 for token in ["rtok", "wtok"] {
1298 let err = adapter
1299 .put_raw(with_token(put_req_with("origin", b"{}", None), token))
1300 .await
1301 .unwrap_err();
1302 assert_eq!(err.code(), tonic::Code::PermissionDenied, "{token}");
1303 }
1304 assert_eq!(raw_record(&adapter).await, None);
1305 }
1306
1307 #[tokio::test]
1311 async fn grpc_put_raw_authorizes_before_deserializing() {
1312 let adapter = fs_adapter(tomb_auth());
1313 let err = adapter
1314 .put_raw(with_token(malformed_put_req(), "wtok"))
1315 .await
1316 .unwrap_err();
1317 assert_eq!(err.code(), tonic::Code::PermissionDenied);
1318 }
1319
1320 #[tokio::test]
1321 async fn grpc_put_raw_by_admin_keeps_the_incoming_author() {
1322 let adapter = fs_adapter(tomb_auth());
1324 let resp = adapter
1325 .put_raw(with_token(put_req_with("origin", b"{}", None), "atok"))
1326 .await
1327 .unwrap()
1328 .into_inner();
1329 assert_eq!(resp.outcome, "committed");
1330 assert_eq!(
1331 raw_record(&adapter).await.expect("stored").meta.author,
1332 Identity::new("origin")
1333 );
1334 }
1335
1336 #[tokio::test]
1337 async fn grpc_put_raw_in_open_mode_keeps_the_incoming_author() {
1338 let fs = Arc::new(FsStore::new(tempfile::tempdir().unwrap().keep()));
1340 let adapter = GrpcAdapter::new(Service::new(fs.clone(), fs));
1341 let resp = adapter
1342 .put_raw(Request::new(put_req_with("origin", b"{}", None)))
1343 .await
1344 .unwrap()
1345 .into_inner();
1346 assert_eq!(resp.outcome, "committed");
1347 let raw = adapter
1348 .get_raw(Request::new(get_req("memory")))
1349 .await
1350 .unwrap()
1351 .into_inner();
1352 let stored: Record = serde_json::from_slice(&raw.record_json).unwrap();
1353 assert_eq!(stored.meta.author, Identity::new("origin"));
1354 }
1355
1356 #[tokio::test]
1357 async fn grpc_put_raw_over_tombstone_none_conflicts_some_overwrites() {
1358 let adapter = fs_adapter(tomb_auth());
1359 let tomb = seed_tombstone(&adapter, "wtok").await;
1360
1361 let conflict = adapter
1363 .put_raw(with_token(put_req_with("peer", b"peer", None), "atok"))
1364 .await
1365 .unwrap()
1366 .into_inner();
1367 assert_eq!(conflict.outcome, "conflict");
1368
1369 let ok = adapter
1370 .put_raw(with_token(
1371 put_req_with("peer", b"peer", Some(tomb.revision.clone())),
1372 "atok",
1373 ))
1374 .await
1375 .unwrap()
1376 .into_inner();
1377 assert_eq!(ok.outcome, "committed");
1378 let stored = raw_record(&adapter).await.expect("stored");
1379 assert_eq!(stored.revision, Revision::initial(b"peer"));
1380 }
1381
1382 #[tokio::test]
1383 async fn grpc_put_not_found_is_failed_precondition() {
1384 let adapter = fs_adapter(tomb_auth());
1385 let never = Some(Revision::initial(b"never current"));
1386 let err = adapter
1387 .put_raw(with_token(put_req_with("w", b"{}", never.clone()), "atok"))
1388 .await
1389 .unwrap_err();
1390 assert_eq!(err.code(), tonic::Code::FailedPrecondition);
1391
1392 seed_tombstone(&adapter, "wtok").await;
1393 let err = adapter
1394 .put(with_token(put_req_with("w", b"{}", never), "wtok"))
1395 .await
1396 .unwrap_err();
1397 assert_eq!(err.code(), tonic::Code::FailedPrecondition);
1398 }
1399
1400 #[tokio::test]
1401 async fn grpc_purge_requires_admin() {
1402 let adapter = fs_adapter(tomb_auth());
1403 let tomb = seed_tombstone(&adapter, "wtok").await;
1404
1405 for token in ["rtok", "wtok"] {
1406 let err = adapter
1407 .purge(with_token(purge_req("memory", &tomb.revision), token))
1408 .await
1409 .unwrap_err();
1410 assert_eq!(err.code(), tonic::Code::PermissionDenied, "{token}");
1411 }
1412 let err = adapter
1413 .purge(Request::new(purge_req("memory", &tomb.revision)))
1414 .await
1415 .unwrap_err();
1416 assert_eq!(err.code(), tonic::Code::Unauthenticated);
1417
1418 let ok = adapter
1419 .purge(with_token(purge_req("memory", &tomb.revision), "atok"))
1420 .await
1421 .unwrap()
1422 .into_inner();
1423 assert_eq!(ok.outcome, "deleted");
1424 assert_eq!(raw_record(&adapter).await, None);
1425 }
1426
1427 #[tokio::test]
1430 async fn grpc_purge_of_a_live_record_is_invalid_argument() {
1431 let adapter = fs_adapter(tomb_auth());
1432 adapter
1433 .put(with_token(put_req("memory", "client"), "wtok"))
1434 .await
1435 .unwrap();
1436 let live = raw_record(&adapter).await.expect("live record");
1437 assert!(!live.is_tombstone());
1438
1439 let err = adapter
1440 .purge(with_token(purge_req("memory", &live.revision), "atok"))
1441 .await
1442 .unwrap_err();
1443 assert_eq!(err.code(), tonic::Code::InvalidArgument);
1444 assert!(
1445 err.message().contains("gonzalo delete"),
1446 "{}",
1447 err.message()
1448 );
1449
1450 let after = raw_record(&adapter).await.expect("live record survives");
1452 assert_eq!(after.revision, live.revision);
1453 }
1454
1455 #[tokio::test]
1456 async fn grpc_purge_stale_expected_is_conflict() {
1457 let adapter = fs_adapter(tomb_auth());
1458 let tomb = seed_tombstone(&adapter, "wtok").await;
1459 let resp = adapter
1460 .purge(with_token(
1461 purge_req("memory", &Revision::initial(b"not the tombstone")),
1462 "atok",
1463 ))
1464 .await
1465 .unwrap()
1466 .into_inner();
1467 assert_eq!(resp.outcome, "conflict");
1468 let conflict: gonzalo_core::Conflict = serde_json::from_slice(&resp.payload_json).unwrap();
1469 assert_eq!(conflict.current.revision, tomb.revision);
1470 }
1471
1472 #[tokio::test]
1473 async fn grpc_purge_authorizes_before_parsing() {
1474 let adapter = fs_adapter(tomb_auth());
1475 let garbage = PurgeRequest {
1476 namespace: "memory".into(),
1477 collection: "col".into(),
1478 id: "x".into(),
1479 expected_json: b"not json".to_vec(),
1480 };
1481 let err = adapter
1482 .purge(with_token(garbage.clone(), "wtok"))
1483 .await
1484 .unwrap_err();
1485 assert_eq!(err.code(), tonic::Code::PermissionDenied);
1486 let err = adapter
1487 .purge(with_token(garbage, "atok"))
1488 .await
1489 .unwrap_err();
1490 assert_eq!(err.code(), tonic::Code::InvalidArgument);
1491 }
1492
1493 #[tokio::test]
1494 async fn grpc_raw_backend_error_is_opaque() {
1495 let fs = Arc::new(FsStore::new(tempfile::tempdir().unwrap().keep()));
1496 let adapter = GrpcAdapter::new(Service::new(Arc::new(DownStore), fs));
1497 let err = adapter
1498 .get_raw(Request::new(get_req("any")))
1499 .await
1500 .unwrap_err();
1501 assert_eq!(err.code(), tonic::Code::Internal);
1502 assert_eq!(err.message(), "internal error");
1503
1504 let err = adapter
1509 .put_raw(Request::new(put_req_with("origin", b"{}", None)))
1510 .await
1511 .unwrap_err();
1512 assert_eq!(err.code(), tonic::Code::Internal);
1513 assert_eq!(err.message(), "internal error");
1514
1515 let err = adapter
1516 .list_raw(Request::new(memory_list()))
1517 .await
1518 .unwrap_err();
1519 assert_eq!(err.code(), tonic::Code::Internal);
1520 assert_eq!(err.message(), "internal error");
1521
1522 let err = adapter
1523 .purge(Request::new(purge_req("memory", &Revision::initial(b"x"))))
1524 .await
1525 .unwrap_err();
1526 assert_eq!(err.code(), tonic::Code::Internal);
1527 assert_eq!(err.message(), "internal error");
1528 }
1529
1530 #[tokio::test]
1533 async fn grpc_delete_by_non_admin_ignores_a_claimed_author() {
1534 let adapter = fs_adapter(tomb_auth());
1537 adapter
1538 .put(with_token(put_req("memory", "admin"), "atok"))
1539 .await
1540 .unwrap();
1541 let mut req = delete_req("memory");
1542 req.author_json = serde_json::to_vec(&Identity::new("forged")).unwrap();
1543 let del = adapter
1544 .delete(with_token(req, "wtok"))
1545 .await
1546 .unwrap()
1547 .into_inner();
1548 assert_eq!(del.outcome, "deleted");
1549 let tomb = raw_record(&adapter).await.expect("tombstone");
1550 assert_eq!(tomb.meta.author, Identity::new("writer"));
1551 }
1552
1553 #[tokio::test]
1554 async fn grpc_delete_by_admin_keeps_a_claimed_author() {
1555 let adapter = fs_adapter(tomb_auth());
1556 adapter
1557 .put(with_token(put_req("memory", "writer"), "wtok"))
1558 .await
1559 .unwrap();
1560 let mut req = delete_req("memory");
1561 req.author_json = serde_json::to_vec(&Identity::new("origin")).unwrap();
1562 let del = adapter
1563 .delete(with_token(req, "atok"))
1564 .await
1565 .unwrap()
1566 .into_inner();
1567 assert_eq!(del.outcome, "deleted");
1568 let tomb = raw_record(&adapter).await.expect("tombstone");
1569 assert_eq!(tomb.meta.author, Identity::new("origin"));
1570 }
1571
1572 #[tokio::test]
1573 async fn grpc_open_mode_delete_keeps_a_claimed_author() {
1574 let fs = Arc::new(FsStore::new(tempfile::tempdir().unwrap().keep()));
1575 let adapter = GrpcAdapter::new(Service::new(fs.clone(), fs));
1576 adapter
1577 .put(Request::new(put_req("memory", "writer")))
1578 .await
1579 .unwrap();
1580 let mut req = delete_req("memory");
1581 req.author_json = serde_json::to_vec(&Identity::new("origin")).unwrap();
1582 let del = adapter
1583 .delete(Request::new(req))
1584 .await
1585 .unwrap()
1586 .into_inner();
1587 assert_eq!(del.outcome, "deleted");
1588 let raw = adapter
1589 .get_raw(Request::new(get_req("memory")))
1590 .await
1591 .unwrap()
1592 .into_inner();
1593 let tomb: Record = serde_json::from_slice(&raw.record_json).unwrap();
1594 assert_eq!(tomb.meta.author, Identity::new("origin"));
1595 }
1596
1597 #[tokio::test]
1598 async fn grpc_delete_rejects_a_malformed_author() {
1599 let adapter = fs_adapter(tomb_auth());
1600 adapter
1601 .put(with_token(put_req("memory", "admin"), "atok"))
1602 .await
1603 .unwrap();
1604 let mut req = delete_req("memory");
1605 req.author_json = b"not json".to_vec();
1606 let err = adapter.delete(with_token(req, "atok")).await.unwrap_err();
1607 assert_eq!(err.code(), tonic::Code::InvalidArgument);
1608
1609 let got = adapter
1612 .get(with_token(get_req("memory"), "atok"))
1613 .await
1614 .unwrap()
1615 .into_inner();
1616 assert!(got.found);
1617 }
1618}