@layerzerolabs/common-utils-stellar-contracts 0.2.122

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,559 @@
1
+ extern crate std;
2
+
3
+ use crate::{
4
+ auth::Auth,
5
+ errors::RbacError,
6
+ rbac::{
7
+ ensure_role, grant_role_no_auth, remove_role_admin_no_auth, revoke_role_no_auth, RbacStorage, RoleAdminChanged,
8
+ RoleBasedAccessControl, RoleGranted, RoleRevoked, MAX_ROLES,
9
+ },
10
+ testing_utils::{assert_eq_event, assert_eq_events},
11
+ tests::test_helper::mock_auth,
12
+ };
13
+ use soroban_sdk::{contract, contractimpl, testutils::Address as _, Address, Env, Symbol, Vec};
14
+
15
+ // ============================================
16
+ // Test Contract
17
+ // ============================================
18
+
19
+ #[contract]
20
+ pub struct RbacTestContract;
21
+
22
+ fn authorizer_key(env: &Env) -> Symbol {
23
+ Symbol::new(env, "authorizer")
24
+ }
25
+
26
+ #[contractimpl]
27
+ impl RbacTestContract {
28
+ /// Test-only helper to set the authorizer in instance storage.
29
+ ///
30
+ /// NOTE: This is intentionally *not* protected by auth, since it's only used in unit tests.
31
+ pub fn set_authorizer_for_test(env: &Env, authorizer: Address) {
32
+ env.storage().instance().set(&authorizer_key(env), &authorizer);
33
+ }
34
+
35
+ // ----------------------------
36
+ // Public helper wrappers
37
+ // ----------------------------
38
+
39
+ pub fn rbac_ensure_role(env: &Env, role: Symbol, caller: Address) {
40
+ ensure_role::<Self>(env, &role, &caller);
41
+ }
42
+
43
+ pub fn rbac_rm_role_admin_no_auth(env: &Env, role: Symbol) {
44
+ remove_role_admin_no_auth(env, &role);
45
+ }
46
+
47
+ // ----------------------------
48
+ // Raw storage setters (test-only)
49
+ // ----------------------------
50
+
51
+ pub fn raw_set_existing_roles(env: &Env, roles: Vec<Symbol>) {
52
+ RbacStorage::set_existing_roles(env, &roles);
53
+ }
54
+
55
+ pub fn raw_set_role_accounts_count(env: &Env, role: Symbol, count: u32) {
56
+ RbacStorage::set_role_accounts_count(env, &role, &count);
57
+ }
58
+
59
+ pub fn raw_set_role_account_to_index(env: &Env, role: Symbol, account: Address, index: u32) {
60
+ RbacStorage::set_role_account_to_index(env, &role, &account, &index);
61
+ }
62
+
63
+ pub fn raw_set_role_index_to_account(env: &Env, role: Symbol, index: u32, account: Address) {
64
+ RbacStorage::set_role_index_to_account(env, &role, index, &account);
65
+ }
66
+ }
67
+
68
+ /// `Auth` implementation for the test contract - uses a stored address as the authorizer.
69
+ impl Auth for RbacTestContract {
70
+ fn authorizer(env: &Env) -> Option<Address> {
71
+ env.storage().instance().get(&authorizer_key(env))
72
+ }
73
+ }
74
+
75
+ // Expose `RoleBasedAccessControl` default methods as Soroban entrypoints.
76
+ #[contractimpl(contracttrait)]
77
+ impl RoleBasedAccessControl for RbacTestContract {}
78
+
79
+ fn setup_contract() -> (Env, Address, Address, RbacTestContractClient<'static>) {
80
+ let env = Env::default();
81
+ let contract_id = env.register(RbacTestContract, ());
82
+ let client = RbacTestContractClient::new(&env, &contract_id);
83
+
84
+ let authorizer = Address::generate(&env);
85
+ client.set_authorizer_for_test(&authorizer);
86
+
87
+ (env, contract_id, authorizer, client)
88
+ }
89
+
90
+ // ============================================
91
+ // Views + basic errors
92
+ // ============================================
93
+
94
+ #[test]
95
+ fn views_default_and_index_out_of_bounds_error_code() {
96
+ let (env, _contract_id, _authorizer, client) = setup_contract();
97
+
98
+ let role = Symbol::new(&env, "ROLE");
99
+ let acct = Address::generate(&env);
100
+
101
+ assert_eq!(client.get_existing_roles(), Vec::<Symbol>::new(&env));
102
+ assert_eq!(client.get_role_admin(&role), None);
103
+ assert_eq!(client.get_role_member_count(&role), 0);
104
+ assert_eq!(client.has_role(&acct, &role), None);
105
+
106
+ // Ensure-role should fail when the caller is unauthorized.
107
+ let res = client.try_rbac_ensure_role(&role, &acct);
108
+ assert_eq!(res.err().unwrap().ok().unwrap(), RbacError::Unauthorized.into());
109
+
110
+ // Member lookup out of bounds should return IndexOutOfBounds.
111
+ let res = client.try_get_role_member(&role, &0u32);
112
+ assert_eq!(res.err().unwrap().ok().unwrap(), RbacError::IndexOutOfBounds.into());
113
+ }
114
+
115
+ // ============================================
116
+ // grant_role / grant_role_no_auth
117
+ // ============================================
118
+
119
+ #[test]
120
+ fn grant_role_authorizer_emits_event_and_enumerates() {
121
+ let (env, contract_id, authorizer, client) = setup_contract();
122
+
123
+ let role = Symbol::new(&env, "ROLE1");
124
+ let acct = Address::generate(&env);
125
+
126
+ mock_auth(&env, &contract_id, &authorizer, "grant_role", (acct.clone(), role.clone(), authorizer.clone()));
127
+ client.grant_role(&acct, &role, &authorizer);
128
+
129
+ assert_eq_event(
130
+ &env,
131
+ &contract_id,
132
+ RoleGranted { role: role.clone(), account: acct.clone(), caller: authorizer.clone() },
133
+ );
134
+
135
+ assert_eq!(client.get_role_member_count(&role), 1);
136
+ assert_eq!(client.has_role(&acct, &role), Some(0));
137
+ assert_eq!(client.get_role_member(&role, &0), acct.clone());
138
+
139
+ let roles = client.get_existing_roles();
140
+ assert_eq!(roles.len(), 1);
141
+ assert_eq!(roles.get(0).unwrap(), role);
142
+
143
+ // ensure_role succeeds for a held role.
144
+ client.rbac_ensure_role(&Symbol::new(&env, "ROLE1"), &acct);
145
+ }
146
+
147
+ #[test]
148
+ fn authorizer_can_revoke_role() {
149
+ let (env, contract_id, authorizer, client) = setup_contract();
150
+
151
+ let role = Symbol::new(&env, "ROLE_AUTHZ_REVOKE");
152
+ let user = Address::generate(&env);
153
+
154
+ // Step 1: authorizer grants role to user.
155
+ mock_auth(&env, &contract_id, &authorizer, "grant_role", (user.clone(), role.clone(), authorizer.clone()));
156
+ client.grant_role(&user, &role, &authorizer);
157
+ assert_eq_event(
158
+ &env,
159
+ &contract_id,
160
+ RoleGranted { role: role.clone(), account: user.clone(), caller: authorizer.clone() },
161
+ );
162
+ assert_eq!(client.has_role(&user, &role), Some(0));
163
+ assert_eq!(client.get_role_member_count(&role), 1);
164
+
165
+ // Step 2: authorizer revokes role from user.
166
+ mock_auth(&env, &contract_id, &authorizer, "revoke_role", (user.clone(), role.clone(), authorizer.clone()));
167
+ client.revoke_role(&user, &role, &authorizer);
168
+ assert_eq_event(&env, &contract_id, RoleRevoked { role: role.clone(), account: user.clone(), caller: authorizer });
169
+ assert_eq!(client.has_role(&user, &role), None);
170
+ assert_eq!(client.get_role_member_count(&role), 0);
171
+ }
172
+
173
+ #[test]
174
+ fn grant_role_no_auth_is_idempotent_and_emits_once() {
175
+ let (env, contract_id, _authorizer, client) = setup_contract();
176
+
177
+ let role = Symbol::new(&env, "ROLE2");
178
+ let acct = Address::generate(&env);
179
+ let caller = Address::generate(&env);
180
+
181
+ env.as_contract(&contract_id, || {
182
+ grant_role_no_auth(&env, &acct, &role, &caller);
183
+ // Second call should hit the early-return branch and emit no event.
184
+ grant_role_no_auth(&env, &acct, &role, &caller);
185
+ });
186
+
187
+ // Only the first grant should emit.
188
+ assert_eq_event(&env, &contract_id, RoleGranted { role: role.clone(), account: acct.clone(), caller });
189
+
190
+ assert_eq!(client.get_role_member_count(&role), 1);
191
+ assert_eq!(client.has_role(&acct, &role), Some(0));
192
+ }
193
+
194
+ #[test]
195
+ fn grant_role_unauthorized_returns_error_code() {
196
+ let (env, contract_id, _authorizer, client) = setup_contract();
197
+
198
+ let role = Symbol::new(&env, "ROLE3");
199
+ let acct = Address::generate(&env);
200
+ let caller = Address::generate(&env);
201
+
202
+ // Provide auth so we reach the RBAC Unauthorized check (not Auth::InvalidAction).
203
+ mock_auth(&env, &contract_id, &caller, "grant_role", (acct.clone(), role.clone(), caller.clone()));
204
+ let res = client.try_grant_role(&acct, &role, &caller);
205
+ assert_eq!(res.err().unwrap().ok().unwrap(), RbacError::Unauthorized.into());
206
+
207
+ assert_eq!(client.get_role_member_count(&role), 0);
208
+ assert_eq!(client.get_existing_roles(), Vec::<Symbol>::new(&env));
209
+ }
210
+
211
+ #[test]
212
+ #[should_panic(expected = "Error(Auth, InvalidAction)")]
213
+ fn grant_role_requires_auth() {
214
+ let (env, _contract_id, authorizer, client) = setup_contract();
215
+
216
+ let role = Symbol::new(&env, "ROLE_AUTH");
217
+ let acct = Address::generate(&env);
218
+
219
+ // No mock_auth -> caller.require_auth() must fail.
220
+ client.grant_role(&acct, &role, &authorizer);
221
+ }
222
+
223
+ // ============================================
224
+ // set_role_admin
225
+ // ============================================
226
+
227
+ #[test]
228
+ fn set_role_admin_emits_event_with_previous_admin_values() {
229
+ let (env, contract_id, authorizer, client) = setup_contract();
230
+
231
+ let role = Symbol::new(&env, "ROLE_ADMIN_TEST");
232
+ let admin1 = Symbol::new(&env, "ADMIN1");
233
+ let admin2 = Symbol::new(&env, "ADMIN2");
234
+
235
+ mock_auth(&env, &contract_id, &authorizer, "set_role_admin", (role.clone(), admin1.clone()));
236
+ client.set_role_admin(&role, &admin1);
237
+
238
+ assert_eq_event(
239
+ &env,
240
+ &contract_id,
241
+ RoleAdminChanged { role: role.clone(), previous_admin_role: None, new_admin_role: Some(admin1.clone()) },
242
+ );
243
+ assert_eq!(client.get_role_admin(&role), Some(admin1.clone()));
244
+
245
+ mock_auth(&env, &contract_id, &authorizer, "set_role_admin", (role.clone(), admin2.clone()));
246
+ client.set_role_admin(&role, &admin2);
247
+
248
+ assert_eq_event(
249
+ &env,
250
+ &contract_id,
251
+ RoleAdminChanged {
252
+ role: role.clone(),
253
+ previous_admin_role: Some(admin1),
254
+ new_admin_role: Some(admin2.clone()),
255
+ },
256
+ );
257
+ assert_eq!(client.get_role_admin(&role), Some(admin2));
258
+ }
259
+
260
+ #[test]
261
+ fn remove_role_admin_errors_and_success() {
262
+ let (env, contract_id, authorizer, client) = setup_contract();
263
+
264
+ let role = Symbol::new(&env, "ROLE_ADMIN_REMOVE");
265
+ let admin = Symbol::new(&env, "ADMIN_REMOVE");
266
+
267
+ // If no admin role is set, removing should fail with AdminRoleNotFound.
268
+ mock_auth(&env, &contract_id, &authorizer, "remove_role_admin", (role.clone(),));
269
+ let res = client.try_remove_role_admin(&role);
270
+ assert_eq!(res.err().unwrap().ok().unwrap(), RbacError::AdminRoleNotFound.into());
271
+
272
+ // Set admin role (requires authorizer auth).
273
+ mock_auth(&env, &contract_id, &authorizer, "set_role_admin", (role.clone(), admin.clone()));
274
+ client.set_role_admin(&role, &admin);
275
+ assert_eq!(client.get_role_admin(&role), Some(admin));
276
+
277
+ // Now removal should succeed and clear the admin role.
278
+ mock_auth(&env, &contract_id, &authorizer, "remove_role_admin", (role.clone(),));
279
+ client.remove_role_admin(&role);
280
+ assert_eq!(client.get_role_admin(&role), None);
281
+
282
+ // Removing again should fail with AdminRoleNotFound.
283
+ mock_auth(&env, &contract_id, &authorizer, "remove_role_admin", (role.clone(),));
284
+ let res = client.try_remove_role_admin(&role);
285
+ assert_eq!(res.err().unwrap().ok().unwrap(), RbacError::AdminRoleNotFound.into());
286
+ }
287
+
288
+ #[test]
289
+ #[should_panic(expected = "Error(Auth, InvalidAction)")]
290
+ fn set_role_admin_requires_auth() {
291
+ let (env, _contract_id, _authorizer, client) = setup_contract();
292
+ let role = Symbol::new(&env, "ROLE_ADMIN_AUTH");
293
+ let admin = Symbol::new(&env, "ADMIN");
294
+
295
+ // No mock_auth -> only_auth/authorizer.require_auth() must fail.
296
+ client.set_role_admin(&role, &admin);
297
+ }
298
+
299
+ // ============================================
300
+ // Admin-role pathway + revoke + renounce
301
+ // ============================================
302
+
303
+ #[test]
304
+ fn admin_role_holder_can_grant_revoke_and_renounce_emits_events() {
305
+ let (env, contract_id, authorizer, client) = setup_contract();
306
+
307
+ let role = Symbol::new(&env, "ROLE4");
308
+ let admin_role = Symbol::new(&env, "ADMIN_ROLE");
309
+ let admin = Address::generate(&env);
310
+ let user = Address::generate(&env);
311
+
312
+ // Authorizer sets admin role for `role`.
313
+ mock_auth(&env, &contract_id, &authorizer, "set_role_admin", (role.clone(), admin_role.clone()));
314
+ client.set_role_admin(&role, &admin_role);
315
+ assert_eq_event(
316
+ &env,
317
+ &contract_id,
318
+ RoleAdminChanged { role: role.clone(), previous_admin_role: None, new_admin_role: Some(admin_role.clone()) },
319
+ );
320
+
321
+ // Authorizer grants the admin role to `admin`.
322
+ mock_auth(&env, &contract_id, &authorizer, "grant_role", (admin.clone(), admin_role.clone(), authorizer.clone()));
323
+ client.grant_role(&admin, &admin_role, &authorizer);
324
+ assert_eq_event(
325
+ &env,
326
+ &contract_id,
327
+ RoleGranted { role: admin_role.clone(), account: admin.clone(), caller: authorizer.clone() },
328
+ );
329
+ assert_eq!(client.has_role(&admin, &admin_role), Some(0));
330
+ assert_eq!(client.get_role_member_count(&admin_role), 1);
331
+
332
+ // Admin grants `role` to `user` (admin-role pathway).
333
+ mock_auth(&env, &contract_id, &admin, "grant_role", (user.clone(), role.clone(), admin.clone()));
334
+ client.grant_role(&user, &role, &admin);
335
+ assert_eq_event(
336
+ &env,
337
+ &contract_id,
338
+ RoleGranted { role: role.clone(), account: user.clone(), caller: admin.clone() },
339
+ );
340
+ assert_eq!(client.has_role(&user, &role), Some(0));
341
+ assert_eq!(client.get_role_member_count(&role), 1);
342
+
343
+ // Admin revokes `role` from `user`.
344
+ mock_auth(&env, &contract_id, &admin, "revoke_role", (user.clone(), role.clone(), admin.clone()));
345
+ client.revoke_role(&user, &role, &admin);
346
+ assert_eq_event(
347
+ &env,
348
+ &contract_id,
349
+ RoleRevoked { role: role.clone(), account: user.clone(), caller: admin.clone() },
350
+ );
351
+ assert_eq!(client.has_role(&user, &role), None);
352
+ assert_eq!(client.get_role_member_count(&role), 0);
353
+ // Admin should still hold the admin role.
354
+ assert_eq!(client.has_role(&admin, &admin_role), Some(0));
355
+
356
+ // Admin grants again, then user renounces.
357
+ mock_auth(&env, &contract_id, &admin, "grant_role", (user.clone(), role.clone(), admin.clone()));
358
+ client.grant_role(&user, &role, &admin);
359
+ assert_eq_event(&env, &contract_id, RoleGranted { role: role.clone(), account: user.clone(), caller: admin });
360
+ assert_eq!(client.has_role(&user, &role), Some(0));
361
+
362
+ mock_auth(&env, &contract_id, &user, "renounce_role", (role.clone(), user.clone()));
363
+ client.renounce_role(&role, &user);
364
+ assert_eq_event(&env, &contract_id, RoleRevoked { role, account: user.clone(), caller: user.clone() });
365
+ assert_eq!(client.has_role(&user, &Symbol::new(&env, "ROLE4")), None);
366
+ assert_eq!(client.get_role_member_count(&Symbol::new(&env, "ROLE4")), 0);
367
+ }
368
+
369
+ #[test]
370
+ #[should_panic(expected = "Error(Auth, InvalidAction)")]
371
+ fn revoke_role_requires_auth() {
372
+ let (env, _contract_id, authorizer, client) = setup_contract();
373
+ let role = Symbol::new(&env, "ROLE_REVOKE_AUTH");
374
+ let acct = Address::generate(&env);
375
+
376
+ // No mock_auth -> caller.require_auth() must fail.
377
+ client.revoke_role(&acct, &role, &authorizer);
378
+ }
379
+
380
+ #[test]
381
+ #[should_panic(expected = "Error(Auth, InvalidAction)")]
382
+ fn renounce_role_requires_auth() {
383
+ let (env, _contract_id, _authorizer, client) = setup_contract();
384
+ let role = Symbol::new(&env, "ROLE_RENOUNCE_AUTH");
385
+ let caller = Address::generate(&env);
386
+
387
+ // No mock_auth -> caller.require_auth() must fail.
388
+ client.renounce_role(&role, &caller);
389
+ }
390
+
391
+ // ============================================
392
+ // revoke_role_no_auth + enumeration branches
393
+ // ============================================
394
+
395
+ #[test]
396
+ fn swap_remove_updates_indices_and_existing_roles_removed_on_last_member() {
397
+ let (env, contract_id, authorizer, client) = setup_contract();
398
+
399
+ let role = Symbol::new(&env, "ROLE6");
400
+ let a1 = Address::generate(&env);
401
+ let a2 = Address::generate(&env);
402
+ let caller = Address::generate(&env);
403
+
404
+ // Single contract context emits 3 events and hits the swap-remove branch.
405
+ env.as_contract(&contract_id, || {
406
+ grant_role_no_auth(&env, &a1, &role, &caller);
407
+ grant_role_no_auth(&env, &a2, &role, &caller);
408
+ revoke_role_no_auth(&env, &a1, &role, &caller);
409
+ });
410
+ assert_eq_events(
411
+ &env,
412
+ &contract_id,
413
+ &[
414
+ &RoleGranted { role: role.clone(), account: a1.clone(), caller: caller.clone() },
415
+ &RoleGranted { role: role.clone(), account: a2.clone(), caller: caller.clone() },
416
+ &RoleRevoked { role: role.clone(), account: a1.clone(), caller: caller.clone() },
417
+ ],
418
+ );
419
+
420
+ // After swap-remove, only a2 should remain at index 0.
421
+ assert_eq!(client.get_role_member_count(&role), 1);
422
+ assert_eq!(client.has_role(&a2, &role), Some(0));
423
+ assert_eq!(client.has_role(&a1, &role), None);
424
+ assert_eq!(client.get_role_member(&role, &0), a2.clone());
425
+ assert_eq!(client.get_existing_roles().len(), 1);
426
+
427
+ // Remove last remaining member; role should be removed from existing roles.
428
+ mock_auth(&env, &contract_id, &authorizer, "revoke_role", (a2.clone(), role.clone(), authorizer.clone()));
429
+ client.revoke_role(&a2, &role, &authorizer);
430
+ assert_eq_event(&env, &contract_id, RoleRevoked { role: role.clone(), account: a2, caller: authorizer });
431
+ assert_eq!(client.get_existing_roles(), Vec::<Symbol>::new(&env));
432
+ }
433
+
434
+ #[test]
435
+ fn revoke_last_member_of_two_no_swap_branch() {
436
+ let (env, contract_id, _authorizer, client) = setup_contract();
437
+
438
+ let role = Symbol::new(&env, "ROLE_NO_SWAP");
439
+ let a1 = Address::generate(&env);
440
+ let a2 = Address::generate(&env);
441
+ let caller = Address::generate(&env);
442
+
443
+ // Emit 3 events in one contract context: grant a1, grant a2, revoke a2.
444
+ // This hits the `to_remove_idx == last_idx` branch with `count > 1`.
445
+ env.as_contract(&contract_id, || {
446
+ grant_role_no_auth(&env, &a1, &role, &caller);
447
+ grant_role_no_auth(&env, &a2, &role, &caller);
448
+ revoke_role_no_auth(&env, &a2, &role, &caller);
449
+ });
450
+
451
+ assert_eq_events(
452
+ &env,
453
+ &contract_id,
454
+ &[
455
+ &RoleGranted { role: role.clone(), account: a1.clone(), caller: caller.clone() },
456
+ &RoleGranted { role: role.clone(), account: a2.clone(), caller: caller.clone() },
457
+ &RoleRevoked { role: role.clone(), account: a2.clone(), caller: caller.clone() },
458
+ ],
459
+ );
460
+
461
+ assert_eq!(client.get_role_member_count(&role), 1);
462
+ assert_eq!(client.has_role(&a1, &role), Some(0));
463
+ assert_eq!(client.has_role(&a2, &role), None);
464
+ assert_eq!(client.get_role_member(&role, &0), a1);
465
+ }
466
+
467
+ // This test intentionally constructs an *inconsistent* RBAC storage state where a role has
468
+ // a member but the role is missing from `ExistingRoles`.
469
+ //
470
+ // The implementation detects this corruption on last-member removal: `remove_from_role_enumeration`
471
+ // calls `existing.first_index_of(role).unwrap_or_panic(..., RoleNotFound)`, which returns
472
+ // `RbacError::RoleNotFound` to the caller (it does not silently skip the removal).
473
+ #[test]
474
+ fn removing_last_member_when_role_missing_from_existing_roles_returns_role_not_found() {
475
+ let (env, contract_id, authorizer, client) = setup_contract();
476
+
477
+ let role = Symbol::new(&env, "ROLE7");
478
+ let acct = Address::generate(&env);
479
+
480
+ // Create a consistent single-member role, but *do not* include it in ExistingRoles.
481
+ client.raw_set_existing_roles(&Vec::<Symbol>::new(&env));
482
+ client.raw_set_role_accounts_count(&role, &1);
483
+ client.raw_set_role_account_to_index(&role, &acct, &0);
484
+ client.raw_set_role_index_to_account(&role, &0, &acct);
485
+
486
+ // Revoking should fail with RoleNotFound because the role is missing from ExistingRoles.
487
+ mock_auth(&env, &contract_id, &authorizer, "revoke_role", (acct.clone(), role.clone(), authorizer.clone()));
488
+ let res = client.try_revoke_role(&acct, &role, &authorizer);
489
+ assert_eq!(res.err().unwrap().ok().unwrap(), RbacError::RoleNotFound.into());
490
+ }
491
+
492
+ // ============================================
493
+ // Remaining error codes: RoleIsEmpty, MaxRolesExceeded
494
+ // ============================================
495
+
496
+ #[test]
497
+ fn role_is_empty_error_code() {
498
+ let (env, contract_id, authorizer, client) = setup_contract();
499
+
500
+ let role = Symbol::new(&env, "ROLE8");
501
+ let acct = Address::generate(&env);
502
+
503
+ // Force: account appears to hold role (mapping exists), but count is 0 -> RoleIsEmpty.
504
+ client.raw_set_role_account_to_index(&role, &acct, &0);
505
+
506
+ mock_auth(&env, &contract_id, &authorizer, "revoke_role", (acct.clone(), role.clone(), authorizer.clone()));
507
+ let res = client.try_revoke_role(&acct, &role, &authorizer);
508
+ assert_eq!(res.err().unwrap().ok().unwrap(), RbacError::RoleIsEmpty.into());
509
+ }
510
+
511
+ #[test]
512
+ fn revoke_role_entrypoint_missing_role_returns_error_code() {
513
+ let (env, contract_id, authorizer, client) = setup_contract();
514
+
515
+ let role = Symbol::new(&env, "ROLE9");
516
+ let acct = Address::generate(&env);
517
+
518
+ // Provide auth so we reach the RoleNotHeld error (not Auth::InvalidAction).
519
+ mock_auth(&env, &contract_id, &authorizer, "revoke_role", (acct.clone(), role.clone(), authorizer.clone()));
520
+ let res = client.try_revoke_role(&acct, &role, &authorizer);
521
+ assert_eq!(res.err().unwrap().ok().unwrap(), RbacError::RoleNotHeld.into());
522
+ }
523
+
524
+ #[test]
525
+ fn revoke_role_entrypoint_unauthorized_returns_error_code() {
526
+ let (env, contract_id, authorizer, client) = setup_contract();
527
+
528
+ let role = Symbol::new(&env, "ROLE10");
529
+ let acct = Address::generate(&env);
530
+ let caller = Address::generate(&env);
531
+
532
+ // Role has an admin role, but caller has neither admin role nor is authorizer -> Unauthorized.
533
+ let admin_role = Symbol::new(&env, "ADMIN_X");
534
+ mock_auth(&env, &contract_id, &authorizer, "set_role_admin", (role.clone(), admin_role.clone()));
535
+ client.set_role_admin(&role, &admin_role);
536
+
537
+ mock_auth(&env, &contract_id, &caller, "revoke_role", (acct.clone(), role.clone(), caller.clone()));
538
+ let res = client.try_revoke_role(&acct, &role, &caller);
539
+ assert_eq!(res.err().unwrap().ok().unwrap(), RbacError::Unauthorized.into());
540
+ }
541
+
542
+ #[test]
543
+ fn max_roles_exceeded_error_code() {
544
+ let (env, contract_id, authorizer, client) = setup_contract();
545
+
546
+ let mut roles = Vec::<Symbol>::new(&env);
547
+ for i in 0..MAX_ROLES {
548
+ let s = std::format!("R{i}");
549
+ roles.push_back(Symbol::new(&env, &s));
550
+ }
551
+ client.raw_set_existing_roles(&roles);
552
+
553
+ let new_role = Symbol::new(&env, "NEW_ROLE");
554
+ let acct = Address::generate(&env);
555
+
556
+ mock_auth(&env, &contract_id, &authorizer, "grant_role", (acct.clone(), new_role.clone(), authorizer.clone()));
557
+ let res = client.try_grant_role(&acct, &new_role, &authorizer);
558
+ assert_eq!(res.err().unwrap().ok().unwrap(), RbacError::MaxRolesExceeded.into());
559
+ }
@@ -0,0 +1,67 @@
1
+ // `utils` crate is `#![no_std]`, but unit tests can use `std`.
2
+ extern crate std;
3
+
4
+ use soroban_sdk::{
5
+ address_payload::AddressPayload,
6
+ testutils::{MockAuth, MockAuthInvoke},
7
+ Address, BytesN, Env, IntoVal, Val,
8
+ };
9
+
10
+ /// Same style as `common-macros` tests helper.
11
+ pub(in crate::tests) fn assert_panics_contains<F>(case: &str, expected_substring: &str, f: F)
12
+ where
13
+ F: FnOnce(),
14
+ {
15
+ use std::any::Any;
16
+ use std::boxed::Box;
17
+ use std::format;
18
+ use std::panic::{catch_unwind, AssertUnwindSafe};
19
+ use std::string::{String, ToString};
20
+
21
+ let result = catch_unwind(AssertUnwindSafe(f));
22
+ assert!(result.is_err(), "{case}: expected panic, but function returned normally");
23
+
24
+ let payload: Box<dyn Any + Send> = result.expect_err("checked above");
25
+ let msg = if let Some(s) = payload.downcast_ref::<&str>() {
26
+ (*s).to_string()
27
+ } else if let Some(s) = payload.downcast_ref::<String>() {
28
+ s.clone()
29
+ } else {
30
+ format!("{payload:?}")
31
+ };
32
+
33
+ assert!(
34
+ msg.contains(expected_substring),
35
+ "{case}: expected panic message to contain '{expected_substring}', got '{msg}'"
36
+ );
37
+ }
38
+
39
+ /// Helper to assert address payload matches expected payload.
40
+ ///
41
+ /// Used in buffer_reader and buffer_writer tests to verify address encoding/decoding.
42
+ pub(in crate::tests) fn assert_address_payload_matches(actual: BytesN<32>, expected: AddressPayload) {
43
+ match expected {
44
+ AddressPayload::ContractIdHash(expected_payload) => {
45
+ assert_eq!(actual, expected_payload);
46
+ }
47
+ AddressPayload::AccountIdPublicKeyEd25519(expected_payload) => {
48
+ assert_eq!(actual, expected_payload);
49
+ }
50
+ }
51
+ }
52
+
53
+ /// Test helper to mock a single contract invocation auth.
54
+ ///
55
+ /// This keeps test files from repeating `env.mock_auths(&[MockAuth { ... }])` blocks.
56
+ pub(in crate::tests) fn mock_auth<A: IntoVal<Env, soroban_sdk::Vec<Val>>>(
57
+ env: &Env,
58
+ contract_id: &Address,
59
+ address: &Address,
60
+ fn_name: &'static str,
61
+ args: A,
62
+ ) {
63
+ env.mock_auths(&[MockAuth {
64
+ address,
65
+ invoke: &MockAuthInvoke { contract: contract_id, args: args.into_val(env), fn_name, sub_invokes: &[] },
66
+ }]);
67
+ }