1pub mod v1 {
4 tonic::include_proto!("anvil.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/anvil.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("_anvil/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 listindexes(listindexesrequest)",
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 generated_personaldb_client_is_publicly_exposed() {
133 let _: Option<
134 super::v1::personal_db_service_client::PersonalDbServiceClient<
135 tonic::transport::Channel,
136 >,
137 > = None;
138
139 let schema = include_str!("../proto/personaldb.proto").to_ascii_lowercase();
140 for rpc in [
141 "rpc creategroup(",
142 "rpc describegroup(",
143 "rpc listgroups(",
144 "rpc grantgrouprole(",
145 "rpc revokegrouprole(",
146 "rpc appendentry(",
147 "rpc materializeprojection(",
148 "rpc catchup(",
149 "rpc registersnapshot(",
150 "rpc getsnapshot(",
151 ] {
152 assert!(schema.contains(rpc), "PersonalDB schema is missing `{rpc}`");
153 }
154 }
155
156 #[test]
157 fn object_surface_has_only_explicit_typed_mutations() {
158 use super::v1::{
159 BulkOperation, BulkPutIfVersionRequest, CreateBucketRequest, DeleteIfVersionRequest,
160 DeleteRequest, DeleteVersionRequest, DeleteVersionResponse, Durability,
161 ListObjectsRequest, ListObjectsResponse, ObjectAddress, ObjectVersioning, PutHeader,
162 PutIfVersionOperation, PutRequest, PutToken, bulk_operation, put_header,
163 };
164
165 let address = Some(ObjectAddress {
166 tenant: "acme".into(),
167 bucket: "objects".into(),
168 path: "one".into(),
169 });
170 let header = PutHeader {
171 address: address.clone(),
172 content_type: "application/json".into(),
173 command_id: "command-1".into(),
174 durability: Durability::Local as i32,
175 operation: Some(put_header::Operation::PutIfVersion(PutIfVersionOperation {
176 expected_version: 8,
177 })),
178 };
179 let frame = PutRequest {
180 token: Some(PutToken {
181 value: b"opaque".to_vec(),
182 expires_at: None,
183 }),
184 chunk: Vec::new(),
185 };
186 assert!(matches!(
187 header.operation,
188 Some(put_header::Operation::PutIfVersion(_))
189 ));
190 assert!(frame.chunk.is_empty());
191
192 let operations = [
193 bulk_operation::Operation::Put(Default::default()),
194 bulk_operation::Operation::PutIfAbsent(Default::default()),
195 bulk_operation::Operation::PutIfVersion(BulkPutIfVersionRequest::default()),
196 bulk_operation::Operation::PutImmutable(Default::default()),
197 bulk_operation::Operation::Delete(DeleteRequest {
198 address: address.clone(),
199 ..Default::default()
200 }),
201 bulk_operation::Operation::DeleteIfVersion(DeleteIfVersionRequest {
202 address: address.clone(),
203 expected_version: 8,
204 ..Default::default()
205 }),
206 ];
207 assert_eq!(
208 operations
209 .into_iter()
210 .map(|operation| BulkOperation {
211 operation: Some(operation),
212 })
213 .count(),
214 6
215 );
216
217 let current_head_delete = DeleteIfVersionRequest {
218 address: address.clone(),
219 expected_version: 8,
220 ..Default::default()
221 };
222 let retained_version_delete = DeleteVersionRequest {
223 address,
224 version: 7,
225 ..Default::default()
226 };
227 assert_eq!(current_head_delete.expected_version, 8);
228 assert_eq!(retained_version_delete.version, 7);
229 let replaced_current = DeleteVersionResponse {
230 deleted: true,
231 replacement_tombstone_version: Some(9),
232 };
233 assert_eq!(replaced_current.replacement_tombstone_version, Some(9));
234 assert_eq!(
235 CreateBucketRequest::default().versioning,
236 ObjectVersioning::Unversioned as i32
237 );
238
239 let list_request = ListObjectsRequest {
240 tenant: "acme".into(),
241 bucket: "objects".into(),
242 prefix: "reports/".into(),
243 start_after: Some("reports/2025.json".into()),
244 limit: 100,
245 };
246 let list_response = ListObjectsResponse {
247 paths: vec!["reports/2026.json".into()],
248 has_more: false,
249 };
250 assert_eq!(
251 list_request.start_after.as_deref(),
252 Some("reports/2025.json")
253 );
254 assert_eq!(list_response.paths, vec!["reports/2026.json".to_owned()]);
255 }
256
257 #[test]
258 fn authorization_wire_types_preserve_scope_and_typed_unions() {
259 use super::v1::{
260 AnyUsersetSelector, AtLeastRevision, AuthzConsistency, AuthzScope, DirectRelation,
261 InheritRule, NamespaceDefinition, ObjectRef, Permission, PermissionRule,
262 PutSchemaRequest, RelationDefinition, SubjectSelector, Userset, authz_consistency,
263 object_ref, permission_rule, relation_definition, subject, subject_selector,
264 };
265
266 let account = ObjectRef {
267 namespace: "account".into(),
268 id: Some(object_ref::Id::OpaqueId("acme".into())),
269 };
270 let members = Userset {
271 object: Some(account),
272 relation: "member".into(),
273 };
274 let schema = NamespaceDefinition {
275 name: "ledger".into(),
276 relations: vec![
277 RelationDefinition {
278 name: "reader".into(),
279 kind: Some(relation_definition::Kind::Direct(DirectRelation {
280 allowed_subjects: vec![SubjectSelector {
281 selector: Some(subject_selector::Selector::AnyUserset(
282 AnyUsersetSelector {
283 namespace: "account".into(),
284 relation: "member".into(),
285 },
286 )),
287 }],
288 })),
289 },
290 RelationDefinition {
291 name: "read".into(),
292 kind: Some(relation_definition::Kind::Permission(Permission {
293 rules: vec![PermissionRule {
294 rule: Some(permission_rule::Rule::Inherit(InheritRule {
295 relation: "reader".into(),
296 })),
297 }],
298 })),
299 },
300 ],
301 };
302 let subject = super::v1::Subject {
303 kind: Some(subject::Kind::Userset(members)),
304 };
305 let publication = PutSchemaRequest {
306 schema_id: "acme".into(),
307 namespaces: vec![schema],
308 };
309 let consistency = AuthzConsistency {
310 requirement: Some(authz_consistency::Requirement::AtLeast(AtLeastRevision {
311 revision: 42,
312 })),
313 };
314 let system_scope = AuthzScope {
315 storage_tenant: "anvil-internal".into(),
316 realm: "_anvil/system".into(),
317 };
318
319 assert_eq!(publication.namespaces[0].relations.len(), 2);
320 assert!(matches!(subject.kind, Some(subject::Kind::Userset(_))));
321 assert!(matches!(
322 consistency.requirement,
323 Some(authz_consistency::Requirement::AtLeast(AtLeastRevision {
324 revision: 42
325 }))
326 ));
327 assert_eq!(system_scope.realm, "_anvil/system");
328 }
329
330 #[test]
331 fn tuple_mutation_and_batch_checks_share_request_scope() {
332 use super::v1::{
333 AuthzScope, CheckPermissionsRequest, MutateTuplesRequest, ObjectRef, PermissionCheck,
334 RelationTuple, Subject, TupleMutation, authz_consistency, object_ref, subject,
335 tuple_mutation,
336 };
337
338 let scope = AuthzScope {
339 storage_tenant: "acme".into(),
340 realm: "default".into(),
341 };
342 let ledger = ObjectRef {
343 namespace: "ledger".into(),
344 id: Some(object_ref::Id::OpaqueId("main".into())),
345 };
346 let alice = Subject {
347 kind: Some(subject::Kind::Object(ObjectRef {
348 namespace: "user".into(),
349 id: Some(object_ref::Id::OpaqueId("alice".into())),
350 })),
351 };
352 let tuple = RelationTuple {
353 object: Some(ledger.clone()),
354 relation: "reader".into(),
355 subject: Some(alice.clone()),
356 };
357 let mutation = MutateTuplesRequest {
358 scope: Some(scope.clone()),
359 operation_id: "grant-alice".into(),
360 expected_revision: Some(41),
361 mutations: vec![TupleMutation {
362 operation: Some(tuple_mutation::Operation::Add(tuple)),
363 }],
364 };
365 let checks = CheckPermissionsRequest {
366 scope: Some(scope),
367 checks: vec![PermissionCheck {
368 subject: Some(alice),
369 object: Some(ledger),
370 relation: "read".into(),
371 }],
372 consistency: Some(super::v1::AuthzConsistency {
373 requirement: Some(authz_consistency::Requirement::Exact(
374 super::v1::ExactRevision { revision: 42 },
375 )),
376 }),
377 };
378
379 assert_eq!(mutation.expected_revision, Some(41));
380 assert_eq!(mutation.mutations.len(), 1);
381 assert_eq!(checks.checks.len(), 1);
382 assert!(matches!(
383 checks
384 .consistency
385 .and_then(|consistency| consistency.requirement),
386 Some(authz_consistency::Requirement::Exact(_))
387 ));
388 }
389}