---
name: testing-conventions
description: Describe the way that tests should be authored, conventions, tools, helpers, superceding any conventions found in existing tests.
---

# Testing Conventions Skill

This skill documents the testing conventions for `test/clj/auto_ap/`.

## Test Focus: User-Observable Behavior

**Primary rule**: Test user-observable behavior. If an endpoint or function makes a database change, verify the change by querying the database directly rather than asserting on markup.

**other rules**: 
1. Don't test the means of doing work. For example, if there is a middleware that makes something available on a request, don't bother testing that wrapper.
2. prefer :refer testing imports, rather than :as reference
3. Prefer structured edits from clojure-mcp

### When to Assert on Database State

When testing an endpoint that modifies data:
1. Verify the database change by querying the entity directly
2. Use `dc/pull` or `dc/q` to verify the data was stored correctly

```clojure
;; CORRECT: Verify the database change directly
(deftest test-create-transaction
  (let [result @(post-create-transaction {:amount 100.0})]
    (let [entity (dc/pull (dc/db conn) [:db/id :transaction/amount] (:transaction/id result))]
      (is (= 100.0 (:transaction/amount entity))))))

;; CORRECT: Verify response status and headers
(is (= 201 (:status response)))
(is (= "application/json" (get-in response [:headers "content-type"])))

;; CORRECT: Check for expected text content
(is (re-find #"Transaction created" (get-in response [:body "message"])))
```

### When Markup Testing is Acceptable

Markup testing (HTML/SSR response bodies) is acceptable when:
- Validating response status codes and headers
- Checking for presence/absence of specific text strings
- Verifying small, expected elements within the markup
- Testing SSR component rendering

```clojure
;; ACCEPTABLE: Response codes and headers
(is (= 200 (:status response)))
(is (= "application/json" (get-in response [:headers "content-type"])))

;; ACCEPTABLE: Text content within markup
(is (re-find #"Transaction found" response-body))

;; ACCEPTABLE: Small element checks
(is (re-find #">Amount: \$100\.00<" response-body))
```

### When to Avoid Markup Testing

Do not use markup assertions for:
- Verifying complex data structures (use database queries instead)
- Complex nested content that's easier to query
- Business logic verification (test behavior, not presentation)

## Database Setup

All tests in `test/clj/auto_ap/` use a shared database fixture (`wrap-setup`) that:
1. Creates a temporary in-memory Datomic database (`datomic:mem://test`)
2. Loads the full schema from `io/resources/schema.edn`
3. Installs custom Datomic functions from `io/resources/functions.edn`
4. Cleans up the database after each test

## Using the Fixture

```clojure
(ns my-test
  (:require
   [auto-ap.integration.util :refer [wrap-setup]]
   [clojure.test :as t]))

(use-fixtures :each wrap-setup)

(deftest my-test
  ;; tests here can access the test database
  )
```

## Helper Functions

`test/clj/auto_ap/integration/util.clj` provides helper functions for creating test data:

### Identity Helpers

```clojure
;; Add a unique string to avoid collisions
(str "CLIENT" (rand-int 100000))
(str "INVOICE " (rand-int 1000000))
```

### Test Entity Builders

```clojure
;; Client
(test-client
  [:db/id "client-id"
   :client/code "CLIENT123"
   :client/locations ["DT" "MH"]
   :client/bank-accounts [:bank-account-id]])

;; Vendor
(test-vendor
  [:db/id "vendor-id"
   :vendor/name "Vendorson"
   :vendor/default-account "test-account-id"])

;; Bank Account
(test-bank-account
  [:db/id "bank-account-id"
   :bank-account/code "TEST-BANK-123"
   :bank-account/type :bank-account-type/check])

;; Transaction
(test-transaction
  [:db/id "transaction-id"
   :transaction/date #inst "2022-01-01"
   :transaction/client "test-client-id"
   :transaction/bank-account "test-bank-account-id"
   :transaction/id (str (java.util.UUID/randomUUID))
   :transaction/amount 100.0
   :transaction/description-original "original description"])

;; Payment
(test-payment
  [:db/id "test-payment-id"
   :payment/date #inst "2022-01-01"
   :payment/client "test-client-id"
   :payment/bank-account "test-bank-account-id"
   :payment/type :payment-type/check
   :payment/vendor "test-vendor-id"
   :payment/amount 100.0])

;; Invoice
(test-invoice
  [:db/id "test-invoice-id"
   :invoice/date #inst "2022-01-01"
   :invoice/client "test-client-id"
   :invoice/status :invoice-status/unpaid
   :invoice/import-status :import-status/imported
   :invoice/total 100.0
   :invoice/outstanding-balance 100.00
   :invoice/vendor "test-vendor-id"
   :invoice/invoice-number "INVOICE 123456"
   :invoice/expense-accounts
   [{:invoice-expense-account/account "test-account-id"
     :invoice-expense-account/amount 100.0
     :invoice-expense-account/location "DT"}]])

;; Account
(test-account
  [:db/id "account-id"
   :account/name "Account"
   :account/type :account-type/asset])
```

### Common Data Setup (`setup-test-data`)

Creates a minimal but complete dataset for testing:

```clojure
(defn setup-test-data [data]
  (:tempids @(dc/transact conn (into data
                                    [(test-account :db/id "test-account-id")
                                     (test-client :db/id "test-client-id"
                                                  :client/bank-accounts [(test-bank-account :db/id "test-bank-account-id")])
                                     (test-vendor :db/id "test-vendor-id")
                                     {:db/id "accounts-payable-id"
                                      :account/name "Accounts Payable"
                                      :db/ident :account/accounts-payable
                                      :account/numeric-code 21000
                                      :account/account-set "default"}]))))
```

Use like:
```clojure
(let [{:strs [test-client-id test-bank-account-id test-vendor-id]} (setup-test-data [])]
  ...)
```

### Token Helpers

```clojure
;; Admin token
(admin-token)

;; User token (optionally scoped to specific client)
(user-token)           ; Default: client-id 1
(user-token client-id) ; Scoped to specific client
```

## Example Usage

```clojure
(ns my-test
  (:require
   [clojure.test :as t]
   [auto-ap.datomic :refer [conn]]
   [auto-ap.integration.util :refer [wrap-setup admin-token setup-test-data test-transaction]]))

(use-fixtures :each wrap-setup)

(deftest test-transaction-import
  (testing "Should import a transaction"
    (let [{:strs [client-id bank-account-id]} (setup-test-data [])
          tx-result @(dc/transact conn
                                  [(test-transaction
                                    {:db/id "test-tx-id"
                                     :transaction/client client-id
                                     :transaction/bank-account bank-account-id
                                     :transaction/amount 50.0})])]
      (is (= 1 (count (:tx-data tx-result))))
      ;; Verify by querying the database, not markup
      (let [entity (dc/pull (dc/db conn) [:transaction/amount] (:db/id tx-result))]
        (is (= 50.0 (:transaction/amount entity)))))))
```

## Note on Temp IDs

Test data often uses string-based temp IDs like `"client-id"`, `"bank-account-id"`, etc. When transacting, the returned `:tempids` map maps these symbolic IDs to Datomic's internal entity IDs:

```clojure
(let [{:strs [client-id bank-account-id]} (:tempids @(dc/transact conn txes))]
  ...)
```

## Memory Database

All tests use `datomic:mem://test` - an in-memory database. This ensures:
- Tests are fast
- Tests don't interfere with each other
- No setup required to run tests locally

The database is automatically deleted after each test completes.

# running tests
prefer to use clojure nrepl evaluation skill over leiningen, but worst case,
use leiningen to run tests
