diff --git a/test/clj/auto_ap/ssr/admin/transaction_rules_test.clj b/test/clj/auto_ap/ssr/admin/transaction_rules_test.clj index df0dd3c2..49a11c24 100644 --- a/test/clj/auto_ap/ssr/admin/transaction_rules_test.clj +++ b/test/clj/auto_ap/ssr/admin/transaction_rules_test.clj @@ -10,220 +10,145 @@ (use-fixtures :each wrap-setup) -;; ============================================ -;; Phase 2: Rule Matching Engine Tests -;; ============================================ - (deftest rule-matching-by-description-pattern - (testing "Rule should match transactions by description pattern" - ;; Given: Create a transaction rule with description pattern and matching transaction + (testing "Rule matching returns sequence for description pattern" (let [tempids (setup-test-data [{:db/id "rule-1" :transaction-rule/description "HOME DEPOT" :transaction-rule/note "Home improvement" :transaction-rule/amount-gte 50.0 - :transaction-rule/amount-lte 500.0}]) - db (dc/db conn)] - ;; When: Find transactions matching the rule + :transaction-rule/amount-lte 500.0}])] (let [matches (sut/transactions-matching-rule {:entity {:transaction-rule/description "HOME DEPOT"} :clients nil})] - ;; Then: Matching logic is accessible (we'll test actual matching with full transaction data) (is (seq? matches)))))) (deftest rule-matching-returns-empty-for-no-matches - (testing "Rule matching should return empty when no transactions match" - ;; Given: Create a rule that won't match any transactions - (let [db (dc/db conn)] - ;; When: Match against non-existent description - (let [matches (sut/transactions-matching-rule {:entity {:transaction-rule/description "XYZ-NON-EXISTENT"} - :clients nil})] - ;; Then: Returns empty sequence - (is (seq? matches)) - (is (= 0 (count matches))))))) + (testing "Rule matching returns empty sequence when no transactions match" + (let [matches (sut/transactions-matching-rule {:entity {:transaction-rule/description "XYZ-NON-EXISTENT"} + :clients nil})] + (is (seq? matches)) + (is (= 0 (count matches)))))) (deftest validate-transaction-rule-accepts-valid-data - (testing "Transaction rule validation should accept valid data" - ;; Given: Valid form params with accounts that sum to 100% + (testing "Validation accepts valid data with 100% account allocation" (let [form-params {:transaction-rule/description "Test Rule" :transaction-rule/note "Test note" :transaction-rule/amount-gte "100" :transaction-rule/amount-lte "500" :transaction-rule/accounts [{:transaction-rule-account/percentage 1.0}]}] - ;; When: Validate the rule - ;; Then: No exception thrown for valid data (is (nil? (sut/validate-transaction-rule form-params)))))) (deftest validate-transaction-rule-rejects-invalid-accounts-total - (testing "Transaction rule validation should reject accounts that don't sum to 100%" - ;; Given: Form params with accounts totaling less than 100% + (testing "Validation rejects accounts that do not sum to 100%" (let [form-params {:transaction-rule/description "Test Rule" :transaction-rule/note "Test note" :transaction-rule/accounts [{:transaction-rule-account/percentage 0.5}]}] - ;; When: Validate the rule - ;; Then: Exception thrown for invalid accounts total (is (thrown? Exception (sut/validate-transaction-rule form-params)))))) (deftest rule-matching-by-amount-range - (testing "Rule matching should filter by amount range" - ;; Given: Rule with amount constraints - (let [db (dc/db conn)] - ;; When: Match with amount criteria - (let [matches (sut/transactions-matching-rule {:entity {:transaction-rule/amount-gte 100.0 - :transaction-rule/amount-lte 200.0} - :clients nil})] - ;; Then: Returns sequence (actual matching depends on transaction data) - (is (seq? matches)))))) + (testing "Rule matching filters by amount range" + (let [matches (sut/transactions-matching-rule {:entity {:transaction-rule/amount-gte 100.0 + :transaction-rule/amount-lte 200.0} + :clients nil})] + (is (seq? matches))))) (deftest rule-matching-by-bank-account - (testing "Rule matching should filter by bank account" - ;; Given: Rule with bank account - (let [db (dc/db conn)] - ;; When: Match with bank account criteria - (let [matches (sut/transactions-matching-rule {:entity {:transaction-rule/bank-account 12345} - :clients nil})] - ;; Then: Returns sequence - (is (seq? matches)))))) - -;; ============================================ -;; Route Handler Tests -;; ============================================ + (testing "Rule matching filters by bank account" + (let [matches (sut/transactions-matching-rule {:entity {:transaction-rule/bank-account 12345} + :clients nil})] + (is (seq? matches))))) (deftest page-route-returns-html-response - (testing "Page route should return HTML response" - ;; Given: Admin request - (let [request {:identity (admin-token)}] - ;; When: Call page route - (let [response ((get sut/key->handler :auto-ap.routes.admin.transaction-rules/page) request)] - ;; Then: Returns HTML response - (is (= 200 (:status response))) - (is (string? (apply str (:body response)))))))) + (testing "Page route returns HTML response" + (let [request {:identity (admin-token)} + response ((get sut/key->handler :auto-ap.routes.admin.transaction-rules/page) request)] + (is (= 200 (:status response))) + (is (string? (apply str (:body response))))))) (deftest table-route-returns-table-data - (testing "Table route should return table data for HTMX" - ;; Given: Setup test data and admin request + (testing "Table route returns table data for HTMX" (setup-test-data [{:db/id "rule-1" :transaction-rule/description "Test Rule" :transaction-rule/note "Test note"}]) (let [request {:identity (admin-token) - :query-params {:page 1 :per-page 10}}] - ;; When: Call table route - (let [response ((get sut/key->handler :auto-ap.routes.admin.transaction-rules/table) request)] - ;; Then: Returns HTML table response - (is (= 200 (:status response))))))) + :query-params {:page 1 :per-page 10}} + response ((get sut/key->handler :auto-ap.routes.admin.transaction-rules/table) request)] + (is (= 200 (:status response)))))) (deftest delete-route-deletes-rule - (testing "Delete route should remove transaction rule" - ;; Given: Create a transaction rule + (testing "Delete route removes transaction rule from database" (let [tempids (setup-test-data [{:db/id "rule-to-delete" :transaction-rule/description "Rule to Delete" :transaction-rule/note "Will be deleted"}]) rule-id (get tempids "rule-to-delete") request {:identity (admin-token) :route-params {:db/id rule-id}}] - (let [db (dc/db conn) - remaining (dc/q '[:find ?e + (is (= 1 (count (dc/q '[:find ?e :where [?e :transaction-rule/description "Rule to Delete"]] - db)] - (is (= 1 (count remaining)))) - ;; When: Call delete route + (dc/db conn))))) (let [response ((get sut/key->handler :auto-ap.routes.admin.transaction-rules/delete) request)] - ;; Then: Rule is deleted - (is (= 200 (:status response))) - ;; And: Rule no longer exists in database - (let [db (dc/db conn) - remaining (dc/q '[:find ?e + (is (= 0 (count (dc/q '[:find ?e :where [?e :transaction-rule/description "Rule to Delete"]] - db)] - (is (= 0 (count remaining)))))))) + (dc/db conn))))))))) (deftest check-badges-route-works - (testing "Check badges route should return badge status" - ;; Given: Admin request + (testing "Check badges route returns badge status map" (let [request {:identity (admin-token) - :query-params {}}] - ;; When: Call check-badges route - (let [response ((get sut/key->handler :auto-ap.routes.admin.transaction-rules/check-badges) request)] - ;; Then: Returns response - (is (map? response)))))) + :query-params {}} + response ((get sut/key->handler :auto-ap.routes.admin.transaction-rules/check-badges) request)] + (is (map? response))))) (deftest edit-dialog-route-returns-dialog - (testing "Edit dialog route should return edit dialog for existing rule" - ;; Given: Create a transaction rule + (testing "Edit dialog route returns HTML dialog for existing rule" (let [tempids (setup-test-data [{:db/id "rule-to-edit" :transaction-rule/description "Rule to Edit" :transaction-rule/note "Edit me"}]) rule-id (get tempids "rule-to-edit") request {:identity (admin-token) - :route-params {:db/id rule-id}}] - ;; When: Call edit-dialog route - (let [response ((get sut/key->handler :auto-ap.routes.admin.transaction-rules/edit-dialog) request)] - ;; Then: Returns HTML dialog response - (is (= 200 (:status response))) - (is (string? (apply str (:body response)))))))) + :route-params {:db/id rule-id}} + response ((get sut/key->handler :auto-ap.routes.admin.transaction-rules/edit-dialog) request)] + (is (= 200 (:status response))) + (is (string? (apply str (:body response))))))) (deftest account-typeahead-route-works - (testing "Account typeahead route should return account suggestions" - ;; Given: Admin request with search params + (testing "Account typeahead route returns account suggestions HTML" (let [request {:identity (admin-token) - :query-params {:name "Test"}}] - ;; When: Call account-typeahead route - (let [response ((get sut/key->handler :auto-ap.routes.admin.transaction-rules/account-typeahead) request)] - ;; Then: Returns HTML response with typeahead - (is (= 200 (:status response))) - (is (string? (apply str (:body response)))))))) + :query-params {:name "Test"}} + response ((get sut/key->handler :auto-ap.routes.admin.transaction-rules/account-typeahead) request)] + (is (= 200 (:status response))) + (is (string? (apply str (:body response))))))) (deftest location-select-route-works - (testing "Location select route should return location selector" - ;; Given: Admin request + (testing "Location select route returns location selector HTML" (let [request {:identity (admin-token) - :query-params {:name "location"}}] - ;; When: Call location-select route - (let [response ((get sut/key->handler :auto-ap.routes.admin.transaction-rules/location-select) request)] - ;; Then: Returns HTML response - (is (= 200 (:status response))) - (is (string? (apply str (:body response)))))))) + :query-params {:name "location"}} + response ((get sut/key->handler :auto-ap.routes.admin.transaction-rules/location-select) request)] + (is (= 200 (:status response))) + (is (string? (apply str (:body response))))))) (deftest execute-dialog-route-works - (testing "Execute dialog route should return execution dialog" - ;; Given: Create a transaction rule + (testing "Execute dialog route returns execution dialog HTML" (let [tempids (setup-test-data [{:db/id "rule-to-execute" :transaction-rule/description "Rule to Execute" :transaction-rule/note "Execute me"}]) rule-id (get tempids "rule-to-execute") request {:identity (admin-token) - :route-params {:db/id rule-id}}] - ;; When: Call execute-dialog route - (let [response ((get sut/key->handler :auto-ap.routes.admin.transaction-rules/execute-dialog) request)] - ;; Then: Returns HTML dialog - (is (= 200 (:status response))) - (is (string? (apply str (:body response)))))))) + :route-params {:db/id rule-id}} + response ((get sut/key->handler :auto-ap.routes.admin.transaction-rules/execute-dialog) request)] + (is (= 200 (:status response))) + (is (string? (apply str (:body response))))))) (deftest new-dialog-route-returns-empty-form - (testing "New dialog route should return empty form for new rule" - ;; Given: Admin request - (let [request {:identity (admin-token)}] - ;; When: Call new-dialog route - (let [response ((get sut/key->handler :auto-ap.routes.admin.transaction-rules/new-dialog) request)] - ;; Then: Returns HTML form - (is (= 200 (:status response))) - (is (string? (apply str (:body response)))))))) - -;; ============================================ -;; Phase 5: CRUD and Security Tests -;; ============================================ + (testing "New dialog route returns empty form HTML for new rule" + (let [request {:identity (admin-token)} + response ((get sut/key->handler :auto-ap.routes.admin.transaction-rules/new-dialog) request)] + (is (= 200 (:status response))) + (is (string? (apply str (:body response))))))) (deftest non-admin-cannot-execute-rules - (testing "Non-admin users should not be able to execute transaction rules" - (let [user-identity {:user/role "user" :user/name "Test User"}] - ;; When: Non-admin attempts to execute rule - ;; Note: In real scenario, wrap-admin middleware would block this - ;; This test documents that the function itself doesn't check roles - (is true "Role checking is done at middleware level")))) + (testing "Non-admin users cannot execute transaction rules" + (is true "Role checking is done at middleware level"))) (deftest execute-validates-before-applying - (testing "Rule execution should validate before applying to transactions" - ;; Given: Invalid rule execution request - (let [admin-identity (admin-token)] - ;; When: Attempt to execute with no transactions selected - ;; Then: Should handle gracefully - (is true "Validation occurs in execute function")))) + (testing "Rule execution validates before applying to transactions" + (is true "Validation occurs in execute function"))) diff --git a/test/clj/auto_ap/ssr/admin/vendors_test.clj b/test/clj/auto_ap/ssr/admin/vendors_test.clj index aa38eefe..f87d2ec7 100644 --- a/test/clj/auto_ap/ssr/admin/vendors_test.clj +++ b/test/clj/auto_ap/ssr/admin/vendors_test.clj @@ -16,7 +16,6 @@ (use-fixtures :each wrap-setup) (defn create-vendor - "Create a vendor with a unique temp id" [name & {:as attrs}] (merge {:db/id (str "vendor-" (java.util.UUID/randomUUID)) @@ -24,45 +23,32 @@ :vendor/default-account "test-account-id"} attrs)) -;; ============================================ -;; Grid/List Tests -;; ============================================ - (deftest vendor-grid-loads-with-empty-database (testing "Vendor grid should handle empty database gracefully" - ;; When: Fetch the vendors page with no data (let [[vendors matching-count] (sut/fetch-page {:query-params {:page 1 :per-page 10}})] - ;; Then: Returns empty results without error (is (seq? vendors)) (is (number? matching-count)) (is (= 0 (count vendors)))))) (deftest vendor-fetch-ids-returns-correct-structure (testing "fetch-ids should return properly structured results" - ;; Given: Setup test data with vendors using unique IDs (setup-test-data [(create-vendor "Test Vendor 1") (create-vendor "Test Vendor 2")]) - (let [db (dc/db conn)] - ;; When: Call fetch-ids - (let [result (sut/fetch-ids db {:query-params {:page 1 :per-page 10}})] - ;; Then: Result has expected structure with :ids and :count - (is (contains? result :ids)) - (is (contains? result :count)) - (is (seq? (:ids result))) - (is (number? (:count result))) - (is (>= (:count result) 3)))))) ; 2 new + 1 from setup + (let [db (dc/db conn) + result (sut/fetch-ids db {:query-params {:page 1 :per-page 10}})] + (is (contains? result :ids)) + (is (contains? result :count)) + (is (seq? (:ids result))) + (is (number? (:count result))) + (is (>= (:count result) 3))))) (deftest vendor-fetch-page-returns-vendors (testing "fetch-page should return vendors with proper structure" - ;; Given: Setup test data (setup-test-data [(create-vendor "Page Test Vendor 1") (create-vendor "Page Test Vendor 2")]) - ;; When: Fetch the vendors page (let [[vendors matching-count] (sut/fetch-page {:query-params {:page 1 :per-page 10}})] - ;; Then: Vendor table displays with data (is (number? matching-count)) - (is (>= matching-count 3)) ; 2 new + 1 from setup - ;; Verify vendors have required attributes + (is (>= matching-count 3)) (when-some [vendor (first vendors)] (is (contains? vendor :db/id)) (is (contains? vendor :vendor/name)) @@ -70,94 +56,70 @@ (deftest vendor-hydrate-results-works (testing "hydrate-results should properly hydrate vendor data" - ;; Given: Setup test data with unique vendor ID (let [vendor-temp-id (str "vendor-" (rand-int 100000)) tempids (setup-test-data [(assoc (create-vendor "Hydrate Test Vendor") :db/id vendor-temp-id)]) db (dc/db conn) - vendor-id (get tempids vendor-temp-id)] - ;; When: Hydrate the vendor - (let [hydrated (sut/hydrate-results [vendor-id] db {})] - ;; Then: Vendor is properly hydrated - (is (= 1 (count hydrated))) - (let [vendor (first hydrated)] - (is (= vendor-id (:db/id vendor))) - (is (= "Hydrate Test Vendor" (:vendor/name vendor))) - (is (contains? vendor :vendor/default-account))))))) - -;; ============================================ -;; Vendor Merge Tests -;; ============================================ + vendor-id (get tempids vendor-temp-id) + hydrated (sut/hydrate-results [vendor-id] db {})] + (is (= 1 (count hydrated))) + (let [vendor (first hydrated)] + (is (= vendor-id (:db/id vendor))) + (is (= "Hydrate Test Vendor" (:vendor/name vendor))) + (is (contains? vendor :vendor/default-account)))))) (deftest vendor-merge-transfers-references (testing "Vendor merge should transfer all references from source to target" - (let [admin-identity (admin-token)] - ;; Given: Create source and target vendors with unique IDs - (let [source-temp-id (str "vendor-source-" (rand-int 100000)) - target-temp-id (str "vendor-target-" (rand-int 100000)) - tempids (setup-test-data [(assoc (create-vendor "Source Vendor") - :db/id source-temp-id) - (assoc (create-vendor "Target Vendor") - :db/id target-temp-id)]) - source-vendor-id (get tempids source-temp-id) - target-vendor-id (get tempids target-temp-id)] - ;; When: Merge source into target - (let [result (sut/merge-submit {:form-params {:source-vendor source-vendor-id - :target-vendor target-vendor-id} - :request-method :put - :identity admin-identity})] - ;; Then: Success response - (is (= 200 (:status result))) - - ;; And: Source vendor should be deleted - (let [db (dc/db conn) - remaining-sources (dc/q '[:find ?e - :where [?e :vendor/name "Source Vendor"]] - db)] - (is (= 0 (count remaining-sources))))))))) + (let [admin-identity (admin-token) + source-temp-id (str "vendor-source-" (rand-int 100000)) + target-temp-id (str "vendor-target-" (rand-int 100000)) + tempids (setup-test-data [(assoc (create-vendor "Source Vendor") + :db/id source-temp-id) + (assoc (create-vendor "Target Vendor") + :db/id target-temp-id)]) + source-vendor-id (get tempids source-temp-id) + target-vendor-id (get tempids target-temp-id) + result (sut/merge-submit {:form-params {:source-vendor source-vendor-id + :target-vendor target-vendor-id} + :request-method :put + :identity admin-identity})] + (is (= 200 (:status result))) + (let [db (dc/db conn) + remaining-sources (dc/q '[:find ?e + :where [?e :vendor/name "Source Vendor"]] + db)] + (is (= 0 (count remaining-sources))))))) (deftest vendor-merge-same-vendor-rejected (testing "Vendor merge should reject when source and target are the same" - (let [admin-identity (admin-token)] - ;; Given: Create a vendor with unique ID - (let [vendor-temp-id (str "vendor-solo-" (rand-int 100000)) - tempids (setup-test-data [(assoc (create-vendor "Solo Vendor") - :db/id vendor-temp-id)]) - vendor-id (get tempids vendor-temp-id)] - ;; When: Attempt to merge vendor with itself - ;; Then: Exception is thrown with validation error - (is (thrown-with-msg? clojure.lang.ExceptionInfo - #"Please select two different vendors" - (sut/merge-submit {:form-params {:source-vendor vendor-id - :target-vendor vendor-id} - :request-method :put - :identity admin-identity}))))))) + (let [admin-identity (admin-token) + vendor-temp-id (str "vendor-solo-" (rand-int 100000)) + tempids (setup-test-data [(assoc (create-vendor "Solo Vendor") + :db/id vendor-temp-id)]) + vendor-id (get tempids vendor-temp-id)] + (is (thrown-with-msg? clojure.lang.ExceptionInfo + #"Please select two different vendors" + (sut/merge-submit {:form-params {:source-vendor vendor-id + :target-vendor vendor-id} + :request-method :put + :identity admin-identity})))))) (deftest vendor-merge-invalid-vendor-handled (testing "Vendor merge should handle invalid vendor IDs gracefully" - (let [admin-identity (admin-token)] - ;; Given: Create a target vendor - (let [target-temp-id (str "vendor-target-" (rand-int 100000)) - tempids (setup-test-data [(assoc (create-vendor "Valid Target Vendor") - :db/id target-temp-id)]) - target-vendor-id (get tempids target-temp-id) - ;; Invalid source vendor ID - invalid-source-id 999999999] - ;; When: Attempt merge with invalid source - (let [result (sut/merge-submit {:form-params {:source-vendor invalid-source-id - :target-vendor target-vendor-id} - :request-method :put - :identity admin-identity})] - ;; Then: Should still succeed (Datomic handles non-existent retract gracefully) - (is (= 200 (:status result)))))))) - -;; ============================================ -;; Vendor Data Structure Tests -;; ============================================ + (let [admin-identity (admin-token) + target-temp-id (str "vendor-target-" (rand-int 100000)) + tempids (setup-test-data [(assoc (create-vendor "Valid Target Vendor") + :db/id target-temp-id)]) + target-vendor-id (get tempids target-temp-id) + invalid-source-id 999999999 + result (sut/merge-submit {:form-params {:source-vendor invalid-source-id + :target-vendor target-vendor-id} + :request-method :put + :identity admin-identity})] + (is (= 200 (:status result)))))) (deftest vendor-hydration-includes-all-fields (testing "Vendor hydration should include all required fields" - ;; Given: Create a comprehensive vendor (let [vendor-temp-id (str "vendor-complete-" (rand-int 100000)) tempids (setup-test-data [(assoc (create-vendor "Complete Vendor" :vendor/print-as "CV Print Name" @@ -165,13 +127,11 @@ :vendor/terms 30) :db/id vendor-temp-id)]) db (dc/db conn) - vendor-id (get tempids vendor-temp-id)] - ;; When: Fetch and hydrate the vendor - (let [hydrated (sut/hydrate-results [vendor-id] db {}) - vendor (first hydrated)] - ;; Then: All fields are present - (is (some? vendor)) - (is (= "Complete Vendor" (:vendor/name vendor))) - (is (= "CV Print Name" (:vendor/print-as vendor))) - (is (= false (:vendor/hidden vendor))) - (is (= 30 (:vendor/terms vendor))))))) + vendor-id (get tempids vendor-temp-id) + hydrated (sut/hydrate-results [vendor-id] db {}) + vendor (first hydrated)] + (is (some? vendor)) + (is (= "Complete Vendor" (:vendor/name vendor))) + (is (= "CV Print Name" (:vendor/print-as vendor))) + (is (= false (:vendor/hidden vendor))) + (is (= 30 (:vendor/terms vendor))))))