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