1pub mod v1 {
4 tonic::include_proto!("keldra.v1");
5}
6
7#[cfg(test)]
8mod tests {
9 use super::v1::{DeletedObject, NeverExisted, ObjectHead, PresentObject, object_head};
10
11 #[test]
12 fn exact_path_states_are_distinct() {
13 let states = [
14 ObjectHead {
15 state: Some(object_head::State::Present(PresentObject {
16 version: 7,
17 content_hash: vec![7; 32],
18 content_length: 99,
19 content_type: String::new(),
20 })),
21 },
22 ObjectHead {
23 state: Some(object_head::State::Deleted(DeletedObject { version: 8 })),
24 },
25 ObjectHead {
26 state: Some(object_head::State::NeverExisted(NeverExisted {})),
27 },
28 ];
29
30 assert!(matches!(
31 &states[0].state,
32 Some(object_head::State::Present(_))
33 ));
34 assert!(matches!(
35 &states[1].state,
36 Some(object_head::State::Deleted(_))
37 ));
38 assert!(matches!(
39 &states[2].state,
40 Some(object_head::State::NeverExisted(_))
41 ));
42 }
43
44 #[test]
45 fn schema_keeps_removed_capabilities_out() {
46 let schema = include_str!("../proto/keldra.proto").to_ascii_lowercase();
47 for forbidden in [
48 "rpc uploadblob",
49 "rpc publishobject",
50 "rpc putobject",
51 "message blobref",
52 "rpc listprefix",
53 "rpc begintransaction",
54 "rpc committransaction",
55 "personaldb",
56 ] {
57 assert!(!schema.contains(forbidden), "schema contains `{forbidden}`");
58 }
59
60 assert!(schema.contains("executor_nomination_log_index"));
61 assert!(schema.contains("commit_log_index"));
62 assert!(schema.contains("immutable_path_prefixes"));
63 assert!(schema.contains("program_only_path_prefixes"));
64 assert!(!schema.contains("rpc registerprogram"));
65 assert!(!schema.contains("message registerprogram"));
66 assert!(schema.contains("objectaddress program"));
67 assert!(schema.contains("_keldra/programs/{name}@{version}"));
68 assert!(schema.contains("rpc startput(putheader) returns (puttoken)"));
69 assert!(schema.contains("rpc put(stream putrequest) returns (puttoken)"));
70 assert!(schema.contains("rpc putend(puttoken) returns (mutationreceipt)"));
71 for rpc in [
72 "rpc createindex(createindexrequest)",
73 "rpc updateindex(updateindexrequest)",
74 "rpc getindex(getindexrequest)",
75 "rpc listindices(listindicesrequest)",
76 "rpc deleteindex(deleteindexrequest)",
77 "rpc queryindex(queryindexrequest)",
78 ] {
79 assert!(schema.contains(rpc), "schema is missing `{rpc}`");
80 }
81 assert!(schema.contains("index_kind_tensor"));
82 assert!(schema.contains("tensorindexspec tensor"));
83 assert!(schema.contains("tensorindexquery tensor"));
84
85 for rpc in [
86 "rpc exchangeclientcredentials",
87 "rpc provisiontenant",
88 "rpc createapplication",
89 "rpc rotateapplicationcredential",
90 "rpc disableapplicationcredential",
91 "rpc createbucket",
92 "rpc grantapplicationrole",
93 "rpc revokeapplicationrole",
94 "rpc putschema",
95 "rpc bindschema",
96 "rpc getbinding",
97 "rpc getschema",
98 "rpc mutatetuples",
99 "rpc readtuples",
100 "rpc checkpermission",
101 "rpc checkpermissions",
102 "rpc watchprefix",
103 "rpc listobjects",
104 "rpc deleteversion",
105 "rpc listobjectversions",
106 "rpc setbucketversioning",
107 ] {
108 assert!(schema.contains(rpc), "schema is missing `{rpc}`");
109 }
110 for forbidden in [
111 "rpc createrealm",
112 "rpc deleterealm",
113 "rpc applyschema",
114 "zookie",
115 "caveat",
116 "publication_metadata",
117 "insecure_no_auth",
118 "api_token",
119 ] {
120 assert!(!schema.contains(forbidden), "schema contains `{forbidden}`");
121 }
122 }
123
124 #[test]
125 fn generated_index_client_is_publicly_exposed() {
126 let _: Option<
127 super::v1::index_service_client::IndexServiceClient<tonic::transport::Channel>,
128 > = None;
129 }
130
131 #[test]
132 fn typed_json_fields_have_explicit_type_cardinality_and_capabilities() {
133 use super::v1::{
134 IndexField, IndexFieldCapability, IndexFieldCardinality, KeywordIndexField, index_field,
135 };
136
137 let field = IndexField {
138 name: "document_id".into(),
139 json_pointer: "/id".into(),
140 cardinality: IndexFieldCardinality::Single as i32,
141 capabilities: vec![IndexFieldCapability::Exact as i32],
142 field_type: Some(index_field::FieldType::Keyword(KeywordIndexField {})),
143 };
144
145 assert!(matches!(
146 field.field_type,
147 Some(index_field::FieldType::Keyword(_))
148 ));
149 assert_eq!(field.capabilities, [IndexFieldCapability::Exact as i32]);
150
151 let schema = include_str!("../proto/keldra.proto").to_ascii_lowercase();
152 assert!(schema.contains("rpc listindices(listindicesrequest)"));
153 assert!(schema.contains("repeated indexdefinition indices = 1"));
154 assert!(!schema.contains("listindexes"));
155 assert!(!schema.contains("fields_json"));
156 assert!(!schema.contains("bool multi_valued"));
157 }
158
159 #[test]
160 fn typed_json_queries_expose_fielded_text_facets_and_aggregates() {
161 use super::v1::{
162 IndexAggregateOperation, IndexAggregateRequest, IndexAggregateResult, IndexFacetBucket,
163 IndexFacetRequest, IndexFacetResult, IndexPredicate, IndexPredicateOperator,
164 IndexQueryHit, QueryIndexResponse, TypedJsonIndexQuery,
165 };
166
167 let query = TypedJsonIndexQuery {
168 predicates: vec![
169 IndexPredicate {
170 field: "summary".into(),
171 operator: IndexPredicateOperator::FullText as i32,
172 values_json: vec![br#""memory safety""#.to_vec()],
173 },
174 IndexPredicate {
175 field: "summary".into(),
176 operator: IndexPredicateOperator::Phrase as i32,
177 values_json: vec![br#""memory safety""#.to_vec()],
178 },
179 ],
180 order: Vec::new(),
181 facets: vec![IndexFacetRequest {
182 field: "ecosystem".into(),
183 limit: 10,
184 }],
185 aggregates: vec![IndexAggregateRequest {
186 field: "severity".into(),
187 operation: IndexAggregateOperation::Average as i32,
188 }],
189 };
190 let response = QueryIndexResponse {
191 hits: vec![IndexQueryHit {
192 address: None,
193 object_version: 7,
194 score: Some(0.75),
195 }],
196 next_page_token: Vec::new(),
197 freshness: None,
198 facet_results: vec![IndexFacetResult {
199 field: "ecosystem".into(),
200 buckets: vec![IndexFacetBucket {
201 value_json: br#""cargo""#.to_vec(),
202 count: 4,
203 }],
204 }],
205 aggregate_results: vec![IndexAggregateResult {
206 field: "severity".into(),
207 operation: IndexAggregateOperation::Average as i32,
208 value_json: Some(b"7.5".to_vec()),
209 contributing_count: 4,
210 }],
211 };
212
213 assert_eq!(query.facets[0].limit, 10);
214 assert_eq!(response.hits[0].object_version, 7);
215 assert_eq!(response.aggregate_results[0].contributing_count, 4);
216 }
217
218 #[test]
219 fn generated_personaldb_client_is_publicly_exposed() {
220 let _: Option<
221 super::v1::personal_db_service_client::PersonalDbServiceClient<
222 tonic::transport::Channel,
223 >,
224 > = None;
225
226 let schema = include_str!("../proto/personaldb.proto").to_ascii_lowercase();
227 for rpc in [
228 "rpc creategroup(",
229 "rpc describegroup(",
230 "rpc listgroups(",
231 "rpc grantgrouprole(",
232 "rpc revokegrouprole(",
233 "rpc appendentry(",
234 "rpc materializeprojection(",
235 "rpc catchup(",
236 "rpc registersnapshot(",
237 "rpc getsnapshot(",
238 ] {
239 assert!(schema.contains(rpc), "PersonalDB schema is missing `{rpc}`");
240 }
241 }
242
243 #[test]
244 fn object_surface_has_only_explicit_typed_mutations() {
245 use super::v1::{
246 BulkOperation, BulkPutIfVersionRequest, CreateBucketRequest, DeleteIfVersionRequest,
247 DeleteRequest, DeleteVersionRequest, DeleteVersionResponse, Durability,
248 ListObjectsRequest, ListObjectsResponse, ObjectAddress, ObjectVersioning, PutHeader,
249 PutIfVersionOperation, PutRequest, PutToken, bulk_operation, put_header,
250 };
251
252 let address = Some(ObjectAddress {
253 tenant: "acme".into(),
254 bucket: "objects".into(),
255 path: "one".into(),
256 });
257 let header = PutHeader {
258 address: address.clone(),
259 content_type: "application/json".into(),
260 command_id: "command-1".into(),
261 durability: Durability::Local as i32,
262 operation: Some(put_header::Operation::PutIfVersion(PutIfVersionOperation {
263 expected_version: 8,
264 })),
265 };
266 let frame = PutRequest {
267 token: Some(PutToken {
268 value: b"opaque".to_vec(),
269 expires_at: None,
270 }),
271 chunk: Vec::new(),
272 };
273 assert!(matches!(
274 header.operation,
275 Some(put_header::Operation::PutIfVersion(_))
276 ));
277 assert!(frame.chunk.is_empty());
278
279 let operations = [
280 bulk_operation::Operation::Put(Default::default()),
281 bulk_operation::Operation::PutIfAbsent(Default::default()),
282 bulk_operation::Operation::PutIfVersion(BulkPutIfVersionRequest::default()),
283 bulk_operation::Operation::PutImmutable(Default::default()),
284 bulk_operation::Operation::Delete(DeleteRequest {
285 address: address.clone(),
286 ..Default::default()
287 }),
288 bulk_operation::Operation::DeleteIfVersion(DeleteIfVersionRequest {
289 address: address.clone(),
290 expected_version: 8,
291 ..Default::default()
292 }),
293 ];
294 assert_eq!(
295 operations
296 .into_iter()
297 .map(|operation| BulkOperation {
298 operation: Some(operation),
299 })
300 .count(),
301 6
302 );
303
304 let current_head_delete = DeleteIfVersionRequest {
305 address: address.clone(),
306 expected_version: 8,
307 ..Default::default()
308 };
309 let retained_version_delete = DeleteVersionRequest {
310 address,
311 version: 7,
312 ..Default::default()
313 };
314 assert_eq!(current_head_delete.expected_version, 8);
315 assert_eq!(retained_version_delete.version, 7);
316 let replaced_current = DeleteVersionResponse {
317 deleted: true,
318 replacement_tombstone_version: Some(9),
319 };
320 assert_eq!(replaced_current.replacement_tombstone_version, Some(9));
321 assert_eq!(
322 CreateBucketRequest::default().versioning,
323 ObjectVersioning::Unversioned as i32
324 );
325
326 let list_request = ListObjectsRequest {
327 tenant: "acme".into(),
328 bucket: "objects".into(),
329 prefix: "reports/".into(),
330 start_after: Some("reports/2025.json".into()),
331 limit: 100,
332 };
333 let list_response = ListObjectsResponse {
334 paths: vec!["reports/2026.json".into()],
335 has_more: false,
336 };
337 assert_eq!(
338 list_request.start_after.as_deref(),
339 Some("reports/2025.json")
340 );
341 assert_eq!(list_response.paths, vec!["reports/2026.json".to_owned()]);
342 }
343
344 #[test]
345 fn authorization_wire_types_preserve_scope_and_typed_unions() {
346 use super::v1::{
347 AnyUsersetSelector, AtLeastRevision, AuthzConsistency, AuthzScope, DirectRelation,
348 InheritRule, NamespaceDefinition, ObjectRef, Permission, PermissionRule,
349 PutSchemaRequest, RelationDefinition, SubjectSelector, Userset, authz_consistency,
350 object_ref, permission_rule, relation_definition, subject, subject_selector,
351 };
352
353 let account = ObjectRef {
354 namespace: "account".into(),
355 id: Some(object_ref::Id::OpaqueId("acme".into())),
356 };
357 let members = Userset {
358 object: Some(account),
359 relation: "member".into(),
360 };
361 let schema = NamespaceDefinition {
362 name: "ledger".into(),
363 relations: vec![
364 RelationDefinition {
365 name: "reader".into(),
366 kind: Some(relation_definition::Kind::Direct(DirectRelation {
367 allowed_subjects: vec![SubjectSelector {
368 selector: Some(subject_selector::Selector::AnyUserset(
369 AnyUsersetSelector {
370 namespace: "account".into(),
371 relation: "member".into(),
372 },
373 )),
374 }],
375 })),
376 },
377 RelationDefinition {
378 name: "read".into(),
379 kind: Some(relation_definition::Kind::Permission(Permission {
380 rules: vec![PermissionRule {
381 rule: Some(permission_rule::Rule::Inherit(InheritRule {
382 relation: "reader".into(),
383 })),
384 }],
385 })),
386 },
387 ],
388 };
389 let subject = super::v1::Subject {
390 kind: Some(subject::Kind::Userset(members)),
391 };
392 let publication = PutSchemaRequest {
393 schema_id: "acme".into(),
394 namespaces: vec![schema],
395 };
396 let consistency = AuthzConsistency {
397 requirement: Some(authz_consistency::Requirement::AtLeast(AtLeastRevision {
398 revision: 42,
399 })),
400 };
401 let system_scope = AuthzScope {
402 storage_tenant: "keldra-internal".into(),
403 realm: "_keldra/system".into(),
404 };
405
406 assert_eq!(publication.namespaces[0].relations.len(), 2);
407 assert!(matches!(subject.kind, Some(subject::Kind::Userset(_))));
408 assert!(matches!(
409 consistency.requirement,
410 Some(authz_consistency::Requirement::AtLeast(AtLeastRevision {
411 revision: 42
412 }))
413 ));
414 assert_eq!(system_scope.realm, "_keldra/system");
415 }
416
417 #[test]
418 fn tuple_mutation_and_batch_checks_share_request_scope() {
419 use super::v1::{
420 AuthzScope, CheckPermissionsRequest, MutateTuplesRequest, ObjectRef, PermissionCheck,
421 RelationTuple, Subject, TupleMutation, authz_consistency, object_ref, subject,
422 tuple_mutation,
423 };
424
425 let scope = AuthzScope {
426 storage_tenant: "acme".into(),
427 realm: "default".into(),
428 };
429 let ledger = ObjectRef {
430 namespace: "ledger".into(),
431 id: Some(object_ref::Id::OpaqueId("main".into())),
432 };
433 let alice = Subject {
434 kind: Some(subject::Kind::Object(ObjectRef {
435 namespace: "user".into(),
436 id: Some(object_ref::Id::OpaqueId("alice".into())),
437 })),
438 };
439 let tuple = RelationTuple {
440 object: Some(ledger.clone()),
441 relation: "reader".into(),
442 subject: Some(alice.clone()),
443 };
444 let mutation = MutateTuplesRequest {
445 scope: Some(scope.clone()),
446 operation_id: "grant-alice".into(),
447 expected_revision: Some(41),
448 mutations: vec![TupleMutation {
449 operation: Some(tuple_mutation::Operation::Add(tuple)),
450 }],
451 };
452 let checks = CheckPermissionsRequest {
453 scope: Some(scope),
454 checks: vec![PermissionCheck {
455 subject: Some(alice),
456 object: Some(ledger),
457 relation: "read".into(),
458 }],
459 consistency: Some(super::v1::AuthzConsistency {
460 requirement: Some(authz_consistency::Requirement::Exact(
461 super::v1::ExactRevision { revision: 42 },
462 )),
463 }),
464 };
465
466 assert_eq!(mutation.expected_revision, Some(41));
467 assert_eq!(mutation.mutations.len(), 1);
468 assert_eq!(checks.checks.len(), 1);
469 assert!(matches!(
470 checks
471 .consistency
472 .and_then(|consistency| consistency.requirement),
473 Some(authz_consistency::Requirement::Exact(_))
474 ));
475 }
476}