diff --git a/backend/src/app/rpc/commands/profile.clj b/backend/src/app/rpc/commands/profile.clj index 0dc0629eac..a26fc9ea9e 100644 --- a/backend/src/app/rpc/commands/profile.clj +++ b/backend/src/app/rpc/commands/profile.clj @@ -45,6 +45,11 @@ [:email-comments [::sm/one-of #{:all :partial :none}]] [:email-invites [::sm/one-of #{:all :none}]]]) +(def schema:nudge + [:map {:title "Nudge"} + [:big {:optional true} ::sm/number] + [:small {:optional true} ::sm/number]]) + (def system-managed-props "Props keys managed by the system (not user-writable via RPC)." #{:subscription}) @@ -58,6 +63,8 @@ [:newsletter-news {:optional true} ::sm/boolean] [:onboarding-team-id {:optional true} ::sm/uuid] [:onboarding-viewed {:optional true} ::sm/boolean] + [:onboarding-questions {:optional true} [:map-of :keyword :string]] + [:onboarding-questions-answered {:optional true} ::sm/boolean] [:nitrate-onboarding-viewed {:optional true} ::sm/boolean] [:v2-info-shown {:optional true} ::sm/boolean] [:welcome-file-id {:optional true} [:maybe ::sm/boolean]] @@ -66,7 +73,8 @@ [:notifications {:optional true} schema:props-notifications] [:workspace-visited {:optional true} ::sm/boolean] [:custom-shortcuts {:optional true} - [:map-of {:gen/max 10} :keyword [:map-of :keyword :string]]]]) + [:map-of {:gen/max 10} :keyword [:map-of :keyword :string]]] + [:nudge {:optional true} schema:nudge]]) (def schema:profile [:map {:title "Profile"} diff --git a/backend/test/backend_tests/rpc_profile_test.clj b/backend/test/backend_tests/rpc_profile_test.clj index 5c5ef63c0d..ffbb55ca7c 100644 --- a/backend/test/backend_tests/rpc_profile_test.clj +++ b/backend/test/backend_tests/rpc_profile_test.clj @@ -1202,3 +1202,72 @@ (t/is (true? (get-in props [:props :onboarding-viewed]))) (t/is (false? (get-in props [:props :newsletter-updates]))) (t/is (= :wasm (get-in props [:props :renderer])))))) + + +(t/deftest update-profile-props-accepts-onboarding-questions + ;; The onboarding questions flow sends these props on the final "START" step + (let [profile (th/create-profile* 1) + data {::th/type :update-profile-props + ::rpc/profile-id (:id profile) + :props {:onboarding-questions-answered true + :onboarding-questions + {:expected-use "work" + :role "ux" + :start-with "prototyping"}}} + out (th/command! data)] + + ;; The call should succeed + (t/is (nil? (:error out))) + + ;; And all keys should be persisted + (let [saved (th/db-get :profile {:id (:id profile)}) + props (profile/decode-row saved)] + (t/is (true? (get-in props [:props :onboarding-questions-answered]))) + (t/is (= {:expected-use "work" + :role "ux" + :start-with "prototyping"} + (get-in props [:props :onboarding-questions])))))) + + +(t/deftest update-profile-props-rejects-invalid-onboarding-questions + ;; The schema is closed and :onboarding-questions only accepts string values + (let [profile (th/create-profile* 1) + data {::th/type :update-profile-props + ::rpc/profile-id (:id profile) + :props {:onboarding-questions {:expected-use 42}}} + out (th/command! data)] + + ;; The call must fail with validation error + (t/is (th/ex-info? (:error out))) + (t/is (th/ex-of-type? (:error out) :validation)) + (t/is (th/ex-of-code? (:error out) :params-validation)))) + + +(t/deftest update-profile-props-accepts-nudge + ;; Nudge settings are persisted per-profile via update-profile-props + (let [profile (th/create-profile* 1) + data {::th/type :update-profile-props + ::rpc/profile-id (:id profile) + :props {:nudge {:big 20 :small 0.5}}} + out (th/command! data)] + + ;; The call should succeed + (t/is (nil? (:error out))) + + ;; And the nudge values should be persisted + (let [saved (th/db-get :profile {:id (:id profile)}) + props (profile/decode-row saved)] + (t/is (= {:big 20 :small 0.5} (get-in props [:props :nudge])))))) + +(t/deftest update-profile-props-rejects-invalid-nudge + ;; The nudge map only accepts :big/:small numbers + (let [profile (th/create-profile* 1) + data {::th/type :update-profile-props + ::rpc/profile-id (:id profile) + :props {:nudge {:big "ten"}}} + out (th/command! data)] + + ;; The call must fail with validation error + (t/is (th/ex-info? (:error out))) + (t/is (th/ex-of-type? (:error out) :validation)) + (t/is (th/ex-of-code? (:error out) :params-validation))))