From a6ef2dcf0ada39342ea9411202ad962c49123038 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Thu, 5 Feb 2026 08:28:28 +0100 Subject: [PATCH 01/23] feat(frontend): ephemeral access keys - support expires_at --- src/libs/shared/src/segments/controllers.rs | 48 +++++++++++++++++++-- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/src/libs/shared/src/segments/controllers.rs b/src/libs/shared/src/segments/controllers.rs index dad87a693e..5da75091bd 100644 --- a/src/libs/shared/src/segments/controllers.rs +++ b/src/libs/shared/src/segments/controllers.rs @@ -96,7 +96,10 @@ pub fn controller_can_write(caller: UserId, controllers: &Controllers) -> bool { .iter() .any(|(&controller_id, controller)| match controller.scope { ControllerScope::Submit => false, - _ => principal_equal(controller_id, caller), + _ => { + principal_equal(controller_id, caller) + && is_controller_not_expired(controller) + } })) } @@ -111,9 +114,46 @@ pub fn controller_can_write(caller: UserId, controllers: &Controllers) -> bool { pub fn is_controller(caller: UserId, controllers: &Controllers) -> bool { principal_not_anonymous(caller) && (caller_is_self(caller) - || controllers - .iter() - .any(|(&controller_id, _)| principal_equal(controller_id, caller))) + || controllers.iter().any(|(&controller_id, controller)| { + principal_equal(controller_id, caller) && is_controller_not_expired(controller) + })) +} + +/// Checks if a controller (access key) has not expired. +/// +/// Admin controllers never expire. Other controllers are considered not expired if: +/// - They have no expiration date set, or +/// - Their expiration date is in the future +/// +/// # Arguments +/// - `controller`: The controller to check +/// +/// # Returns +/// `true` if the controller has not expired, `false` otherwise. +fn is_controller_not_expired(controller: &Controller) -> bool { + !is_controller_expired(controller) +} + +/// Checks if a controller (access key) has expired. +/// +/// Admin controllers never expire. Other controllers are considered expired if: +/// - They have an expiration date set, and +/// - That expiration date is in the past +/// +/// # Arguments +/// - `controller`: The controller to check +/// +/// # Returns +/// `true` if the controller has expired, `false` otherwise. +fn is_controller_expired(controller: &Controller) -> bool { + // Admin controller cannot expire + if matches!(controller.scope, ControllerScope::Admin) { + return false; + } + + controller + .expires_at + .map_or(false, |expires_at| expires_at >= time()) } /// Checks if a caller is an admin controller. From fe42a71d9ca0b3f8c0fcd619ca955fbc08612972 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Thu, 5 Feb 2026 08:39:19 +0100 Subject: [PATCH 02/23] docs: review text --- src/libs/shared/src/segments/controllers.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/shared/src/segments/controllers.rs b/src/libs/shared/src/segments/controllers.rs index 44ec671f30..b213a3b8d5 100644 --- a/src/libs/shared/src/segments/controllers.rs +++ b/src/libs/shared/src/segments/controllers.rs @@ -81,7 +81,7 @@ pub fn delete_controllers(remove_controllers: &[UserId], controllers: &mut Contr } } -/// Checks if a caller is a controller with admin or write scope (permissions). +/// Checks if a caller is a non-expired controller with admin or write scope (permissions). /// /// # Arguments /// - `caller`: `UserId` of the caller. @@ -103,7 +103,7 @@ pub fn controller_can_write(caller: UserId, controllers: &Controllers) -> bool { })) } -/// Checks if a caller is a valid controller regardless of its scope (admin, write or submit). +/// Checks if a caller is a non-expired controller regardless of scope (admin, write, or submit). /// /// # Arguments /// - `caller`: `UserId` of the caller. From e30cf5e00062709c978690ba4579d059d33a4b8d Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Thu, 5 Feb 2026 09:09:51 +0100 Subject: [PATCH 03/23] test: expired controllers --- src/libs/shared/src/ic/api.rs | 6 ++-- src/libs/shared/src/segments/controllers.rs | 38 ++++++++++++++++++++- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/libs/shared/src/ic/api.rs b/src/libs/shared/src/ic/api.rs index 373db11f58..7200d8491d 100644 --- a/src/libs/shared/src/ic/api.rs +++ b/src/libs/shared/src/ic/api.rs @@ -1,9 +1,7 @@ use candid::Principal; #[cfg(target_arch = "wasm32")] -use ic_cdk::api::{ - canister_self, debug_print, is_controller as ic_is_controller, msg_caller, time as ic_time, -}; +use ic_cdk::api::{canister_self, debug_print, msg_caller, time as ic_time, is_controller as ic_is_controller}; /// Returns the **principal** of the current module. /// @@ -100,4 +98,4 @@ pub fn is_canister_controller(principal: &Principal) -> bool { // For tests, return true for specific test principals or implement mock logic // This is a simple mock - adjust based on your needs *principal == Principal::from_text("ck4tp-3iaaa-aaaal-ab7da-cai").unwrap() -} +} \ No newline at end of file diff --git a/src/libs/shared/src/segments/controllers.rs b/src/libs/shared/src/segments/controllers.rs index 262def368a..5d46abdf51 100644 --- a/src/libs/shared/src/segments/controllers.rs +++ b/src/libs/shared/src/segments/controllers.rs @@ -152,7 +152,7 @@ fn is_controller_expired(controller: &Controller) -> bool { controller .expires_at - .map_or(false, |expires_at| expires_at >= time()) + .map_or(false, |expires_at| expires_at < time()) } /// Checks if a caller is an admin controller. @@ -365,6 +365,42 @@ mod tests { } } + #[test] + fn test_is_controller_expired_admin_never_expires() { + let admin = create_controller(ControllerScope::Admin, Some(mock_time() - 1000), None); + assert!(!is_controller_expired(&admin)); + } + + #[test] + fn test_is_controller_expired_no_expiration() { + let controller = create_controller(ControllerScope::Write, None, None); + assert!(!is_controller_expired(&controller)); + } + + #[test] + fn test_is_controller_expired_future_expiration() { + let controller = create_controller(ControllerScope::Write, Some(time() + 1_000_000), None); + assert!(!is_controller_expired(&controller)); + } + + #[test] + fn test_is_controller_not_expired() { + let admin = create_controller(ControllerScope::Admin, Some(time() - 1000), None); + assert!(is_controller_not_expired(&admin)); + + let expired = create_controller(ControllerScope::Write, Some(time() - 1), None); + assert!(!is_controller_not_expired(&expired)); + + let valid = create_controller(ControllerScope::Write, Some(time() + 1000), None); + assert!(is_controller_not_expired(&valid)); + } + + #[test] + fn test_is_controller_expired_past_expiration() { + let controller = create_controller(ControllerScope::Write, Some(mock_time() - 1), None); + assert!(is_controller_expired(&controller)); + } + #[test] fn test_controller_can_write_anonymous_rejected() { let controllers = Controllers::new(); From 6e46110c50b15d0e56dc75f713a462e09ad0de2e Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Thu, 5 Feb 2026 09:10:02 +0100 Subject: [PATCH 04/23] chore: fmt --- src/libs/shared/src/ic/api.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/libs/shared/src/ic/api.rs b/src/libs/shared/src/ic/api.rs index 7200d8491d..373db11f58 100644 --- a/src/libs/shared/src/ic/api.rs +++ b/src/libs/shared/src/ic/api.rs @@ -1,7 +1,9 @@ use candid::Principal; #[cfg(target_arch = "wasm32")] -use ic_cdk::api::{canister_self, debug_print, msg_caller, time as ic_time, is_controller as ic_is_controller}; +use ic_cdk::api::{ + canister_self, debug_print, is_controller as ic_is_controller, msg_caller, time as ic_time, +}; /// Returns the **principal** of the current module. /// @@ -98,4 +100,4 @@ pub fn is_canister_controller(principal: &Principal) -> bool { // For tests, return true for specific test principals or implement mock logic // This is a simple mock - adjust based on your needs *principal == Principal::from_text("ck4tp-3iaaa-aaaal-ab7da-cai").unwrap() -} \ No newline at end of file +} From e3c3ab153c7a05deb295ea7807ab6e5856a18b2a Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Thu, 5 Feb 2026 09:15:08 +0100 Subject: [PATCH 05/23] chore: merge main --- src/libs/shared/src/segments/controllers.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/shared/src/segments/controllers.rs b/src/libs/shared/src/segments/controllers.rs index 0fd8a78d3a..5d46abdf51 100644 --- a/src/libs/shared/src/segments/controllers.rs +++ b/src/libs/shared/src/segments/controllers.rs @@ -652,7 +652,6 @@ mod tests { assert!(is_admin_controller(admin_principal, &controllers)); } - #[test] fn test_is_admin_controller_not_canister_controller() { let mut controllers = Controllers::new(); From 63105a1f7d420236f572659e907e9ed8af6cd522 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Thu, 5 Feb 2026 10:06:21 +0100 Subject: [PATCH 06/23] test: init guards --- .../satellite.controllers.guards.spec.ts | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 src/tests/specs/satellite/stock/controllers/satellite.controllers.guards.spec.ts diff --git a/src/tests/specs/satellite/stock/controllers/satellite.controllers.guards.spec.ts b/src/tests/specs/satellite/stock/controllers/satellite.controllers.guards.spec.ts new file mode 100644 index 0000000000..a3af5002f0 --- /dev/null +++ b/src/tests/specs/satellite/stock/controllers/satellite.controllers.guards.spec.ts @@ -0,0 +1,125 @@ +import { idlFactorySatellite, type SatelliteActor, type SatelliteDid } from '$declarations'; +import { toBigIntNanoSeconds } from '$lib/utils/date.utils'; +import { type Actor, PocketIc } from '@dfinity/pic'; +import { toNullable } from '@dfinity/utils'; +import { Ed25519KeyIdentity } from '@icp-sdk/core/identity'; +import { + JUNO_AUTH_ERROR_NOT_CONTROLLER, + JUNO_AUTH_ERROR_NOT_WRITE_CONTROLLER +} from '@junobuild/errors'; +import { inject } from 'vitest'; +import { CONTROLLER_METADATA } from '../../../../constants/controller-tests.constants'; +import { controllersInitArgs, SATELLITE_WASM_PATH } from '../../../../utils/setup-tests.utils'; + +describe.each([ + { title: 'heap', memory: { Heap: null } }, + { title: 'stable', memory: { Stable: null } } +])('Satellite > Controllers > Guards $title', ({ memory }) => { + let pic: PocketIc; + let actor: Actor; + + const controller = Ed25519KeyIdentity.generate(); + + const TEST_COLLECTION = 'test'; + + const currentDate = new Date(2021, 6, 10, 0, 0, 0, 0); + + beforeAll(async () => { + pic = await PocketIc.create(inject('PIC_URL')); + + await pic.setTime(currentDate.getTime()); + + const { actor: c } = await pic.setupCanister({ + idlFactory: idlFactorySatellite, + wasm: SATELLITE_WASM_PATH, + arg: controllersInitArgs(controller), + sender: controller.getPrincipal() + }); + + actor = c; + + actor.setIdentity(controller); + + const setRule: SatelliteDid.SetRule = { + memory: toNullable(memory), + max_size: toNullable(), + read: { Managed: null }, + mutable_permissions: toNullable(), + write: { Managed: null }, + version: toNullable(), + max_capacity: toNullable(), + rate_config: toNullable(), + max_changes_per_user: toNullable() + }; + + const { set_rule } = actor; + await set_rule({ Db: null }, TEST_COLLECTION, setRule); + }); + + afterAll(async () => { + await pic?.tearDown(); + }); + + describe.each([ + { title: 'write', scope: { Write: null } }, + { title: 'submit', scope: { Submit: null } } + ])('Caller is valid controller $title', ({ scope }) => { + const testController = Ed25519KeyIdentity.generate(); + + beforeAll(async () => { + actor.setIdentity(controller); + + const { set_controllers } = actor; + + await set_controllers({ + controller: { + ...CONTROLLER_METADATA, + scope, + expires_at: [toBigIntNanoSeconds(currentDate)] + }, + controllers: [testController.getPrincipal()] + }); + + actor.setIdentity(testController); + + await pic.advanceTime(100); + }); + + it('should throw on get_proposal', async () => { + const { get_proposal } = actor; + + await expect(get_proposal(123n)).rejects.toThrowError(JUNO_AUTH_ERROR_NOT_CONTROLLER); + }); + }); + + describe('Caller with write', () => { + const testController = Ed25519KeyIdentity.generate(); + + beforeAll(async () => { + actor.setIdentity(controller); + + const { set_controllers } = actor; + + await set_controllers({ + controller: { + ...CONTROLLER_METADATA, + scope: { Write: null }, + expires_at: [toBigIntNanoSeconds(currentDate)] + }, + controllers: [testController.getPrincipal()] + }); + + actor.setIdentity(testController); + + await pic.advanceTime(100); + }); + + it('should throw on del_docs', async () => { + const { del_docs } = actor; + + await expect(del_docs(TEST_COLLECTION)).rejects.toThrowError( + JUNO_AUTH_ERROR_NOT_WRITE_CONTROLLER + ); + }); + }); +}); From c5779fcdeeefc64db8756815b3c1f52ff62271cd Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Thu, 5 Feb 2026 10:33:51 +0100 Subject: [PATCH 07/23] test: guards --- .../satellite.controllers.guards.spec.ts | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/src/tests/specs/satellite/stock/controllers/satellite.controllers.guards.spec.ts b/src/tests/specs/satellite/stock/controllers/satellite.controllers.guards.spec.ts index a3af5002f0..320e2a0b4a 100644 --- a/src/tests/specs/satellite/stock/controllers/satellite.controllers.guards.spec.ts +++ b/src/tests/specs/satellite/stock/controllers/satellite.controllers.guards.spec.ts @@ -9,6 +9,7 @@ import { } from '@junobuild/errors'; import { inject } from 'vitest'; import { CONTROLLER_METADATA } from '../../../../constants/controller-tests.constants'; +import { mockListProposalsParams } from '../../../../mocks/list.mocks'; import { controllersInitArgs, SATELLITE_WASM_PATH } from '../../../../utils/setup-tests.utils'; describe.each([ @@ -90,6 +91,120 @@ describe.each([ await expect(get_proposal(123n)).rejects.toThrowError(JUNO_AUTH_ERROR_NOT_CONTROLLER); }); + + it('should throw on list_proposals', async () => { + const { list_proposals } = actor; + + await expect(list_proposals(mockListProposalsParams)).rejects.toThrowError( + JUNO_AUTH_ERROR_NOT_CONTROLLER + ); + }); + + it('should throw on count_proposals', async () => { + const { count_proposals } = actor; + + await expect(count_proposals()).rejects.toThrowError(JUNO_AUTH_ERROR_NOT_CONTROLLER); + }); + + it('should throw on init_proposal', async () => { + const { init_proposal } = actor; + + await expect( + init_proposal({ + AssetsUpgrade: { + clear_existing_assets: toNullable() + } + }) + ).rejects.toThrowError(JUNO_AUTH_ERROR_NOT_CONTROLLER); + }); + + it('should throw on submit_proposal', async () => { + const { submit_proposal } = actor; + + await expect(submit_proposal(123n)).rejects.toThrowError(JUNO_AUTH_ERROR_NOT_CONTROLLER); + }); + + it('should throw on init_proposal_asset_upload', async () => { + const { init_proposal_asset_upload } = actor; + + await expect( + init_proposal_asset_upload( + { + collection: TEST_COLLECTION, + description: toNullable(), + encoding_type: [], + full_path: '/test', + name: '/test', + token: toNullable() + }, + 123n + ) + ).rejects.toThrowError(JUNO_AUTH_ERROR_NOT_CONTROLLER); + }); + + it('should throw on init_proposal_many_assets_upload', async () => { + const { init_proposal_many_assets_upload } = actor; + + await expect( + init_proposal_many_assets_upload( + [ + { + collection: TEST_COLLECTION, + description: toNullable(), + encoding_type: [], + full_path: '/test', + name: '/test', + token: toNullable() + } + ], + 123n + ) + ).rejects.toThrowError(JUNO_AUTH_ERROR_NOT_CONTROLLER); + }); + + it('should throw on upload_proposal_asset_chunk', async () => { + const { upload_proposal_asset_chunk } = actor; + + await expect( + upload_proposal_asset_chunk({ + batch_id: 123n, + content: Uint8Array.from([1, 2]), + order_id: [0n] + }) + ).rejects.toThrowError(JUNO_AUTH_ERROR_NOT_CONTROLLER); + }); + + it('should throw on commit_proposal_asset_upload', async () => { + const { commit_proposal_asset_upload } = actor; + + await expect( + commit_proposal_asset_upload({ + batch_id: 12n, + chunk_ids: [1n], + headers: [] + }) + ).rejects.toThrowError(JUNO_AUTH_ERROR_NOT_CONTROLLER); + }); + + it('should throw on commit_proposal_many_assets_upload', async () => { + const { commit_proposal_many_assets_upload } = actor; + + await expect( + commit_proposal_many_assets_upload([ + { + batch_id: 12n, + chunk_ids: [1n], + headers: [] + } + ]) + ).rejects.toThrowError(JUNO_AUTH_ERROR_NOT_CONTROLLER); + }); + + it('should throw on memory_size', async () => { + const { memory_size } = actor; + + await expect(memory_size()).rejects.toThrowError(JUNO_AUTH_ERROR_NOT_CONTROLLER); + }); }); describe('Caller with write', () => { From bad47887564cfac9547dda8a2e20cfb8699d3041 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Thu, 5 Feb 2026 10:59:07 +0100 Subject: [PATCH 08/23] test: write --- .../satellite.controllers.guards.spec.ts | 73 +++++++++++++++---- 1 file changed, 57 insertions(+), 16 deletions(-) diff --git a/src/tests/specs/satellite/stock/controllers/satellite.controllers.guards.spec.ts b/src/tests/specs/satellite/stock/controllers/satellite.controllers.guards.spec.ts index 320e2a0b4a..ecdce332d3 100644 --- a/src/tests/specs/satellite/stock/controllers/satellite.controllers.guards.spec.ts +++ b/src/tests/specs/satellite/stock/controllers/satellite.controllers.guards.spec.ts @@ -1,4 +1,4 @@ -import { idlFactorySatellite, type SatelliteActor, type SatelliteDid } from '$declarations'; +import { idlFactorySatellite, type SatelliteActor } from '$declarations'; import { toBigIntNanoSeconds } from '$lib/utils/date.utils'; import { type Actor, PocketIc } from '@dfinity/pic'; import { toNullable } from '@dfinity/utils'; @@ -40,21 +40,6 @@ describe.each([ actor = c; actor.setIdentity(controller); - - const setRule: SatelliteDid.SetRule = { - memory: toNullable(memory), - max_size: toNullable(), - read: { Managed: null }, - mutable_permissions: toNullable(), - write: { Managed: null }, - version: toNullable(), - max_capacity: toNullable(), - rate_config: toNullable(), - max_changes_per_user: toNullable() - }; - - const { set_rule } = actor; - await set_rule({ Db: null }, TEST_COLLECTION, setRule); }); afterAll(async () => { @@ -236,5 +221,61 @@ describe.each([ JUNO_AUTH_ERROR_NOT_WRITE_CONTROLLER ); }); + + it('should throw on count_collection_docs', async () => { + const { count_collection_docs } = actor; + + await expect(count_collection_docs(TEST_COLLECTION)).rejects.toThrowError( + JUNO_AUTH_ERROR_NOT_WRITE_CONTROLLER + ); + }); + + it('should throw on reject_proposal', async () => { + const { reject_proposal } = actor; + + await expect( + reject_proposal({ + proposal_id: 1123n, + sha256: Uint8Array.from([1, 2]) + }) + ).rejects.toThrowError(JUNO_AUTH_ERROR_NOT_WRITE_CONTROLLER); + }); + + it('should throw on commit_proposal', async () => { + const { commit_proposal } = actor; + + await expect( + commit_proposal({ + proposal_id: 1123n, + sha256: Uint8Array.from([1, 2]) + }) + ).rejects.toThrowError(JUNO_AUTH_ERROR_NOT_WRITE_CONTROLLER); + }); + + it('should throw on delete_proposal_assets', async () => { + const { delete_proposal_assets } = actor; + + await expect( + delete_proposal_assets({ + proposal_ids: [1n] + }) + ).rejects.toThrowError(JUNO_AUTH_ERROR_NOT_WRITE_CONTROLLER); + }); + + it('should throw on del_assets', async () => { + const { del_assets } = actor; + + await expect(del_assets(TEST_COLLECTION)).rejects.toThrowError( + JUNO_AUTH_ERROR_NOT_WRITE_CONTROLLER + ); + }); + + it('should throw on count_collection_assets', async () => { + const { count_collection_assets } = actor; + + await expect(count_collection_assets(TEST_COLLECTION)).rejects.toThrowError( + JUNO_AUTH_ERROR_NOT_WRITE_CONTROLLER + ); + }); }); }); From dd6ff608f525e526879b7b0eef8bcf4381bc5fdd Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Thu, 5 Feb 2026 12:26:02 +0100 Subject: [PATCH 09/23] feat: controller logic --- src/libs/collections/src/assert/stores.rs | 397 +++++++++++++++++++++- 1 file changed, 392 insertions(+), 5 deletions(-) diff --git a/src/libs/collections/src/assert/stores.rs b/src/libs/collections/src/assert/stores.rs index 042a9ed062..19405b9622 100644 --- a/src/libs/collections/src/assert/stores.rs +++ b/src/libs/collections/src/assert/stores.rs @@ -22,9 +22,12 @@ pub fn assert_permission_with( ) -> bool { match permission { Permission::Public => true, - Permission::Private => assert_caller(caller, owner), + Permission::Private => is_owner_and_valid(caller, owner, controllers), Permission::Managed => { - assert_caller(caller, owner) || controller_can_write(caller, controllers) + // if owner, then it's either not a controller or a valid controller + // else if valid controller + is_owner_and_valid(caller, owner, controllers) + || controller_can_write(caller, controllers) } Permission::Controllers => is_allowed_controller(caller, controllers), } @@ -48,13 +51,16 @@ pub fn assert_create_permission_with( ) -> bool { match permission { Permission::Public => true, - Permission::Private => assert_not_anonymous(caller), - Permission::Managed => assert_not_anonymous(caller), Permission::Controllers => is_allowed_controller(caller, controllers), + _ => { + assert_not_anonymous(caller) + && (is_not_controller(caller, controllers) + || is_controller_and_allowed(caller, controllers, is_allowed_controller)) + } } } -fn assert_caller(caller: Principal, owner: Principal) -> bool { +fn is_owner(caller: Principal, owner: Principal) -> bool { principal_not_anonymous_and_equal(caller, owner) } @@ -65,3 +71,384 @@ fn assert_not_anonymous(caller: Principal) -> bool { pub fn public_permission(permission: &Permission) -> bool { matches!(permission, Permission::Public) } + +fn is_controller(caller: Principal, controllers: &Controllers) -> bool { + controllers.contains_key(&caller) +} + +fn is_not_controller(caller: Principal, controllers: &Controllers) -> bool { + !is_controller(caller, controllers) +} + +fn is_controller_and_allowed( + caller: Principal, + controllers: &Controllers, + is_allowed_controller: fn(UserId, &Controllers) -> bool, +) -> bool { + is_controller(caller, controllers) && is_allowed_controller(caller, controllers) +} + +fn is_owner_and_valid(caller: Principal, owner: Principal, controllers: &Controllers) -> bool { + is_owner(caller, owner) + && (is_not_controller(caller, controllers) || controller_can_write(caller, controllers)) +} + +#[cfg(test)] +mod tests { + use super::*; + use junobuild_shared::types::state::{Controller, ControllerScope, Controllers}; + use std::collections::HashMap; + + fn test_principal(id: u8) -> Principal { + Principal::from_slice(&[id]) + } + + fn create_controller(scope: ControllerScope, expires_at: Option) -> Controller { + Controller { + metadata: HashMap::new(), + created_at: 1000, + updated_at: 1000, + expires_at, + scope, + kind: None, + } + } + + fn mock_time() -> u64 { + 1_000_000_000_000 + } + + #[test] + fn test_is_owner() { + let owner = test_principal(1); + let caller = test_principal(1); + let other = test_principal(2); + + assert!(is_owner(caller, owner)); + assert!(!is_owner(other, owner)); + assert!(!is_owner(Principal::anonymous(), owner)); + } + + #[test] + fn test_is_controller() { + let mut controllers = Controllers::new(); + let controller_principal = test_principal(1); + let non_controller = test_principal(2); + + controllers.insert( + controller_principal, + create_controller(ControllerScope::Write, None), + ); + + assert!(is_controller(controller_principal, &controllers)); + assert!(!is_controller(non_controller, &controllers)); + } + + #[test] + fn test_public_permission_allows_anyone() { + let controllers = Controllers::new(); + let owner = test_principal(1); + let caller = test_principal(2); + + assert!(assert_permission( + &Permission::Public, + owner, + caller, + &controllers + )); + } + + #[test] + fn test_private_permission_allows_owner_not_controller() { + let controllers = Controllers::new(); + let owner = test_principal(1); + + assert!(assert_permission( + &Permission::Private, + owner, + owner, + &controllers + )); + } + + #[test] + fn test_private_permission_allows_owner_valid_controller() { + let mut controllers = Controllers::new(); + let owner = test_principal(1); + + controllers.insert( + owner, + create_controller(ControllerScope::Write, Some(mock_time() + 1000)), + ); + + assert!(assert_permission( + &Permission::Private, + owner, + owner, + &controllers + )); + } + + #[test] + fn test_private_permission_rejects_owner_expired_controller() { + let mut controllers = Controllers::new(); + let owner = test_principal(1); + + controllers.insert( + owner, + create_controller(ControllerScope::Write, Some(mock_time() - 1)), + ); + + assert!(!assert_permission( + &Permission::Private, + owner, + owner, + &controllers + )); + } + + #[test] + fn test_private_permission_rejects_non_owner() { + let controllers = Controllers::new(); + let owner = test_principal(1); + let caller = test_principal(2); + + assert!(!assert_permission( + &Permission::Private, + owner, + caller, + &controllers + )); + } + + #[test] + fn test_managed_permission_allows_owner_not_controller() { + let controllers = Controllers::new(); + let owner = test_principal(1); + + assert!(assert_permission( + &Permission::Managed, + owner, + owner, + &controllers + )); + } + + #[test] + fn test_managed_permission_allows_owner_valid_controller() { + let mut controllers = Controllers::new(); + let owner = test_principal(1); + + controllers.insert( + owner, + create_controller(ControllerScope::Write, Some(mock_time() + 1000)), + ); + + assert!(assert_permission( + &Permission::Managed, + owner, + owner, + &controllers + )); + } + + #[test] + fn test_managed_permission_rejects_owner_expired_controller() { + let mut controllers = Controllers::new(); + let owner = test_principal(1); + + controllers.insert( + owner, + create_controller(ControllerScope::Write, Some(mock_time() - 1)), + ); + + assert!(!assert_permission( + &Permission::Managed, + owner, + owner, + &controllers + )); + } + + #[test] + fn test_managed_permission_allows_valid_write_controller() { + let mut controllers = Controllers::new(); + let owner = test_principal(1); + let controller = test_principal(2); + + controllers.insert( + controller, + create_controller(ControllerScope::Write, Some(mock_time() + 1000)), + ); + + assert!(assert_permission( + &Permission::Managed, + owner, + controller, + &controllers + )); + } + + #[test] + fn test_managed_permission_rejects_expired_write_controller() { + let mut controllers = Controllers::new(); + let owner = test_principal(1); + let controller = test_principal(2); + + controllers.insert( + controller, + create_controller(ControllerScope::Write, Some(mock_time() - 1)), + ); + + assert!(!assert_permission( + &Permission::Managed, + owner, + controller, + &controllers + )); + } + + #[test] + fn test_managed_permission_rejects_submit_controller() { + let mut controllers = Controllers::new(); + let owner = test_principal(1); + let controller = test_principal(2); + + controllers.insert(controller, create_controller(ControllerScope::Submit, None)); + + assert!(!assert_permission( + &Permission::Managed, + owner, + controller, + &controllers + )); + } + + #[test] + fn test_controllers_permission_allows_valid_controller() { + let mut controllers = Controllers::new(); + let owner = test_principal(1); + let controller = test_principal(2); + + controllers.insert( + controller, + create_controller(ControllerScope::Write, Some(mock_time() + 1000)), + ); + + assert!(assert_permission( + &Permission::Controllers, + owner, + controller, + &controllers + )); + } + + #[test] + fn test_controllers_permission_rejects_expired_controller() { + let mut controllers = Controllers::new(); + let owner = test_principal(1); + let controller = test_principal(2); + + controllers.insert( + controller, + create_controller(ControllerScope::Write, Some(mock_time() - 1)), + ); + + assert!(!assert_permission( + &Permission::Controllers, + owner, + controller, + &controllers + )); + } + + // Create permission tests + #[test] + fn test_create_public_allows_anyone() { + let controllers = Controllers::new(); + let caller = test_principal(1); + + assert!(assert_create_permission( + &Permission::Public, + caller, + &controllers + )); + } + + #[test] + fn test_create_private_allows_non_controller() { + let controllers = Controllers::new(); + let caller = test_principal(1); + + assert!(assert_create_permission( + &Permission::Private, + caller, + &controllers + )); + } + + #[test] + fn test_create_private_allows_valid_controller() { + let mut controllers = Controllers::new(); + let caller = test_principal(1); + + controllers.insert( + caller, + create_controller(ControllerScope::Write, Some(mock_time() + 1000)), + ); + + assert!(assert_create_permission( + &Permission::Private, + caller, + &controllers + )); + } + + #[test] + fn test_create_private_rejects_expired_controller() { + let mut controllers = Controllers::new(); + let caller = test_principal(1); + + controllers.insert( + caller, + create_controller(ControllerScope::Write, Some(mock_time() - 1)), + ); + + assert!(!assert_create_permission( + &Permission::Private, + caller, + &controllers + )); + } + + #[test] + fn test_create_controllers_allows_valid_write_controller() { + let mut controllers = Controllers::new(); + let caller = test_principal(1); + + controllers.insert( + caller, + create_controller(ControllerScope::Write, Some(mock_time() + 1000)), + ); + + assert!(assert_create_permission( + &Permission::Controllers, + caller, + &controllers + )); + } + + #[test] + fn test_create_controllers_rejects_submit_controller() { + let mut controllers = Controllers::new(); + let caller = test_principal(1); + + controllers.insert(caller, create_controller(ControllerScope::Submit, None)); + + assert!(!assert_create_permission( + &Permission::Controllers, + caller, + &controllers + )); + } +} From 8b0c7e3d3260fcc5a16f3d51d2530e52782294b6 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Thu, 5 Feb 2026 12:26:34 +0100 Subject: [PATCH 10/23] test: set and get doc --- .../satellite.controllers.datastore.spec.ts | 116 ++++++++++++++++++ .../satellite.controllers.guards.spec.ts | 5 +- 2 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.spec.ts diff --git a/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.spec.ts b/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.spec.ts new file mode 100644 index 0000000000..870a881d41 --- /dev/null +++ b/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.spec.ts @@ -0,0 +1,116 @@ +import { idlFactorySatellite, type SatelliteActor, type SatelliteDid } from '$declarations'; +import { toBigIntNanoSeconds } from '$lib/utils/date.utils'; +import { type Actor, PocketIc } from '@dfinity/pic'; +import { toNullable } from '@dfinity/utils'; +import { Ed25519KeyIdentity } from '@icp-sdk/core/identity'; +import { JUNO_DATASTORE_ERROR_CANNOT_WRITE } from '@junobuild/errors'; +import { inject } from 'vitest'; +import { CONTROLLER_METADATA } from '../../../../constants/controller-tests.constants'; +import { tick } from '../../../../utils/pic-tests.utils'; +import { createDoc as createDocUtils } from '../../../../utils/satellite-doc-tests.utils'; +import { controllersInitArgs, SATELLITE_WASM_PATH } from '../../../../utils/setup-tests.utils'; + +describe.each([ + { title: 'heap', memory: { Heap: null } }, + { title: 'stable', memory: { Stable: null } } +])('Satellite > Controllers > Datastore $title', ({ memory }) => { + let pic: PocketIc; + let actor: Actor; + + const controller = Ed25519KeyIdentity.generate(); + + const TEST_COLLECTION = 'test'; + + const currentDate = new Date(2021, 6, 10, 0, 0, 0, 0); + + const createDoc = (): Promise => + createDocUtils({ + actor, + collection: TEST_COLLECTION + }); + + beforeAll(async () => { + pic = await PocketIc.create(inject('PIC_URL')); + + await pic.setTime(currentDate.getTime()); + + const { actor: c } = await pic.setupCanister({ + idlFactory: idlFactorySatellite, + wasm: SATELLITE_WASM_PATH, + arg: controllersInitArgs(controller), + sender: controller.getPrincipal() + }); + + actor = c; + + actor.setIdentity(controller); + + const setRule: SatelliteDid.SetRule = { + memory: toNullable(memory), + max_size: toNullable(), + read: { Managed: null }, + mutable_permissions: toNullable(), + write: { Managed: null }, + version: toNullable(), + max_capacity: toNullable(), + rate_config: toNullable(), + max_changes_per_user: toNullable() + }; + + const { set_rule } = actor; + await set_rule({ Db: null }, TEST_COLLECTION, setRule); + }); + + afterAll(async () => { + await pic?.tearDown(); + }); + + describe.each([ + { title: 'write', scope: { Write: null } } + // { title: 'submit', scope: { Submit: null } } + ])('Caller is $title', ({ scope }) => { + const generateController = async (futureMilliseconds?: number) => { + actor.setIdentity(controller); + + const testController = Ed25519KeyIdentity.generate(); + + const { set_controllers } = actor; + + await set_controllers({ + controller: { + ...CONTROLLER_METADATA, + scope, + expires_at: [ + toBigIntNanoSeconds(new Date((await pic.getTime()) + (futureMilliseconds ?? 0))) + ] + }, + controllers: [testController.getPrincipal()] + }); + + actor.setIdentity(testController); + }; + + it('should throw on set document', async () => { + await generateController(); + + await pic.advanceTime(100); + + await expect(createDoc()).rejects.toThrowError(JUNO_DATASTORE_ERROR_CANNOT_WRITE); + }); + + it.only('should throw on get document', async () => { + const futureMilliseconds = 10_000; + + await generateController(futureMilliseconds); + + const key = await createDoc(); + + await pic.advanceTime(futureMilliseconds + 1); + await tick(pic); + + const { get_doc } = actor; + + expect(await get_doc(TEST_COLLECTION, key)).toEqual([]); + }); + }); +}); diff --git a/src/tests/specs/satellite/stock/controllers/satellite.controllers.guards.spec.ts b/src/tests/specs/satellite/stock/controllers/satellite.controllers.guards.spec.ts index ecdce332d3..be5e6dfbbb 100644 --- a/src/tests/specs/satellite/stock/controllers/satellite.controllers.guards.spec.ts +++ b/src/tests/specs/satellite/stock/controllers/satellite.controllers.guards.spec.ts @@ -12,10 +12,7 @@ import { CONTROLLER_METADATA } from '../../../../constants/controller-tests.cons import { mockListProposalsParams } from '../../../../mocks/list.mocks'; import { controllersInitArgs, SATELLITE_WASM_PATH } from '../../../../utils/setup-tests.utils'; -describe.each([ - { title: 'heap', memory: { Heap: null } }, - { title: 'stable', memory: { Stable: null } } -])('Satellite > Controllers > Guards $title', ({ memory }) => { +describe('Satellite > Controllers > Guards', () => { let pic: PocketIc; let actor: Actor; From 81f97cc2cad1bcafb11027f1cfa861048a63d17d Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Thu, 5 Feb 2026 12:28:32 +0100 Subject: [PATCH 11/23] test: simplify --- src/libs/collections/src/assert/stores.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/collections/src/assert/stores.rs b/src/libs/collections/src/assert/stores.rs index 19405b9622..5e4252ab17 100644 --- a/src/libs/collections/src/assert/stores.rs +++ b/src/libs/collections/src/assert/stores.rs @@ -55,7 +55,7 @@ pub fn assert_create_permission_with( _ => { assert_not_anonymous(caller) && (is_not_controller(caller, controllers) - || is_controller_and_allowed(caller, controllers, is_allowed_controller)) + || is_allowed_controller(caller, controllers)) } } } From 3abdae9f3a10f8797c0fc8fa61463bcd9470ee02 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Thu, 5 Feb 2026 12:29:03 +0100 Subject: [PATCH 12/23] feat: remove unused --- src/libs/collections/src/assert/stores.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/libs/collections/src/assert/stores.rs b/src/libs/collections/src/assert/stores.rs index 5e4252ab17..e730fc4d90 100644 --- a/src/libs/collections/src/assert/stores.rs +++ b/src/libs/collections/src/assert/stores.rs @@ -80,14 +80,6 @@ fn is_not_controller(caller: Principal, controllers: &Controllers) -> bool { !is_controller(caller, controllers) } -fn is_controller_and_allowed( - caller: Principal, - controllers: &Controllers, - is_allowed_controller: fn(UserId, &Controllers) -> bool, -) -> bool { - is_controller(caller, controllers) && is_allowed_controller(caller, controllers) -} - fn is_owner_and_valid(caller: Principal, owner: Principal, controllers: &Controllers) -> bool { is_owner(caller, owner) && (is_not_controller(caller, controllers) || controller_can_write(caller, controllers)) From 4a0efa3ed75528bfb90b6977ad81a6a4096484a9 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Thu, 5 Feb 2026 12:31:59 +0100 Subject: [PATCH 13/23] feat: write --- src/libs/collections/src/assert/stores.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/collections/src/assert/stores.rs b/src/libs/collections/src/assert/stores.rs index e730fc4d90..957fcccf7d 100644 --- a/src/libs/collections/src/assert/stores.rs +++ b/src/libs/collections/src/assert/stores.rs @@ -55,7 +55,7 @@ pub fn assert_create_permission_with( _ => { assert_not_anonymous(caller) && (is_not_controller(caller, controllers) - || is_allowed_controller(caller, controllers)) + || controller_can_write(caller, controllers)) } } } From 5e20d08a90de13aa03b41dc625c7ed93dad6c887 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Thu, 5 Feb 2026 13:51:15 +0100 Subject: [PATCH 14/23] test: remove only --- .../stock/controllers/satellite.controllers.datastore.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.spec.ts b/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.spec.ts index 870a881d41..2aceb3e4ef 100644 --- a/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.spec.ts +++ b/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.spec.ts @@ -98,7 +98,7 @@ describe.each([ await expect(createDoc()).rejects.toThrowError(JUNO_DATASTORE_ERROR_CANNOT_WRITE); }); - it.only('should throw on get document', async () => { + it('should throw on get document', async () => { const futureMilliseconds = 10_000; await generateController(futureMilliseconds); From 269f3a0e5a4d4d7b3cffc68fda6aa66ad61785d7 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Thu, 5 Feb 2026 15:51:31 +0100 Subject: [PATCH 15/23] test: get list etc. --- .../satellite.controllers.datastore.spec.ts | 67 ++++++++++++++++++- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.spec.ts b/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.spec.ts index 2aceb3e4ef..75692e9775 100644 --- a/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.spec.ts +++ b/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.spec.ts @@ -1,11 +1,12 @@ import { idlFactorySatellite, type SatelliteActor, type SatelliteDid } from '$declarations'; import { toBigIntNanoSeconds } from '$lib/utils/date.utils'; import { type Actor, PocketIc } from '@dfinity/pic'; -import { toNullable } from '@dfinity/utils'; +import { assertNonNullish, fromNullable, toNullable } from '@dfinity/utils'; import { Ed25519KeyIdentity } from '@icp-sdk/core/identity'; import { JUNO_DATASTORE_ERROR_CANNOT_WRITE } from '@junobuild/errors'; import { inject } from 'vitest'; import { CONTROLLER_METADATA } from '../../../../constants/controller-tests.constants'; +import { mockListParams } from '../../../../mocks/list.mocks'; import { tick } from '../../../../utils/pic-tests.utils'; import { createDoc as createDocUtils } from '../../../../utils/satellite-doc-tests.utils'; import { controllersInitArgs, SATELLITE_WASM_PATH } from '../../../../utils/setup-tests.utils'; @@ -98,19 +99,79 @@ describe.each([ await expect(createDoc()).rejects.toThrowError(JUNO_DATASTORE_ERROR_CANNOT_WRITE); }); - it('should throw on get document', async () => { + it('should return empty on get document', async () => { const futureMilliseconds = 10_000; await generateController(futureMilliseconds); const key = await createDoc(); + const { get_doc } = actor; + + expect(await get_doc(TEST_COLLECTION, key)).toHaveLength(1); + await pic.advanceTime(futureMilliseconds + 1); await tick(pic); + expect(await get_doc(TEST_COLLECTION, key)).toEqual([]); + }); + + it('should throw on delete document', async () => { + const futureMilliseconds = 10_000; + + await generateController(futureMilliseconds); + + const key = await createDoc(); + + await pic.advanceTime(futureMilliseconds + 1); + await tick(pic); + + const { del_doc } = actor; + + await expect(del_doc(TEST_COLLECTION, key, { version: [1n] })).rejects.toThrowError( + JUNO_DATASTORE_ERROR_CANNOT_WRITE + ); + }); + + it('should throw on update document', async () => { + const futureMilliseconds = 10_000; + + await generateController(futureMilliseconds); + + const key = await createDoc(); + const { get_doc } = actor; - expect(await get_doc(TEST_COLLECTION, key)).toEqual([]); + const doc = fromNullable(await get_doc(TEST_COLLECTION, key)); + + assertNonNullish(doc); + + await pic.advanceTime(futureMilliseconds + 1); + await tick(pic); + + const { set_doc } = actor; + + await expect( + set_doc(TEST_COLLECTION, key, { + ...doc, + version: doc.version + }) + ).rejects.toThrowError(JUNO_DATASTORE_ERROR_CANNOT_WRITE); + }); + + it('should return empty on list documents', async () => { + const futureMilliseconds = 10_000; + + await generateController(futureMilliseconds); + + const { list_docs } = actor; + + expect((await list_docs(TEST_COLLECTION, mockListParams)).items).toHaveLength(1); + + await pic.advanceTime(futureMilliseconds + 1); + await tick(pic); + + expect((await list_docs(TEST_COLLECTION, mockListParams)).items).toHaveLength(0); }); }); }); From 1274cfb914fa7d5ac9c73f09eb967009028b1b64 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Thu, 5 Feb 2026 15:55:39 +0100 Subject: [PATCH 16/23] test: get list etc. --- .../stock/controllers/satellite.controllers.datastore.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.spec.ts b/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.spec.ts index 75692e9775..0f8b29e5b4 100644 --- a/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.spec.ts +++ b/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.spec.ts @@ -108,7 +108,7 @@ describe.each([ const { get_doc } = actor; - expect(await get_doc(TEST_COLLECTION, key)).toHaveLength(1); + expect(fromNullable(await get_doc(TEST_COLLECTION, key))).not.toBeUndefined(); await pic.advanceTime(futureMilliseconds + 1); await tick(pic); @@ -166,7 +166,7 @@ describe.each([ const { list_docs } = actor; - expect((await list_docs(TEST_COLLECTION, mockListParams)).items).toHaveLength(1); + expect((await list_docs(TEST_COLLECTION, mockListParams)).items).toBeGreaterThan(0); await pic.advanceTime(futureMilliseconds + 1); await tick(pic); From 680098f138ea888d29e04ffc1ddc2086d981bf95 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Thu, 5 Feb 2026 15:56:47 +0100 Subject: [PATCH 17/23] test: candid... --- .../stock/controllers/satellite.controllers.datastore.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.spec.ts b/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.spec.ts index 0f8b29e5b4..f01fcb0768 100644 --- a/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.spec.ts +++ b/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.spec.ts @@ -166,12 +166,12 @@ describe.each([ const { list_docs } = actor; - expect((await list_docs(TEST_COLLECTION, mockListParams)).items).toBeGreaterThan(0); + expect((await list_docs(TEST_COLLECTION, mockListParams)).items_length).toBeGreaterThan(0); await pic.advanceTime(futureMilliseconds + 1); await tick(pic); - expect((await list_docs(TEST_COLLECTION, mockListParams)).items).toHaveLength(0); + expect((await list_docs(TEST_COLLECTION, mockListParams)).items_length).toHaveLength(0); }); }); }); From 7a4aeab04f3a56333ebe70c845e2cddebacb1c60 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Thu, 5 Feb 2026 15:57:50 +0100 Subject: [PATCH 18/23] test: candid... --- .../stock/controllers/satellite.controllers.datastore.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.spec.ts b/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.spec.ts index f01fcb0768..bccf252539 100644 --- a/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.spec.ts +++ b/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.spec.ts @@ -166,12 +166,12 @@ describe.each([ const { list_docs } = actor; - expect((await list_docs(TEST_COLLECTION, mockListParams)).items_length).toBeGreaterThan(0); + expect((await list_docs(TEST_COLLECTION, mockListParams)).items_length).toBeGreaterThan(0n); await pic.advanceTime(futureMilliseconds + 1); await tick(pic); - expect((await list_docs(TEST_COLLECTION, mockListParams)).items_length).toHaveLength(0); + expect((await list_docs(TEST_COLLECTION, mockListParams)).items_length).toEqual(0n); }); }); }); From a190b09f3caf5268a15c3856f2efddde27ecefb1 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Thu, 5 Feb 2026 16:13:26 +0100 Subject: [PATCH 19/23] test: split --- .../satellite.controllers.datastore.spec.ts | 177 ------------------ ...llite.controllers.datastore.submit.spec.ts | 165 ++++++++++++++++ ...ellite.controllers.datastore.write.spec.ts | 172 +++++++++++++++++ 3 files changed, 337 insertions(+), 177 deletions(-) delete mode 100644 src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.spec.ts create mode 100644 src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.submit.spec.ts create mode 100644 src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.write.spec.ts diff --git a/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.spec.ts b/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.spec.ts deleted file mode 100644 index bccf252539..0000000000 --- a/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.spec.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { idlFactorySatellite, type SatelliteActor, type SatelliteDid } from '$declarations'; -import { toBigIntNanoSeconds } from '$lib/utils/date.utils'; -import { type Actor, PocketIc } from '@dfinity/pic'; -import { assertNonNullish, fromNullable, toNullable } from '@dfinity/utils'; -import { Ed25519KeyIdentity } from '@icp-sdk/core/identity'; -import { JUNO_DATASTORE_ERROR_CANNOT_WRITE } from '@junobuild/errors'; -import { inject } from 'vitest'; -import { CONTROLLER_METADATA } from '../../../../constants/controller-tests.constants'; -import { mockListParams } from '../../../../mocks/list.mocks'; -import { tick } from '../../../../utils/pic-tests.utils'; -import { createDoc as createDocUtils } from '../../../../utils/satellite-doc-tests.utils'; -import { controllersInitArgs, SATELLITE_WASM_PATH } from '../../../../utils/setup-tests.utils'; - -describe.each([ - { title: 'heap', memory: { Heap: null } }, - { title: 'stable', memory: { Stable: null } } -])('Satellite > Controllers > Datastore $title', ({ memory }) => { - let pic: PocketIc; - let actor: Actor; - - const controller = Ed25519KeyIdentity.generate(); - - const TEST_COLLECTION = 'test'; - - const currentDate = new Date(2021, 6, 10, 0, 0, 0, 0); - - const createDoc = (): Promise => - createDocUtils({ - actor, - collection: TEST_COLLECTION - }); - - beforeAll(async () => { - pic = await PocketIc.create(inject('PIC_URL')); - - await pic.setTime(currentDate.getTime()); - - const { actor: c } = await pic.setupCanister({ - idlFactory: idlFactorySatellite, - wasm: SATELLITE_WASM_PATH, - arg: controllersInitArgs(controller), - sender: controller.getPrincipal() - }); - - actor = c; - - actor.setIdentity(controller); - - const setRule: SatelliteDid.SetRule = { - memory: toNullable(memory), - max_size: toNullable(), - read: { Managed: null }, - mutable_permissions: toNullable(), - write: { Managed: null }, - version: toNullable(), - max_capacity: toNullable(), - rate_config: toNullable(), - max_changes_per_user: toNullable() - }; - - const { set_rule } = actor; - await set_rule({ Db: null }, TEST_COLLECTION, setRule); - }); - - afterAll(async () => { - await pic?.tearDown(); - }); - - describe.each([ - { title: 'write', scope: { Write: null } } - // { title: 'submit', scope: { Submit: null } } - ])('Caller is $title', ({ scope }) => { - const generateController = async (futureMilliseconds?: number) => { - actor.setIdentity(controller); - - const testController = Ed25519KeyIdentity.generate(); - - const { set_controllers } = actor; - - await set_controllers({ - controller: { - ...CONTROLLER_METADATA, - scope, - expires_at: [ - toBigIntNanoSeconds(new Date((await pic.getTime()) + (futureMilliseconds ?? 0))) - ] - }, - controllers: [testController.getPrincipal()] - }); - - actor.setIdentity(testController); - }; - - it('should throw on set document', async () => { - await generateController(); - - await pic.advanceTime(100); - - await expect(createDoc()).rejects.toThrowError(JUNO_DATASTORE_ERROR_CANNOT_WRITE); - }); - - it('should return empty on get document', async () => { - const futureMilliseconds = 10_000; - - await generateController(futureMilliseconds); - - const key = await createDoc(); - - const { get_doc } = actor; - - expect(fromNullable(await get_doc(TEST_COLLECTION, key))).not.toBeUndefined(); - - await pic.advanceTime(futureMilliseconds + 1); - await tick(pic); - - expect(await get_doc(TEST_COLLECTION, key)).toEqual([]); - }); - - it('should throw on delete document', async () => { - const futureMilliseconds = 10_000; - - await generateController(futureMilliseconds); - - const key = await createDoc(); - - await pic.advanceTime(futureMilliseconds + 1); - await tick(pic); - - const { del_doc } = actor; - - await expect(del_doc(TEST_COLLECTION, key, { version: [1n] })).rejects.toThrowError( - JUNO_DATASTORE_ERROR_CANNOT_WRITE - ); - }); - - it('should throw on update document', async () => { - const futureMilliseconds = 10_000; - - await generateController(futureMilliseconds); - - const key = await createDoc(); - - const { get_doc } = actor; - - const doc = fromNullable(await get_doc(TEST_COLLECTION, key)); - - assertNonNullish(doc); - - await pic.advanceTime(futureMilliseconds + 1); - await tick(pic); - - const { set_doc } = actor; - - await expect( - set_doc(TEST_COLLECTION, key, { - ...doc, - version: doc.version - }) - ).rejects.toThrowError(JUNO_DATASTORE_ERROR_CANNOT_WRITE); - }); - - it('should return empty on list documents', async () => { - const futureMilliseconds = 10_000; - - await generateController(futureMilliseconds); - - const { list_docs } = actor; - - expect((await list_docs(TEST_COLLECTION, mockListParams)).items_length).toBeGreaterThan(0n); - - await pic.advanceTime(futureMilliseconds + 1); - await tick(pic); - - expect((await list_docs(TEST_COLLECTION, mockListParams)).items_length).toEqual(0n); - }); - }); -}); diff --git a/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.submit.spec.ts b/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.submit.spec.ts new file mode 100644 index 0000000000..17d9b47c37 --- /dev/null +++ b/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.submit.spec.ts @@ -0,0 +1,165 @@ +import { idlFactorySatellite, type SatelliteActor, type SatelliteDid } from '$declarations'; +import { type Actor, PocketIc } from '@dfinity/pic'; +import { assertNonNullish, fromNullable, toNullable } from '@dfinity/utils'; +import { Ed25519KeyIdentity } from '@icp-sdk/core/identity'; +import { JUNO_DATASTORE_ERROR_CANNOT_WRITE } from '@junobuild/errors'; +import { inject } from 'vitest'; +import { CONTROLLER_METADATA } from '../../../../constants/controller-tests.constants'; +import { mockListParams } from '../../../../mocks/list.mocks'; +import { createDoc as createDocUtils } from '../../../../utils/satellite-doc-tests.utils'; +import { controllersInitArgs, SATELLITE_WASM_PATH } from '../../../../utils/setup-tests.utils'; + +describe.each([ + { title: 'heap', memory: { Heap: null } }, + { title: 'stable', memory: { Stable: null } } +])('Satellite > Controllers > Datastore $title > Submit', ({ memory }) => { + let pic: PocketIc; + let actor: Actor; + + const controller = Ed25519KeyIdentity.generate(); + + const TEST_COLLECTION = 'test'; + + const currentDate = new Date(2021, 6, 10, 0, 0, 0, 0); + + const createDoc = (): Promise => + createDocUtils({ + actor, + collection: TEST_COLLECTION + }); + + let testWriteController: Ed25519KeyIdentity; + let testSubmitController: Ed25519KeyIdentity; + + beforeAll(async () => { + pic = await PocketIc.create(inject('PIC_URL')); + + await pic.setTime(currentDate.getTime()); + + const { actor: c } = await pic.setupCanister({ + idlFactory: idlFactorySatellite, + wasm: SATELLITE_WASM_PATH, + arg: controllersInitArgs(controller), + sender: controller.getPrincipal() + }); + + actor = c; + + actor.setIdentity(controller); + + const setRule: SatelliteDid.SetRule = { + memory: toNullable(memory), + max_size: toNullable(), + read: { Managed: null }, + mutable_permissions: toNullable(), + write: { Managed: null }, + version: toNullable(), + max_capacity: toNullable(), + rate_config: toNullable(), + max_changes_per_user: toNullable() + }; + + const { set_rule } = actor; + await set_rule({ Db: null }, TEST_COLLECTION, setRule); + }); + + afterAll(async () => { + await pic?.tearDown(); + }); + + beforeEach(async () => { + actor.setIdentity(controller); + + testWriteController = Ed25519KeyIdentity.generate(); + testSubmitController = Ed25519KeyIdentity.generate(); + + const { set_controllers } = actor; + + await set_controllers({ + controller: { + ...CONTROLLER_METADATA, + scope: { Write: null } + }, + controllers: [testWriteController.getPrincipal()] + }); + + await set_controllers({ + controller: { + ...CONTROLLER_METADATA, + scope: { Submit: null } + }, + controllers: [testSubmitController.getPrincipal()] + }); + + actor.setIdentity(testSubmitController); + }); + + it('should throw on set document', async () => { + await pic.advanceTime(100); + + await expect(createDoc()).rejects.toThrowError(JUNO_DATASTORE_ERROR_CANNOT_WRITE); + }); + + it('should return empty on get document', async () => { + actor.setIdentity(testWriteController); + + const key = await createDoc(); + + const { get_doc } = actor; + + expect(fromNullable(await get_doc(TEST_COLLECTION, key))).not.toBeUndefined(); + + actor.setIdentity(testSubmitController); + + expect(await get_doc(TEST_COLLECTION, key)).toEqual([]); + }); + + it('should throw on delete document', async () => { + actor.setIdentity(testWriteController); + + const key = await createDoc(); + + actor.setIdentity(testSubmitController); + + const { del_doc } = actor; + + await expect(del_doc(TEST_COLLECTION, key, { version: [1n] })).rejects.toThrowError( + JUNO_DATASTORE_ERROR_CANNOT_WRITE + ); + }); + + it('should throw on update document', async () => { + actor.setIdentity(testWriteController); + + const key = await createDoc(); + + const { get_doc } = actor; + + const doc = fromNullable(await get_doc(TEST_COLLECTION, key)); + + assertNonNullish(doc); + + actor.setIdentity(testSubmitController); + + const { set_doc } = actor; + + await expect( + set_doc(TEST_COLLECTION, key, { + ...doc, + version: doc.version + }) + ).rejects.toThrowError(JUNO_DATASTORE_ERROR_CANNOT_WRITE); + }); + + it('should return empty on list documents', async () => { + actor.setIdentity(testWriteController); + + const { list_docs } = actor; + + expect((await list_docs(TEST_COLLECTION, mockListParams)).items_length).toBeGreaterThan(0n); + + actor.setIdentity(testSubmitController); + + expect((await list_docs(TEST_COLLECTION, mockListParams)).items_length).toEqual(0n); + }); +}); diff --git a/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.write.spec.ts b/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.write.spec.ts new file mode 100644 index 0000000000..fe0ec8b258 --- /dev/null +++ b/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.write.spec.ts @@ -0,0 +1,172 @@ +import { idlFactorySatellite, type SatelliteActor, type SatelliteDid } from '$declarations'; +import { toBigIntNanoSeconds } from '$lib/utils/date.utils'; +import { type Actor, PocketIc } from '@dfinity/pic'; +import { assertNonNullish, fromNullable, toNullable } from '@dfinity/utils'; +import { Ed25519KeyIdentity } from '@icp-sdk/core/identity'; +import { JUNO_DATASTORE_ERROR_CANNOT_WRITE } from '@junobuild/errors'; +import { inject } from 'vitest'; +import { CONTROLLER_METADATA } from '../../../../constants/controller-tests.constants'; +import { mockListParams } from '../../../../mocks/list.mocks'; +import { tick } from '../../../../utils/pic-tests.utils'; +import { createDoc as createDocUtils } from '../../../../utils/satellite-doc-tests.utils'; +import { controllersInitArgs, SATELLITE_WASM_PATH } from '../../../../utils/setup-tests.utils'; + +describe.each([ + { title: 'heap', memory: { Heap: null } }, + { title: 'stable', memory: { Stable: null } } +])('Satellite > Controllers > Datastore $title > Write', ({ memory }) => { + let pic: PocketIc; + let actor: Actor; + + const controller = Ed25519KeyIdentity.generate(); + + const TEST_COLLECTION = 'test'; + + const currentDate = new Date(2021, 6, 10, 0, 0, 0, 0); + + const createDoc = (): Promise => + createDocUtils({ + actor, + collection: TEST_COLLECTION + }); + + beforeAll(async () => { + pic = await PocketIc.create(inject('PIC_URL')); + + await pic.setTime(currentDate.getTime()); + + const { actor: c } = await pic.setupCanister({ + idlFactory: idlFactorySatellite, + wasm: SATELLITE_WASM_PATH, + arg: controllersInitArgs(controller), + sender: controller.getPrincipal() + }); + + actor = c; + + actor.setIdentity(controller); + + const setRule: SatelliteDid.SetRule = { + memory: toNullable(memory), + max_size: toNullable(), + read: { Managed: null }, + mutable_permissions: toNullable(), + write: { Managed: null }, + version: toNullable(), + max_capacity: toNullable(), + rate_config: toNullable(), + max_changes_per_user: toNullable() + }; + + const { set_rule } = actor; + await set_rule({ Db: null }, TEST_COLLECTION, setRule); + }); + + afterAll(async () => { + await pic?.tearDown(); + }); + + const generateController = async (futureMilliseconds?: number) => { + actor.setIdentity(controller); + + const testController = Ed25519KeyIdentity.generate(); + + const { set_controllers } = actor; + + await set_controllers({ + controller: { + ...CONTROLLER_METADATA, + scope: {Write: null}, + expires_at: [ + toBigIntNanoSeconds(new Date((await pic.getTime()) + (futureMilliseconds ?? 0))) + ] + }, + controllers: [testController.getPrincipal()] + }); + + actor.setIdentity(testController); + }; + + it('should throw on set document', async () => { + await generateController(); + + await pic.advanceTime(100); + + await expect(createDoc()).rejects.toThrowError(JUNO_DATASTORE_ERROR_CANNOT_WRITE); + }); + + it('should return empty on get document', async () => { + const futureMilliseconds = 10_000; + + await generateController(futureMilliseconds); + + const key = await createDoc(); + + const { get_doc } = actor; + + expect(fromNullable(await get_doc(TEST_COLLECTION, key))).not.toBeUndefined(); + + await pic.advanceTime(futureMilliseconds + 1); + await tick(pic); + + expect(await get_doc(TEST_COLLECTION, key)).toEqual([]); + }); + + it('should throw on delete document', async () => { + const futureMilliseconds = 10_000; + + await generateController(futureMilliseconds); + + const key = await createDoc(); + + await pic.advanceTime(futureMilliseconds + 1); + await tick(pic); + + const { del_doc } = actor; + + await expect(del_doc(TEST_COLLECTION, key, { version: [1n] })).rejects.toThrowError( + JUNO_DATASTORE_ERROR_CANNOT_WRITE + ); + }); + + it('should throw on update document', async () => { + const futureMilliseconds = 10_000; + + await generateController(futureMilliseconds); + + const key = await createDoc(); + + const { get_doc } = actor; + + const doc = fromNullable(await get_doc(TEST_COLLECTION, key)); + + assertNonNullish(doc); + + await pic.advanceTime(futureMilliseconds + 1); + await tick(pic); + + const { set_doc } = actor; + + await expect( + set_doc(TEST_COLLECTION, key, { + ...doc, + version: doc.version + }) + ).rejects.toThrowError(JUNO_DATASTORE_ERROR_CANNOT_WRITE); + }); + + it('should return empty on list documents', async () => { + const futureMilliseconds = 10_000; + + await generateController(futureMilliseconds); + + const { list_docs } = actor; + + expect((await list_docs(TEST_COLLECTION, mockListParams)).items_length).toBeGreaterThan(0n); + + await pic.advanceTime(futureMilliseconds + 1); + await tick(pic); + + expect((await list_docs(TEST_COLLECTION, mockListParams)).items_length).toEqual(0n); + }); +}); From 3ffe2f11240362d2b5ced4e1f2f19a6cf7cac7cc Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Thu, 5 Feb 2026 16:36:14 +0100 Subject: [PATCH 20/23] test: write --- ...atellite.controllers.storage.write.spec.ts | 191 ++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 src/tests/specs/satellite/stock/controllers/satellite.controllers.storage.write.spec.ts diff --git a/src/tests/specs/satellite/stock/controllers/satellite.controllers.storage.write.spec.ts b/src/tests/specs/satellite/stock/controllers/satellite.controllers.storage.write.spec.ts new file mode 100644 index 0000000000..79239d2097 --- /dev/null +++ b/src/tests/specs/satellite/stock/controllers/satellite.controllers.storage.write.spec.ts @@ -0,0 +1,191 @@ +import { idlFactorySatellite, type SatelliteActor, type SatelliteDid } from '$declarations'; +import { toBigIntNanoSeconds } from '$lib/utils/date.utils'; +import { type Actor, PocketIc } from '@dfinity/pic'; +import { assertNonNullish, fromNullable, toNullable } from '@dfinity/utils'; +import { Ed25519KeyIdentity } from '@icp-sdk/core/identity'; +import { + JUNO_STORAGE_ERROR_ASSET_NOT_FOUND, + JUNO_STORAGE_ERROR_UPLOAD_NOT_ALLOWED +} from '@junobuild/errors'; +import { inject } from 'vitest'; +import { CONTROLLER_METADATA } from '../../../../constants/controller-tests.constants'; +import { mockListParams } from '../../../../mocks/list.mocks'; +import { tick } from '../../../../utils/pic-tests.utils'; +import { uploadAsset } from '../../../../utils/satellite-storage-tests.utils'; +import { controllersInitArgs, SATELLITE_WASM_PATH } from '../../../../utils/setup-tests.utils'; + +describe.each([ + { title: 'heap', memory: { Heap: null } }, + { title: 'stable', memory: { Stable: null } } +])('Satellite > Controllers > Datastore $title > Write', ({ memory }) => { + let pic: PocketIc; + let actor: Actor; + + const controller = Ed25519KeyIdentity.generate(); + + const TEST_COLLECTION = 'test'; + + const currentDate = new Date(2021, 6, 10, 0, 0, 0, 0); + + const upload = async (params: { + full_path: string; + name: string; + collection: string; + headers?: [string, string][]; + encoding_type?: [] | [string]; + }) => { + await uploadAsset({ + ...params, + actor + }); + }; + + beforeAll(async () => { + pic = await PocketIc.create(inject('PIC_URL')); + + await pic.setTime(currentDate.getTime()); + + const { actor: c } = await pic.setupCanister({ + idlFactory: idlFactorySatellite, + wasm: SATELLITE_WASM_PATH, + arg: controllersInitArgs(controller), + sender: controller.getPrincipal() + }); + + actor = c; + + actor.setIdentity(controller); + + const setRule: SatelliteDid.SetRule = { + memory: toNullable(memory), + max_size: toNullable(), + read: { Managed: null }, + mutable_permissions: toNullable(), + write: { Managed: null }, + version: toNullable(), + max_capacity: toNullable(), + rate_config: toNullable(), + max_changes_per_user: toNullable() + }; + + const { set_rule } = actor; + await set_rule({ Storage: null }, TEST_COLLECTION, setRule); + }); + + afterAll(async () => { + await pic?.tearDown(); + }); + + const generateController = async (futureMilliseconds?: number) => { + actor.setIdentity(controller); + + const testController = Ed25519KeyIdentity.generate(); + + const { set_controllers } = actor; + + await set_controllers({ + controller: { + ...CONTROLLER_METADATA, + scope: { Write: null }, + expires_at: [ + toBigIntNanoSeconds(new Date((await pic.getTime()) + (futureMilliseconds ?? 0))) + ] + }, + controllers: [testController.getPrincipal()] + }); + + actor.setIdentity(testController); + }; + + it('should throw on upload asset', async () => { + await generateController(); + + await pic.advanceTime(100); + + const name = 'hello.html'; + const full_path = `/${TEST_COLLECTION}/${name}`; + + await expect(upload({ full_path, name, collection: TEST_COLLECTION })).rejects.toThrowError( + JUNO_STORAGE_ERROR_UPLOAD_NOT_ALLOWED + ); + }); + + it('should return empty on get asset', async () => { + const futureMilliseconds = 10_000; + + await generateController(futureMilliseconds); + + const name = 'hello.html'; + const full_path = `/${TEST_COLLECTION}/${name}`; + + await upload({ full_path, name, collection: TEST_COLLECTION }); + + const { get_asset } = actor; + + expect(fromNullable(await get_asset(TEST_COLLECTION, full_path))).not.toBeUndefined(); + + await pic.advanceTime(futureMilliseconds + 1); + await tick(pic); + + expect(await get_asset(TEST_COLLECTION, full_path)).toEqual([]); + }); + + it('should throw on delete asset', async () => { + const futureMilliseconds = 10_000; + + await generateController(futureMilliseconds); + + const name = 'hello1.html'; + const full_path = `/${TEST_COLLECTION}/${name}`; + + await upload({ full_path, name, collection: TEST_COLLECTION }); + + await pic.advanceTime(futureMilliseconds + 1); + await tick(pic); + + const { del_asset } = actor; + + await expect(del_asset(TEST_COLLECTION, full_path)).rejects.toThrowError( + JUNO_STORAGE_ERROR_ASSET_NOT_FOUND + ); + }); + + it('should throw on update asset', async () => { + const futureMilliseconds = 10_000; + + await generateController(futureMilliseconds); + + const name = 'hello2.html'; + const full_path = `/${TEST_COLLECTION}/${name}`; + + await upload({ full_path, name, collection: TEST_COLLECTION }); + + const { get_asset, list_assets } = actor; + + const asset = fromNullable(await get_asset(TEST_COLLECTION, full_path)); + + assertNonNullish(asset); + + await pic.advanceTime(futureMilliseconds + 1); + await tick(pic); + + await expect(upload({ full_path, name, collection: TEST_COLLECTION })).rejects.toThrowError( + JUNO_STORAGE_ERROR_UPLOAD_NOT_ALLOWED + ); + }); + + it('should return empty on list assets', async () => { + const futureMilliseconds = 10_000; + + await generateController(futureMilliseconds); + + const { list_assets } = actor; + + expect((await list_assets(TEST_COLLECTION, mockListParams)).items_length).toBeGreaterThan(0n); + + await pic.advanceTime(futureMilliseconds + 1); + await tick(pic); + + expect((await list_assets(TEST_COLLECTION, mockListParams)).items_length).toEqual(0n); + }); +}); From c24ce6ef3efbebdaa70e833cd14f40b640f6e86c Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Thu, 5 Feb 2026 16:38:54 +0100 Subject: [PATCH 21/23] test: write and submit --- ...tellite.controllers.storage.submit.spec.ts | 186 ++++++++++++++++++ ...atellite.controllers.storage.write.spec.ts | 2 +- 2 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 src/tests/specs/satellite/stock/controllers/satellite.controllers.storage.submit.spec.ts diff --git a/src/tests/specs/satellite/stock/controllers/satellite.controllers.storage.submit.spec.ts b/src/tests/specs/satellite/stock/controllers/satellite.controllers.storage.submit.spec.ts new file mode 100644 index 0000000000..26941f326b --- /dev/null +++ b/src/tests/specs/satellite/stock/controllers/satellite.controllers.storage.submit.spec.ts @@ -0,0 +1,186 @@ +import { idlFactorySatellite, type SatelliteActor, type SatelliteDid } from '$declarations'; +import { toBigIntNanoSeconds } from '$lib/utils/date.utils'; +import { type Actor, PocketIc } from '@dfinity/pic'; +import { assertNonNullish, fromNullable, toNullable } from '@dfinity/utils'; +import { Ed25519KeyIdentity } from '@icp-sdk/core/identity'; +import { + JUNO_STORAGE_ERROR_ASSET_NOT_FOUND, + JUNO_STORAGE_ERROR_UPLOAD_NOT_ALLOWED +} from '@junobuild/errors'; +import { inject } from 'vitest'; +import { CONTROLLER_METADATA } from '../../../../constants/controller-tests.constants'; +import { mockListParams } from '../../../../mocks/list.mocks'; +import { tick } from '../../../../utils/pic-tests.utils'; +import { uploadAsset } from '../../../../utils/satellite-storage-tests.utils'; +import { controllersInitArgs, SATELLITE_WASM_PATH } from '../../../../utils/setup-tests.utils'; + +describe.each([ + { title: 'heap', memory: { Heap: null } }, + { title: 'stable', memory: { Stable: null } } +])('Satellite > Controllers > Datastore $title > Submit', ({ memory }) => { + let pic: PocketIc; + let actor: Actor; + + const controller = Ed25519KeyIdentity.generate(); + + const TEST_COLLECTION = 'test'; + + const currentDate = new Date(2021, 6, 10, 0, 0, 0, 0); + + const upload = async (params: { + full_path: string; + name: string; + collection: string; + headers?: [string, string][]; + encoding_type?: [] | [string]; + }) => { + await uploadAsset({ + ...params, + actor + }); + }; + + let testWriteController: Ed25519KeyIdentity; + let testSubmitController: Ed25519KeyIdentity; + + beforeAll(async () => { + pic = await PocketIc.create(inject('PIC_URL')); + + await pic.setTime(currentDate.getTime()); + + const { actor: c } = await pic.setupCanister({ + idlFactory: idlFactorySatellite, + wasm: SATELLITE_WASM_PATH, + arg: controllersInitArgs(controller), + sender: controller.getPrincipal() + }); + + actor = c; + + actor.setIdentity(controller); + + const setRule: SatelliteDid.SetRule = { + memory: toNullable(memory), + max_size: toNullable(), + read: { Managed: null }, + mutable_permissions: toNullable(), + write: { Managed: null }, + version: toNullable(), + max_capacity: toNullable(), + rate_config: toNullable(), + max_changes_per_user: toNullable() + }; + + const { set_rule } = actor; + await set_rule({ Storage: null }, TEST_COLLECTION, setRule); + }); + + afterAll(async () => { + await pic?.tearDown(); + }); + + beforeEach(async () => { + actor.setIdentity(controller); + + testWriteController = Ed25519KeyIdentity.generate(); + testSubmitController = Ed25519KeyIdentity.generate(); + + const { set_controllers } = actor; + + await set_controllers({ + controller: { + ...CONTROLLER_METADATA, + scope: { Write: null } + }, + controllers: [testWriteController.getPrincipal()] + }); + + await set_controllers({ + controller: { + ...CONTROLLER_METADATA, + scope: { Submit: null } + }, + controllers: [testSubmitController.getPrincipal()] + }); + + actor.setIdentity(testSubmitController); + }); + + it('should throw on upload asset', async () => { + await pic.advanceTime(100); + + const name = 'hello.html'; + const full_path = `/${TEST_COLLECTION}/${name}`; + + await expect(upload({ full_path, name, collection: TEST_COLLECTION })).rejects.toThrowError( + JUNO_STORAGE_ERROR_UPLOAD_NOT_ALLOWED + ); + }); + + it('should return empty on get asset', async () => { + actor.setIdentity(testWriteController); + + const name = 'hello.html'; + const full_path = `/${TEST_COLLECTION}/${name}`; + + await upload({ full_path, name, collection: TEST_COLLECTION }); + + const { get_asset } = actor; + + expect(fromNullable(await get_asset(TEST_COLLECTION, full_path))).not.toBeUndefined(); + + actor.setIdentity(testSubmitController); + + expect(await get_asset(TEST_COLLECTION, full_path)).toEqual([]); + }); + + it('should throw on delete asset', async () => { + actor.setIdentity(testWriteController); + + const name = 'hello1.html'; + const full_path = `/${TEST_COLLECTION}/${name}`; + + await upload({ full_path, name, collection: TEST_COLLECTION }); + + actor.setIdentity(testSubmitController); + + const { del_asset } = actor; + + await expect(del_asset(TEST_COLLECTION, full_path)).rejects.toThrowError( + JUNO_STORAGE_ERROR_ASSET_NOT_FOUND + ); + }); + + it('should throw on update asset', async () => { + actor.setIdentity(testWriteController); + + const name = 'hello2.html'; + const full_path = `/${TEST_COLLECTION}/${name}`; + + await upload({ full_path, name, collection: TEST_COLLECTION }); + + const { get_asset } = actor; + + const asset = fromNullable(await get_asset(TEST_COLLECTION, full_path)); + + assertNonNullish(asset); + + actor.setIdentity(testSubmitController); + + await expect(upload({ full_path, name, collection: TEST_COLLECTION })).rejects.toThrowError( + JUNO_STORAGE_ERROR_UPLOAD_NOT_ALLOWED + ); + }); + + it('should return empty on list assets', async () => { + actor.setIdentity(testWriteController); + + const { list_assets } = actor; + + expect((await list_assets(TEST_COLLECTION, mockListParams)).items_length).toBeGreaterThan(0n); + + actor.setIdentity(testSubmitController); + + expect((await list_assets(TEST_COLLECTION, mockListParams)).items_length).toEqual(0n); + }); +}); diff --git a/src/tests/specs/satellite/stock/controllers/satellite.controllers.storage.write.spec.ts b/src/tests/specs/satellite/stock/controllers/satellite.controllers.storage.write.spec.ts index 79239d2097..a9d4d98fab 100644 --- a/src/tests/specs/satellite/stock/controllers/satellite.controllers.storage.write.spec.ts +++ b/src/tests/specs/satellite/stock/controllers/satellite.controllers.storage.write.spec.ts @@ -160,7 +160,7 @@ describe.each([ await upload({ full_path, name, collection: TEST_COLLECTION }); - const { get_asset, list_assets } = actor; + const { get_asset } = actor; const asset = fromNullable(await get_asset(TEST_COLLECTION, full_path)); From 99d311275f5219144faebd0d5e4c2ff13aa5ac68 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Thu, 5 Feb 2026 16:40:30 +0100 Subject: [PATCH 22/23] chore: fmt --- .../controllers/satellite.controllers.datastore.write.spec.ts | 2 +- .../controllers/satellite.controllers.storage.submit.spec.ts | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.write.spec.ts b/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.write.spec.ts index fe0ec8b258..632461880d 100644 --- a/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.write.spec.ts +++ b/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.write.spec.ts @@ -76,7 +76,7 @@ describe.each([ await set_controllers({ controller: { ...CONTROLLER_METADATA, - scope: {Write: null}, + scope: { Write: null }, expires_at: [ toBigIntNanoSeconds(new Date((await pic.getTime()) + (futureMilliseconds ?? 0))) ] diff --git a/src/tests/specs/satellite/stock/controllers/satellite.controllers.storage.submit.spec.ts b/src/tests/specs/satellite/stock/controllers/satellite.controllers.storage.submit.spec.ts index 26941f326b..fc2647914b 100644 --- a/src/tests/specs/satellite/stock/controllers/satellite.controllers.storage.submit.spec.ts +++ b/src/tests/specs/satellite/stock/controllers/satellite.controllers.storage.submit.spec.ts @@ -1,5 +1,4 @@ import { idlFactorySatellite, type SatelliteActor, type SatelliteDid } from '$declarations'; -import { toBigIntNanoSeconds } from '$lib/utils/date.utils'; import { type Actor, PocketIc } from '@dfinity/pic'; import { assertNonNullish, fromNullable, toNullable } from '@dfinity/utils'; import { Ed25519KeyIdentity } from '@icp-sdk/core/identity'; @@ -10,7 +9,6 @@ import { import { inject } from 'vitest'; import { CONTROLLER_METADATA } from '../../../../constants/controller-tests.constants'; import { mockListParams } from '../../../../mocks/list.mocks'; -import { tick } from '../../../../utils/pic-tests.utils'; import { uploadAsset } from '../../../../utils/satellite-storage-tests.utils'; import { controllersInitArgs, SATELLITE_WASM_PATH } from '../../../../utils/setup-tests.utils'; From 7296309e2ebb5199e4dee1056188fc528e277a5e Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Thu, 5 Feb 2026 16:51:37 +0100 Subject: [PATCH 23/23] chore: fmt --- .../controllers/satellite.controllers.datastore.submit.spec.ts | 2 +- .../controllers/satellite.controllers.datastore.write.spec.ts | 2 +- .../controllers/satellite.controllers.storage.submit.spec.ts | 2 +- .../controllers/satellite.controllers.storage.write.spec.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.submit.spec.ts b/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.submit.spec.ts index 17d9b47c37..4d9bc847b3 100644 --- a/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.submit.spec.ts +++ b/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.submit.spec.ts @@ -111,7 +111,7 @@ describe.each([ actor.setIdentity(testSubmitController); - expect(await get_doc(TEST_COLLECTION, key)).toEqual([]); + await expect(get_doc(TEST_COLLECTION, key)).resolves.toEqual([]); }); it('should throw on delete document', async () => { diff --git a/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.write.spec.ts b/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.write.spec.ts index 632461880d..553b2d8745 100644 --- a/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.write.spec.ts +++ b/src/tests/specs/satellite/stock/controllers/satellite.controllers.datastore.write.spec.ts @@ -109,7 +109,7 @@ describe.each([ await pic.advanceTime(futureMilliseconds + 1); await tick(pic); - expect(await get_doc(TEST_COLLECTION, key)).toEqual([]); + await expect(get_doc(TEST_COLLECTION, key)).resolves.toEqual([]); }); it('should throw on delete document', async () => { diff --git a/src/tests/specs/satellite/stock/controllers/satellite.controllers.storage.submit.spec.ts b/src/tests/specs/satellite/stock/controllers/satellite.controllers.storage.submit.spec.ts index fc2647914b..adcec8f07d 100644 --- a/src/tests/specs/satellite/stock/controllers/satellite.controllers.storage.submit.spec.ts +++ b/src/tests/specs/satellite/stock/controllers/satellite.controllers.storage.submit.spec.ts @@ -129,7 +129,7 @@ describe.each([ actor.setIdentity(testSubmitController); - expect(await get_asset(TEST_COLLECTION, full_path)).toEqual([]); + await expect(get_asset(TEST_COLLECTION, full_path)).resolves.toEqual([]); }); it('should throw on delete asset', async () => { diff --git a/src/tests/specs/satellite/stock/controllers/satellite.controllers.storage.write.spec.ts b/src/tests/specs/satellite/stock/controllers/satellite.controllers.storage.write.spec.ts index a9d4d98fab..2dae04971a 100644 --- a/src/tests/specs/satellite/stock/controllers/satellite.controllers.storage.write.spec.ts +++ b/src/tests/specs/satellite/stock/controllers/satellite.controllers.storage.write.spec.ts @@ -127,7 +127,7 @@ describe.each([ await pic.advanceTime(futureMilliseconds + 1); await tick(pic); - expect(await get_asset(TEST_COLLECTION, full_path)).toEqual([]); + await expect(get_asset(TEST_COLLECTION, full_path)).resolves.toEqual([]); }); it('should throw on delete asset', async () => {