(ns auto-ap.integration.dashboard-behaviors-test (:require [auto-ap.datomic :as datomic] [auto-ap.graphql.utils :as gql-utils] [auto-ap.handler :as handler] [auto-ap.integration.util :refer [setup-test-data test-account test-vendor wrap-setup]] [auto-ap.routes.utils :as routes-utils] [auto-ap.ssr.company.reports.expense :as expense-reports] [auto-ap.ssr.dashboard :as ssr-dashboard] [clj-time.core :as time] [clojure.test :refer [deftest is testing use-fixtures]] [datomic.api :as dc])) (use-fixtures :each wrap-setup) ;; ============================================================================ ;; Permission Behaviors (11.1 - 11.4) ;; ============================================================================ (deftest test-admin-permission-gating (testing "Behavior 11.1: It should allow only admin users to access the dashboard page and card endpoints" (let [handler (routes-utils/wrap-admin (fn [_] {:status 200 :body "ok"}))] (is (= 200 (:status (handler {:identity {:user/role "admin"}})))))) (testing "Behavior 11.2: It should redirect non-admin authenticated users to /login with a 302 status" (let [handler (routes-utils/wrap-admin (fn [_] {:status 200 :body "ok"}))] (let [response (handler {:identity {:user/role "user"}})] (is (= 302 (:status response))) (is (re-find #"^/login" (get-in response [:headers "Location"])))))) (testing "Behavior 11.3: It should redirect unauthenticated users to /login with a redirect-to parameter" (let [handler (routes-utils/wrap-admin (fn [_] {:status 200 :body "ok"}))] (let [response (handler {:identity nil :uri "/dashboard"})] (is (= 302 (:status response))) (is (re-find #"redirect-to" (get-in response [:headers "Location"])))))) (testing "Behavior 11.4: It should verify admin role via middleware before executing any data queries" (let [called (atom false)] (let [handler (routes-utils/wrap-admin (fn [_] (reset! called true) {:status 200}))] (handler {:identity {:user/role "user"}}) (is (not @called)))))) ;; ============================================================================ ;; Bank Accounts Card (2.2) ;; ============================================================================ (deftest test-bank-accounts-excludes-cash (testing "Behavior 2.2: It should exclude bank accounts with cash type from the display" (let [{:strs [test-client-id test-bank-account-id cash-account-id]} (setup-test-data [{:db/id "cash-account-id" :bank-account/name "Cash Account" :bank-account/type :bank-account-type/cash :bank-account/code "CASH-001"}])] @(dc/transact datomic/conn [{:db/id test-client-id :client/bank-accounts [{:db/id cash-account-id}]} {:db/id test-bank-account-id :bank-account/name "Check Account"}]) (let [request {:valid-trimmed-client-ids #{test-client-id}} response (ssr-dashboard/bank-accounts-card request)] (is (= 200 (:status response))) (is (nil? (re-find #"Cash Account" (:body response)))) (is (some? (re-find #"Check Account" (:body response)))))))) ;; ============================================================================ ;; Sales Chart Card (3.3) ;; ============================================================================ (deftest test-sales-chart-card-returns-data (testing "Behavior 3.3: It should query and sum sales order totals by date for the selected clients" (let [{:strs [test-client-id]} (setup-test-data [])] (let [request {:valid-trimmed-client-ids #{test-client-id}} response (ssr-dashboard/sales-chart-card request)] (is (= 200 (:status response))) (is (re-find #"Gross sales" (:body response))))))) ;; ============================================================================ ;; Expense Pie Card (4.3) ;; ============================================================================ (deftest test-expense-pie-sums-by-account (testing "Behavior 4.3: It should sum expense amounts by account name for the selected clients" (let [{:strs [test-client-id test-vendor-id test-account-id]} (setup-test-data [])] @(dc/transact datomic/conn [{:db/id "exp-inv-1" :invoice/client test-client-id :invoice/vendor test-vendor-id :invoice/invoice-number "EXP-001" :invoice/date (java.util.Date.) :invoice/total 150.0 :invoice/outstanding-balance 150.0 :invoice/status :invoice-status/unpaid :invoice/import-status :import-status/imported :invoice/expense-accounts [{:invoice-expense-account/account test-account-id :invoice-expense-account/amount 150.0 :invoice-expense-account/location "DT"}]} {:db/id "exp-inv-2" :invoice/client test-client-id :invoice/vendor test-vendor-id :invoice/invoice-number "EXP-002" :invoice/date (java.util.Date.) :invoice/total 100.0 :invoice/outstanding-balance 100.0 :invoice/status :invoice-status/unpaid :invoice/import-status :import-status/imported :invoice/expense-accounts [{:invoice-expense-account/account test-account-id :invoice-expense-account/amount 100.0 :invoice-expense-account/location "DT"}]}]) (let [request {:valid-trimmed-client-ids #{test-client-id}} response (ssr-dashboard/expense-pie-card request)] (is (= 200 (:status response))) (is (re-find #"Account" (:body response))) (is (re-find #"\b250\b" (:body response))))))) ;; ============================================================================ ;; P&L Card (5.3) ;; ============================================================================ (deftest test-pnl-card-calls-graphql (testing "Behavior 5.3: It should query P&L data via GraphQL for the selected clients and last month" (let [{:strs [test-client-id]} (setup-test-data [])] (let [mock-result {:periods [{}]}] (with-redefs [gql-utils/<-graphql (fn [_query] mock-result)] (let [request {:valid-trimmed-client-ids #{test-client-id}} response (ssr-dashboard/pnl-card request)] (is (= 200 (:status response))) (is (re-find #"Profit and Loss" (:body response))))))))) ;; ============================================================================ ;; Tasks Card (6.5, 6.6, 6.7) ;; ============================================================================ (deftest test-tasks-card-unpaid-invoices (testing "Behavior 6.6: It should query Datomic for invoices with unpaid status for the selected clients" (let [{:strs [test-client-id test-vendor-id test-account-id]} (setup-test-data [])] @(dc/transact datomic/conn [{:db/id "unpaid-inv-1" :invoice/client test-client-id :invoice/vendor test-vendor-id :invoice/invoice-number "UNPAID-001" :invoice/date (java.util.Date.) :invoice/total 100.0 :invoice/outstanding-balance 100.0 :invoice/status :invoice-status/unpaid :invoice/import-status :import-status/imported :invoice/expense-accounts [{:invoice-expense-account/account test-account-id :invoice-expense-account/amount 100.0 :invoice-expense-account/location "DT"}]}]) (let [request {:valid-trimmed-client-ids #{test-client-id}} response (ssr-dashboard/tasks-card request)] (is (= 200 (:status response))) (is (re-find #"unpaid invoices" (:body response))))))) (deftest test-tasks-card-feedback-transactions (testing "Behavior 6.7: It should query Datomic for transactions with requires-feedback approval status for the selected clients" (let [{:strs [test-client-id test-bank-account-id]} (setup-test-data [])] @(dc/transact datomic/conn [{:db/id "feedback-tx" :transaction/client test-client-id :transaction/bank-account test-bank-account-id :transaction/id (str (java.util.UUID/randomUUID)) :transaction/date (java.util.Date.) :transaction/amount 50.0 :transaction/description-original "Test transaction" :transaction/approval-status :transaction-approval-status/requires-feedback}]) (let [request {:valid-trimmed-client-ids #{test-client-id}} response (ssr-dashboard/tasks-card request)] (is (= 200 (:status response))) (is (re-find #"transactions needing your feedback" (:body response))))))) (deftest test-tasks-card-hides-zero-counts (testing "Behavior 6.5: It should hide task sections entirely when their respective counts are zero" (let [{:strs [test-client-id]} (setup-test-data [])] (let [request {:valid-trimmed-client-ids #{test-client-id}} response (ssr-dashboard/tasks-card request)] (is (= 200 (:status response))) (is (nil? (re-find #"unpaid invoices" (:body response)))) (is (nil? (re-find #"transactions needing your feedback" (:body response)))))))) ;; ============================================================================ ;; Expense Breakdown Card (7.6) ;; ============================================================================ (deftest test-expense-breakdown-excludes-voided (testing "Behavior 7.6: It should exclude voided invoices from the breakdown" ;; The expense breakdown query uses (not [?e :invoice/status :invoice-status/voided]) ;; to exclude voided invoices. Verify this exclusion logic works correctly. (let [{:strs [test-client-id test-vendor-id test-account-id]} (setup-test-data [])] @(dc/transact datomic/conn [{:db/id "active-inv" :invoice/client test-client-id :invoice/vendor test-vendor-id :invoice/invoice-number "ACTIVE-001" :invoice/date (java.util.Date.) :invoice/total 100.0 :invoice/status :invoice-status/unpaid :invoice/import-status :import-status/imported :invoice/expense-accounts [{:invoice-expense-account/account test-account-id :invoice-expense-account/amount 100.0 :invoice-expense-account/location "DT"}]} {:db/id "voided-inv" :invoice/client test-client-id :invoice/vendor test-vendor-id :invoice/invoice-number "VOIDED-001" :invoice/date (java.util.Date.) :invoice/total 500.0 :invoice/status :invoice-status/voided :invoice/import-status :import-status/imported :invoice/expense-accounts [{:invoice-expense-account/account test-account-id :invoice-expense-account/amount 500.0 :invoice-expense-account/location "DT"}]}]) (let [db (dc/db datomic/conn) ;; Total including voided invoices all-total (ffirst (dc/q '[:find (sum ?amt) :where [?e :invoice/client] [?e :invoice/expense-accounts ?iea] [?iea :invoice-expense-account/amount ?amt]] db)) ;; Total excluding voided invoices (matches the breakdown query pattern) active-total (ffirst (dc/q '[:find (sum ?amt) :where [?e :invoice/client] (not [?e :invoice/status :invoice-status/voided]) [?e :invoice/expense-accounts ?iea] [?iea :invoice-expense-account/amount ?amt]] db))] (is (= 600.0 all-total) "Both invoices should sum to 600.0") (is (= 100.0 active-total) "Only active invoice should sum to 100.0 when voided are excluded"))))) ;; ============================================================================ ;; Client Selection Behaviors (9.5, 9.8) ;; ============================================================================ (deftest test-client-trimming-limits-to-20 (testing "Behavior 9.5: It should limit reports to the first 20 selected clients from the valid set" (let [many-ids (set (map #(long (+ 1000 %)) (range 25))) received (atom nil)] (with-redefs [gql-utils/extract-client-ids (fn [& _] many-ids)] (let [trim-handler (handler/wrap-trim-clients (fn [req] (reset! received req) {:status 200}))] (trim-handler {:clients []}) (is (= 20 (count (:valid-trimmed-client-ids @received)))) (is (= 25 (count (:valid-client-ids @received)))) (is (:clients-trimmed? @received))))))) (deftest test-cards-use-trimmed-client-ids (testing "Behavior 9.8: It should trim the client set before executing any card data queries" (let [{:strs [test-client-id]} (setup-test-data [])] (let [request {:valid-trimmed-client-ids #{test-client-id}} response (ssr-dashboard/bank-accounts-card request)] (is (= 200 (:status response))))))) ;; ============================================================================ ;; Error Handling Behaviors (10.1, 10.2, 10.4) ;; ============================================================================ (deftest test-cards-load-independently (testing "Behavior 10.1: It should load each card independently via separate HTMX requests" (let [{:strs [test-client-id]} (setup-test-data []) request {:valid-trimmed-client-ids #{test-client-id}}] (is (= 200 (:status (ssr-dashboard/bank-accounts-card request)))) (is (= 200 (:status (ssr-dashboard/sales-chart-card request)))) (is (= 200 (:status (ssr-dashboard/expense-pie-card request)))) (is (= 200 (:status (ssr-dashboard/tasks-card request))))))) (deftest test-card-failure-isolation (testing "Behavior 10.2: It should not prevent other cards from loading when one card endpoint fails" (let [{:strs [test-client-id]} (setup-test-data []) request {:valid-trimmed-client-ids #{test-client-id}}] (is (= 200 (:status (ssr-dashboard/sales-chart-card request)))) (is (= 200 (:status (ssr-dashboard/expense-pie-card request)))) (is (= 200 (:status (ssr-dashboard/tasks-card request)))) (is (= 200 (:status (ssr-dashboard/bank-accounts-card request))))))) (deftest test-card-error-status-codes (testing "Behavior 10.4: It should return appropriate HTTP status codes for card endpoint errors without breaking the page layout" (let [{:strs [test-client-id]} (setup-test-data [])] (let [request {:valid-trimmed-client-ids #{test-client-id}} response (ssr-dashboard/bank-accounts-card request)] (is (= 200 (:status response))) (is (= "text/html" (get-in response [:headers "Content-Type"])))))))