From bbc0693512a7611183ca7546e77256729f65ab9b Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Mon, 10 Feb 2025 17:49:16 +0100 Subject: [PATCH 1/5] feat: user usage data assertion --- src/libs/satellite/src/db/assert.rs | 4 +++- src/libs/satellite/src/usage/assert.rs | 27 ++++++++++++++++++++++++++ src/libs/satellite/src/usage/impls.rs | 8 ++++---- src/libs/satellite/src/usage/store.rs | 6 +++--- src/libs/satellite/src/usage/types.rs | 18 +---------------- 5 files changed, 38 insertions(+), 25 deletions(-) diff --git a/src/libs/satellite/src/db/assert.rs b/src/libs/satellite/src/db/assert.rs index 9cf8fbbb4..51d7978e0 100644 --- a/src/libs/satellite/src/db/assert.rs +++ b/src/libs/satellite/src/db/assert.rs @@ -4,7 +4,7 @@ use crate::db::types::config::DbConfig; use crate::db::types::state::{DocAssertDelete, DocAssertSet, DocContext}; use crate::hooks::{invoke_assert_delete_doc, invoke_assert_set_doc}; use crate::types::store::StoreContext; -use crate::usage::assert::increment_and_assert_db_usage; +use crate::usage::assert::{assert_user_usage_collection_data, increment_and_assert_db_usage}; use crate::user::assert::{assert_user_collection_caller_key, assert_user_collection_data}; use crate::{DelDoc, Doc, SetDoc}; use candid::Principal; @@ -39,6 +39,8 @@ pub fn assert_set_doc( assert_user_collection_caller_key(caller, collection, key)?; assert_user_collection_data(collection, value)?; + assert_user_usage_collection_data(collection, value)?; + invoke_assert_set_doc( &caller, &DocContext { diff --git a/src/libs/satellite/src/usage/assert.rs b/src/libs/satellite/src/usage/assert.rs index d1f43bd96..f2909f927 100644 --- a/src/libs/satellite/src/usage/assert.rs +++ b/src/libs/satellite/src/usage/assert.rs @@ -1,9 +1,16 @@ use crate::types::state::CollectionType; use crate::usage::store::increment_usage; +use crate::usage::types::state::UserUsageData; use crate::usage::utils::{is_db_collection_no_usage, is_storage_collection_no_usage}; +use crate::SetDoc; +use junobuild_collections::constants::db::{COLLECTION_USER_KEY, COLLECTION_USER_USAGE_KEY}; use junobuild_collections::types::core::CollectionKey; use junobuild_shared::controllers::is_controller; use junobuild_shared::types::state::{Controllers, UserId}; +use junobuild_utils::decode_doc_data; +// --------------------------------------------------------- +// Increment user usage - i.e. when a user edit, create or delete +// --------------------------------------------------------- pub fn increment_and_assert_db_usage( caller: UserId, @@ -65,3 +72,23 @@ fn increment_and_assert_usage( Ok(()) } + +// --------------------------------------------------------- +// Assert struct - useful when an admit set imperatively a user usage +// --------------------------------------------------------- + +pub fn assert_user_usage_collection_data( + collection: &CollectionKey, + doc: &SetDoc, +) -> Result<(), String> { + let user_usage_collection = COLLECTION_USER_USAGE_KEY; + + if collection != user_usage_collection { + return Ok(()); + } + + decode_doc_data::(&doc.data) + .map_err(|err| format!("Invalid user usage data: {}", err))?; + + Ok(()) +} diff --git a/src/libs/satellite/src/usage/impls.rs b/src/libs/satellite/src/usage/impls.rs index 75f813992..1184d0b55 100644 --- a/src/libs/satellite/src/usage/impls.rs +++ b/src/libs/satellite/src/usage/impls.rs @@ -1,10 +1,10 @@ use crate::types::state::CollectionType; -use crate::usage::types::state::{UserUsage, UserUsageKey}; +use crate::usage::types::state::{UserUsageData, UserUsageKey}; use junobuild_collections::types::core::CollectionKey; use junobuild_shared::types::state::UserId; -impl UserUsage { - pub fn increment(current_user_usage: &Option) -> Self { +impl UserUsageData { + pub fn increment(current_user_usage: &Option) -> Self { let count = 1; let items_count: u32 = match current_user_usage { @@ -12,7 +12,7 @@ impl UserUsage { Some(current_user_usage) => current_user_usage.changes_count.saturating_add(count), }; - UserUsage { + UserUsageData { changes_count: items_count, } } diff --git a/src/libs/satellite/src/usage/store.rs b/src/libs/satellite/src/usage/store.rs index bef9f374a..4130611d6 100644 --- a/src/libs/satellite/src/usage/store.rs +++ b/src/libs/satellite/src/usage/store.rs @@ -1,5 +1,5 @@ use crate::types::state::CollectionType; -use crate::usage::types::state::{UserUsage, UserUsageKey}; +use crate::usage::types::state::{UserUsageData, UserUsageKey}; use crate::{get_doc_store, set_doc_store, SetDoc}; use ic_cdk::id; use junobuild_collections::constants::db::COLLECTION_USER_USAGE_KEY; @@ -11,7 +11,7 @@ pub fn increment_usage( collection_key: &CollectionKey, collection_type: &CollectionType, user_id: &UserId, -) -> Result { +) -> Result { let user_usage_key = UserUsageKey::create(user_id, collection_key, collection_type); let key = user_usage_key.to_key(); @@ -22,7 +22,7 @@ pub fn increment_usage( .map(|doc| decode_doc_data(&doc.data)) .transpose()?; - let update_usage = UserUsage::increment(¤t_usage); + let update_usage = UserUsageData::increment(¤t_usage); let update_doc = SetDoc { data: encode_doc_data(&update_usage)?, diff --git a/src/libs/satellite/src/usage/types.rs b/src/libs/satellite/src/usage/types.rs index 23d2ca3b9..60a19a8fe 100644 --- a/src/libs/satellite/src/usage/types.rs +++ b/src/libs/satellite/src/usage/types.rs @@ -27,23 +27,7 @@ pub mod state { /// - `updated_at`: The timestamp of the last update to this user usage entry. /// - `version`: An optional field representing the version of this usage entry. In the future we might implement checks to avoid overwrite but, this is not the case currently. #[derive(CandidType, Serialize, Deserialize, Clone)] - pub struct UserUsage { - pub changes_count: u32, - } -} - -pub mod interface { - use candid::CandidType; - use serde::{Deserialize, Serialize}; - - /// Represents the parameters for setting or updating a user's usage entry for a controller. - /// - /// This is useful if one want to set a value after the upgrade, given the lack of migration, or if a controller ever wants to reset the value to allow a user who would hit the limit to continue submitted changes. - /// - /// It includes: - /// - `changes_count`: The total number of changes the user has in a specific collection. - #[derive(Default, CandidType, Serialize, Deserialize, Clone)] - pub struct SetUserUsage { + pub struct UserUsageData { pub changes_count: u32, } } From 248716dfa67b909e54591823a6fed8e53d0af865 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 11 Feb 2025 06:59:25 +0100 Subject: [PATCH 2/5] feat: cleanup --- src/libs/satellite/src/usage/types.rs | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/src/libs/satellite/src/usage/types.rs b/src/libs/satellite/src/usage/types.rs index 60a19a8fe..2a7074cef 100644 --- a/src/libs/satellite/src/usage/types.rs +++ b/src/libs/satellite/src/usage/types.rs @@ -1,17 +1,12 @@ pub mod state { use crate::types::state::CollectionType; - use candid::CandidType; use junobuild_collections::types::core::CollectionKey; use junobuild_shared::types::state::UserId; use serde::{Deserialize, Serialize}; /// A unique key for identifying user usage within a collection. - /// - /// It consists of: - /// - `user_id`: The unique identifier for the user which is matched to the caller. - /// - `collection_key`: The collection where usage is tracked. - /// - `collection_type`: The type of collection (`Db` for datastore, `Storage` for assets). - #[derive(CandidType, Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] + /// The key will be parsed to `user-id#db|storage#collection`. + #[derive(Serialize, Deserialize)] pub struct UserUsageKey { pub user_id: UserId, pub collection_key: CollectionKey, @@ -19,14 +14,8 @@ pub mod state { } /// Tracks the usage (create, set and delete) of a user in a collection. - /// - /// - /// Fields: - /// - `changes_count`: The total number of changes (create/update/delete) by the user. - /// - `created_at`: The timestamp when this user usage entry was first recorded. - /// - `updated_at`: The timestamp of the last update to this user usage entry. - /// - `version`: An optional field representing the version of this usage entry. In the future we might implement checks to avoid overwrite but, this is not the case currently. - #[derive(CandidType, Serialize, Deserialize, Clone)] + #[derive(Serialize, Deserialize)] + #[serde(deny_unknown_fields)] pub struct UserUsageData { pub changes_count: u32, } From 588ae85e95b14abc29a9e1ece2d5a35ecaae6469 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 11 Feb 2025 06:59:43 +0100 Subject: [PATCH 3/5] chore: remove unused --- src/libs/satellite/src/usage/assert.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/satellite/src/usage/assert.rs b/src/libs/satellite/src/usage/assert.rs index f2909f927..b471bce81 100644 --- a/src/libs/satellite/src/usage/assert.rs +++ b/src/libs/satellite/src/usage/assert.rs @@ -3,11 +3,12 @@ use crate::usage::store::increment_usage; use crate::usage::types::state::UserUsageData; use crate::usage::utils::{is_db_collection_no_usage, is_storage_collection_no_usage}; use crate::SetDoc; -use junobuild_collections::constants::db::{COLLECTION_USER_KEY, COLLECTION_USER_USAGE_KEY}; +use junobuild_collections::constants::db::{COLLECTION_USER_USAGE_KEY}; use junobuild_collections::types::core::CollectionKey; use junobuild_shared::controllers::is_controller; use junobuild_shared::types::state::{Controllers, UserId}; use junobuild_utils::decode_doc_data; + // --------------------------------------------------------- // Increment user usage - i.e. when a user edit, create or delete // --------------------------------------------------------- From 17006966a69e72e9d3ab5b225e98d38a3ba58546 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 11 Feb 2025 07:05:48 +0100 Subject: [PATCH 4/5] chore: redo UserUsageData --- src/libs/satellite/src/usage/store.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/satellite/src/usage/store.rs b/src/libs/satellite/src/usage/store.rs index 04472c85e..28715d81e 100644 --- a/src/libs/satellite/src/usage/store.rs +++ b/src/libs/satellite/src/usage/store.rs @@ -1,7 +1,7 @@ use crate::db::internal::{unsafe_get_doc, unsafe_set_doc}; use crate::rules::store::get_rule_db; use crate::types::state::CollectionType; -use crate::usage::types::state::{UserUsage, UserUsageKey}; +use crate::usage::types::state::{UserUsageData, UserUsageKey}; use crate::SetDoc; use ic_cdk::id; use junobuild_collections::constants::db::COLLECTION_USER_USAGE_KEY; @@ -14,7 +14,7 @@ pub fn increment_usage( collection_key: &CollectionKey, collection_type: &CollectionType, user_id: &UserId, -) -> Result { +) -> Result { let user_usage_key = UserUsageKey::create(user_id, collection_key, collection_type).to_key(); let user_usage_collection = COLLECTION_USER_USAGE_KEY.to_string(); @@ -29,7 +29,7 @@ pub fn increment_usage( .map(|doc| decode_doc_data(&doc.data)) .transpose()?; - let update_usage = UserUsage::increment(¤t_usage); + let update_usage = UserUsageData::increment(¤t_usage); let update_doc = SetDoc { data: encode_doc_data(&update_usage)?, From 0303fa95276a3248a25215fd2de0140d28a321f1 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 11 Feb 2025 07:35:06 +0100 Subject: [PATCH 5/5] test: assertion struct --- src/tests/specs/satellite.user-usage.spec.ts | 170 ++++++++++++++----- 1 file changed, 130 insertions(+), 40 deletions(-) diff --git a/src/tests/specs/satellite.user-usage.spec.ts b/src/tests/specs/satellite.user-usage.spec.ts index 71e7f48a0..62e37cd65 100644 --- a/src/tests/specs/satellite.user-usage.spec.ts +++ b/src/tests/specs/satellite.user-usage.spec.ts @@ -351,34 +351,79 @@ describe('Satellite User Usage', () => { actor.setIdentity(controller); }); - it('should set usage for user', async () => { - const { set_doc, get_doc } = actor; + describe('success', () => { + it('should set usage for user', async () => { + const { set_doc, get_doc } = actor; - const key = `${user.getPrincipal().toText()}#db#${TEST_COLLECTION}`; + const key = `${user.getPrincipal().toText()}#db#${TEST_COLLECTION}`; - const currentDoc = await get_doc('#user-usage', key); + const currentDoc = await get_doc('#user-usage', key); - const doc: SetDoc = { - data: await toArray({ - changes_count: 345 - }), - description: toNullable(), - version: fromNullable(currentDoc)?.version ?? [] - }; + const doc: SetDoc = { + data: await toArray({ + changes_count: 345 + }), + description: toNullable(), + version: fromNullable(currentDoc)?.version ?? [] + }; + + const usage = await set_doc('#user-usage', key, doc); + + const usageData = await fromArray(usage.data); + + expect(usageData.changes_count).toEqual(345); - const usage = await set_doc('#user-usage', key, doc); + expect(usage.updated_at).not.toBeUndefined(); + expect(usage.updated_at).toBeGreaterThan(0n); + expect(usage.created_at).not.toBeUndefined(); + expect(usage.created_at).toBeGreaterThan(0n); + expect(usage.updated_at).toBeGreaterThan(usage.created_at); - const usageData = await fromArray(usage.data); + expect(usage.version).toEqual(toNullable(BigInt(countTotalChanges + 1))); + }); + }); + + describe('error', () => { + it('should not set usage with invalid type', async () => { + const { set_doc, get_doc } = actor; - expect(usageData.changes_count).toEqual(345); + const key = `${user.getPrincipal().toText()}#db#${TEST_COLLECTION}`; - expect(usage.updated_at).not.toBeUndefined(); - expect(usage.updated_at).toBeGreaterThan(0n); - expect(usage.created_at).not.toBeUndefined(); - expect(usage.created_at).toBeGreaterThan(0n); - expect(usage.updated_at).toBeGreaterThan(usage.created_at); + const currentDoc = await get_doc('#user-usage', key); + + const doc: SetDoc = { + data: await toArray({ + changes_count: 'invalid' + }), + description: toNullable(), + version: fromNullable(currentDoc)?.version ?? [] + }; + + await expect(set_doc('#user-usage', key, doc)).rejects.toThrow( + 'Invalid user usage data: invalid type: string "invalid", expected u32 at line 1 column 26.' + ); + }); - expect(usage.version).toEqual(toNullable(BigInt(countTotalChanges + 1))); + it('should not set usage with invalid additional data fields', async () => { + const { set_doc, get_doc } = actor; + + const key = `${user.getPrincipal().toText()}#db#${TEST_COLLECTION}`; + + const currentDoc = await get_doc('#user-usage', key); + + const doc: SetDoc = { + data: await toArray({ + changes_count: 432, + unknown: 'field' + }), + description: toNullable(), + version: fromNullable(currentDoc)?.version ?? [] + }; + + await expect(set_doc('#user-usage', key, doc)).rejects.toThrow( + 'Invalid user usage data: unknown field `unknown`, expected `changes_count` at line 1 column 30.' + ); + }); }); }); }); @@ -628,34 +673,79 @@ describe('Satellite User Usage', () => { actor.setIdentity(controller); }); - it('should set usage for user', async () => { - const { set_doc, get_doc } = actor; + describe('success', () => { + it('should set usage for user', async () => { + const { set_doc, get_doc } = actor; - const key = `${user.getPrincipal().toText()}#storage#${TEST_COLLECTION}`; + const key = `${user.getPrincipal().toText()}#storage#${TEST_COLLECTION}`; - const currentDoc = await get_doc('#user-usage', key); + const currentDoc = await get_doc('#user-usage', key); - const doc: SetDoc = { - data: await toArray({ - changes_count: 456 - }), - description: toNullable(), - version: fromNullable(currentDoc)?.version ?? [] - }; + const doc: SetDoc = { + data: await toArray({ + changes_count: 456 + }), + description: toNullable(), + version: fromNullable(currentDoc)?.version ?? [] + }; + + const usage = await set_doc('#user-usage', key, doc); + + const usageData = await fromArray(usage.data); - const usage = await set_doc('#user-usage', key, doc); + expect(usageData.changes_count).toEqual(456); - const usageData = await fromArray(usage.data); + expect(usage.updated_at).not.toBeUndefined(); + expect(usage.updated_at).toBeGreaterThan(0n); + expect(usage.created_at).not.toBeUndefined(); + expect(usage.created_at).toBeGreaterThan(0n); + expect(usage.updated_at).toBeGreaterThan(usage.created_at); - expect(usageData.changes_count).toEqual(456); + expect(usage.version).toEqual(toNullable(BigInt(countTotalChanges + 1))); + }); + }); - expect(usage.updated_at).not.toBeUndefined(); - expect(usage.updated_at).toBeGreaterThan(0n); - expect(usage.created_at).not.toBeUndefined(); - expect(usage.created_at).toBeGreaterThan(0n); - expect(usage.updated_at).toBeGreaterThan(usage.created_at); + describe('error', () => { + it('should not set usage with invalid type', async () => { + const { set_doc, get_doc } = actor; - expect(usage.version).toEqual(toNullable(BigInt(countTotalChanges + 1))); + const key = `${user.getPrincipal().toText()}#storage#${TEST_COLLECTION}`; + + const currentDoc = await get_doc('#user-usage', key); + + const doc: SetDoc = { + data: await toArray({ + changes_count: 'invalid' + }), + description: toNullable(), + version: fromNullable(currentDoc)?.version ?? [] + }; + + await expect(set_doc('#user-usage', key, doc)).rejects.toThrow( + 'Invalid user usage data: invalid type: string "invalid", expected u32 at line 1 column 26.' + ); + }); + + it('should not set usage with invalid additional data fields', async () => { + const { set_doc, get_doc } = actor; + + const key = `${user.getPrincipal().toText()}#storage#${TEST_COLLECTION}`; + + const currentDoc = await get_doc('#user-usage', key); + + const doc: SetDoc = { + data: await toArray({ + changes_count: 432, + unknown: 'field' + }), + description: toNullable(), + version: fromNullable(currentDoc)?.version ?? [] + }; + + await expect(set_doc('#user-usage', key, doc)).rejects.toThrow( + 'Invalid user usage data: unknown field `unknown`, expected `changes_count` at line 1 column 30.' + ); + }); }); }); });