diff --git a/.circleci/config.yml b/.circleci/config.yml index b6cc864ac3..16fff49125 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -84,6 +84,8 @@ jobs: - run: lein kaocha --reporter kaocha.report/documentation --plugin rems.kaocha/cache-statistics-plugin integration - store_test_results: path: target/test-results + - store_artifacts: + path: target/cache-statistics-plugin browser-test: executor: db @@ -98,7 +100,7 @@ jobs: at: . - restore_cache: key: *cache-key - - run: lein kaocha --reporter kaocha.report/documentation --plugin rems.kaocha/cache-statistics-plugin --plugin rems.kaocha/circleci-plugin browser + - run: lein kaocha --reporter kaocha.report/documentation --plugin rems.kaocha/cache-statistics-plugin --plugin rems.kaocha/circleci-parallel-plugin browser - store_test_results: path: target/test-results - store_artifacts: @@ -107,6 +109,10 @@ jobs: path: browsertest-downloads - store_artifacts: path: browsertest-accessibility-report + - store_artifacts: + path: target/cache-statistics-plugin + - store_artifacts: + path: target/circleci-parallel-plugin build: executor: clojure diff --git a/.github/workflows/admin-utils-docker-build.yml b/.github/workflows/admin-utils-docker-build.yml index 020b393015..84c17c5af4 100644 --- a/.github/workflows/admin-utils-docker-build.yml +++ b/.github/workflows/admin-utils-docker-build.yml @@ -6,8 +6,8 @@ on: - develop - master paths: - - 'adminUtils.Dockerfile' - - '.github/workflows/admin-utils-docker-build.yml' + - "adminUtils.Dockerfile" + - ".github/workflows/admin-utils-docker-build.yml" workflow_dispatch: permissions: diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c165a64fc..20af988aff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,9 +17,15 @@ Changes since v2.38.1 - When creating attachment license, the save button no longer activates until all attachments have been successfully uploaded. (#3292) - Attachment upload error is indicated by small icon next to the upload button (detailed error is still shown in top of the page). - Pending attachment upload is indicated by spinner next to the upload button. +- Edit buttons on the administration detail pages are hidden when the user is not permitted to edit, like they are hidden on the list pages already (#2814) ### Fixes - Resolved issue where workflow voting could not be removed, which caused UI to display raw translation keys due to nil voting values. (#3357) +- Fixed an issue with the application page where the 'Add member' dropdown menu displayed members that already exist in the application. (#1412) +- Document title no longer shows an extra em dash when the localized application title is empty. (#3398) +- Fix audit log API. Technical error from incorrect response. (#3380) +- Fixes to enable JVM 25 support (https://github.com/tolitius/cprop/issues/60, https://clojure.atlassian.net/browse/CLJ-2764) +- Catalogue table had the add/remove cart commands even when not logged in. (#3408) ## v2.38.1 "Välimerenkatu +1" 2025-01-27 diff --git a/README.md b/README.md index 1d9fdea952..3ce878f016 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ See the [release page](https://github.com/CSCfi/rems/releases) for the releases. - We welcome [issues through GitHub](https://github.com/CSCfi/rems/issues). - You can [contact the team on the discussion board](https://github.com/CSCfi/rems/discussions) or [via email](mailto:rems@csc.fi). - In case you would like to contribute to the development, perhaps you participate in Hacktoberfest, then please refer to the [contributing document](CONTRIBUTING.md). We'll guide you through to get your pull request reviewed and merged. -- You can follow the progress of the project on the [GitHub project board]([https://github.com/CSCfi/rems/projects/1](https://github.com/orgs/CSCfi/projects/13)). +- You can follow the progress of the project on the [GitHub project board](https://github.com/orgs/CSCfi/projects/13/views/1). ## Documentation @@ -43,3 +43,7 @@ You can check out the API docs using [the swagger-ui of the public demo instance](https://rems-test.2.rahtiapp.fi/swagger-ui). Documentation can be found under the [docs](./docs) folder. + +## See Also + +[REMS-COmanage bridge service](https://github.com/GUARDIANS-infrastructure/rems-co) – Service that synchronizes entitlements between REMS and a COManage registry. diff --git a/dev-config.edn b/dev-config.edn index 5c0968a457..70dfa76e0f 100644 --- a/dev-config.edn +++ b/dev-config.edn @@ -56,7 +56,8 @@ :heading false :translations {:fi {:title "Mixed" :filename "mixed-fi.md"} - :en {:url "https://example.org/en/mixed"}} ; missing sv and da + :en {:title "Mixed" + :url "https://example.org/en/mixed"}} ; missing sv and da :show-menu false :show-footer false} {:id "unlocalized" diff --git a/docs/presentations/README.md b/docs/presentations/README.md index f4a597f64a..e7d6db6493 100644 --- a/docs/presentations/README.md +++ b/docs/presentations/README.md @@ -4,11 +4,11 @@ Here we have presentation materials for REMS. Each presentation is in the form o ## How to use – reveal.js -Follow the [https://revealjs.com/installation/#full-setup](instructions) to setup reveal.js, and off you go. +Follow the [instructions](https://revealjs.com/installation/#full-setup) to setup reveal.js, and off you go. ## How to use – CSC's training template -This is a bit more cumbersome than the previous example but does not require setting up a local web server. [https://pandoc.org](Pandoc) is required, however. +This is a bit more cumbersome than the previous example but does not require setting up a local web server. [Pandoc](https://pandoc.org) is required, however. 1. `git clone https://github.com/csc-training/slide-template` diff --git a/package-lock.json b/package-lock.json index 6b0581072f..ebce7832ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1546,13 +1546,18 @@ } }, "node_modules/cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz", + "integrity": "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==", "dev": true, + "license": "MIT", "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.2" + }, + "engines": { + "node": ">= 0.10" } }, "node_modules/cliui": { @@ -3725,16 +3730,24 @@ "dev": true }, "node_modules/sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", "dev": true, + "license": "(MIT AND BSD-3-Clause)", "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" }, "bin": { "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/shadow-cljs": { @@ -4045,15 +4058,13 @@ } }, "node_modules/tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", "dev": true, - "dependencies": { - "rimraf": "^3.0.0" - }, + "license": "MIT", "engines": { - "node": ">=8.17.0" + "node": ">=14.14" } }, "node_modules/to-arraybuffer": { @@ -4063,9 +4074,9 @@ "dev": true }, "node_modules/to-buffer": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz", - "integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", "dev": true, "license": "MIT", "dependencies": { diff --git a/src/clj/rems/api/audit_log.clj b/src/clj/rems/api/audit_log.clj index 030ea20db1..268a96b023 100644 --- a/src/clj/rems/api/audit_log.clj +++ b/src/clj/rems/api/audit_log.clj @@ -1,6 +1,7 @@ (ns rems.api.audit-log (:require [compojure.api.sweet :refer :all] [rems.db.core :as db] + [ring.util.http-response :refer [ok]] [schema.core :as s]) (:import (org.joda.time DateTime))) @@ -23,8 +24,8 @@ {before :- (describe DateTime "Only show entries before this time") nil}] :roles #{:reporter} :return [AuditLogEntry] - (db/get-audit-log {:userid userid - :after after - :path (when application-id - (str "/api/applications/" application-id "%")) - :before before})))) + (ok (db/get-audit-log {:userid userid + :after after + :path (when application-id + (str "/api/applications/" application-id "%")) + :before before}))))) diff --git a/src/clj/rems/cache.clj b/src/clj/rems/cache.clj index 890996e3dd..7e8304219a 100644 --- a/src/clj/rems/cache.clj +++ b/src/clj/rems/cache.clj @@ -1,7 +1,6 @@ (ns rems.cache (:require [clojure.core.cache.wrapped :as w] [clojure.tools.logging.readable :as logr] - [medley.core :refer [update-existing]] [rems.common.util :refer [build-index]] [rems.common.dependency :as dep] [rems.config])) @@ -11,8 +10,6 @@ (def ^{:doc "Value that tells cache to skip entry."} absent ::absent) -(def ^:private initial-statistics {:get 0 :reload 0 :upsert 0 :evict 0}) - (defprotocol RefreshableCacheProtocol "Protocol for cache wrapper that can refresh the underlying cache." @@ -40,8 +37,6 @@ (export-statistics! [this] "Retrieves runtime statistics from cache, and resets appropriate counters.") - (increment-get-statistic! [this] - "Increments cache access counter.") (increment-reload-statistic! [this] "Increments cache reload counter.") (increment-upsert-statistic! [this] @@ -73,14 +68,13 @@ CacheStatisticsProtocol (export-statistics! [this] - (let [stats @statistics] - (reset! statistics (select-keys initial-statistics (keys stats))) - stats)) + (let [value @statistics] + (reset! statistics {:reload 0 :upsert 0 :evict 0}) + value)) - (increment-get-statistic! [this] (swap! statistics update-existing :get inc)) - (increment-reload-statistic! [this] (swap! statistics update :reload inc)) - (increment-upsert-statistic! [this] (swap! statistics update :upsert inc)) - (increment-evict-statistic! [this] (swap! statistics update :evict inc)) + (increment-reload-statistic! [this] (some-> statistics (swap! update :reload inc))) + (increment-upsert-statistic! [this] (some-> statistics (swap! update :upsert inc))) + (increment-evict-statistic! [this] (some-> statistics (swap! update :evict inc))) RefreshableCacheProtocol @@ -106,8 +100,6 @@ (ensure-initialized! [this] ;; NB: reading requires no locking - (increment-get-statistic! this) - (if @initialized? @the-cache (reload! this))) @@ -184,23 +176,27 @@ (increment-evict-statistic! this) (logr/debug "<" id :evict k))))) -(defn basic [{:keys [depends-on id miss-fn reload-fn]}] +(defn- ensure-cache-id-unique! [id] (when (contains? @caches id) + ;; NB: even if config is not started, we default to assert (if (:dev rems.config/env) (logr/warnf "overriding cache id %s" id) - (assert false (format "error overriding cache id %s" id)))) - (let [initialized? false - statistics (if (:dev rems.config/env) - initial-statistics - (select-keys initial-statistics [:reload :upsert :evict])) ; :get statistics can become big quickly - the-cache (w/basic-cache-factory {}) - cache (->RefreshableCache id - (atom statistics) - (atom initialized?) - the-cache - miss-fn - (or reload-fn (constantly {})))] - (swap! caches assoc id cache) - (swap! caches-dag dep/depend id depends-on) ; noop when empty depends-on - (add-watch the-cache id reset-dependents-on-change!) - cache)) + (assert false (format "error overriding cache id %s" id))))) + +(defn basic [{:keys [depends-on id miss-fn reload-fn]}] + (or (ensure-cache-id-unique! id) + (let [initialized? false + statistics {} + the-cache (w/basic-cache-factory {}) + cache (->RefreshableCache id + (atom statistics) + (atom initialized?) + the-cache + miss-fn + (or reload-fn (constantly {}))) + _ (export-statistics! cache) ; NB: initializes statistics + ] + (swap! caches assoc id cache) + (swap! caches-dag dep/depend id depends-on) ; noop when empty depends-on + (add-watch the-cache id reset-dependents-on-change!) + cache))) diff --git a/src/clj/rems/config.clj b/src/clj/rems/config.clj index 4d6e13585e..5e6f866655 100644 --- a/src/clj/rems/config.clj +++ b/src/clj/rems/config.clj @@ -3,7 +3,7 @@ [clojure.java.io :as io] [clojure.test :refer :all] [clojure.tools.logging :as log] - [cprop.core :refer [load-config]] + [cprop.core] [cprop.source :as source] [cprop.tools :refer [merge-maps]] [medley.core :refer [update-existing]] @@ -14,6 +14,14 @@ (:import [java.io FileNotFoundException] [org.joda.time Period])) +(defn load-config [& args] + ;; XXX: workaround for JVM 25 for cprop until fix is released https://github.com/tolitius/cprop/issues/60 + (let [expand-home cprop.tools/expand-home] + (with-redefs [cprop.tools/expand-home #(if (empty? %) + nil + (expand-home %))] + (apply cprop.core/load-config args)))) + (defn- file-sibling [file sibling-name] (.getPath (io/file (.getParentFile (io/file file)) sibling-name))) diff --git a/src/cljc/rems/common/roles.cljc b/src/cljc/rems/common/roles.cljc index 5ff50518fb..79e6ea0fc3 100644 --- a/src/cljc/rems/common/roles.cljc +++ b/src/cljc/rems/common/roles.cljc @@ -50,3 +50,8 @@ (some (comp #{(-> item :organization :organization/id)} :organization/id)))] (or owner? org-owner?)))) + +#?(:cljs + (defn show-when-can-modify-organization-item [item & body] + (when (can-modify-organization-item? item) + (into [:<> body])))) diff --git a/src/cljs/rems/actions/add_member.cljs b/src/cljs/rems/actions/add_member.cljs index 2df77b5668..354906c65d 100644 --- a/src/cljs/rems/actions/add_member.cljs +++ b/src/cljs/rems/actions/add_member.cljs @@ -22,12 +22,23 @@ ::selected-member nil) ::fetch-potential-members #(rf/dispatch [::set-potential-members %])})) -(rf/reg-sub ::potential-members (fn [db _] (::potential-members db))) +(defn potential-members [db] + (let [application (get-in db [:rems.application/application :data]) + applicant-id (get-in application [:application/applicant :userid]) + members (get-in application [:application/members]) + existing-ids (into #{applicant-id} + (map :userid members))] + (->> ::potential-members db + (remove (comp existing-ids :userid)) + (map atoms/enrich-user)))) + +(rf/reg-sub ::potential-members potential-members) + (rf/reg-event-db ::set-potential-members (fn [db [_ members]] (assoc db - ::potential-members (set (map atoms/enrich-user members)) + ::potential-members members ::selected-member nil))) (rf/reg-event-db ::set-selected-member (fn [db [_ member]] (assoc db ::selected-member member))) diff --git a/src/cljs/rems/administration/catalogue_item.cljs b/src/cljs/rems/administration/catalogue_item.cljs index b0c4005b8e..eac15b0329 100644 --- a/src/cljs/rems/administration/catalogue_item.cljs +++ b/src/cljs/rems/administration/catalogue_item.cljs @@ -104,11 +104,11 @@ (let [id (:id catalogue-item)] [:div.col.commands [administration/back-button "/administration/catalogue-items"] - [roles/show-when roles/+admin-write-roles+ + [roles/show-when-can-modify-organization-item catalogue-item [edit-button id] [perform-action-button (toggle-enabled catalogue-item)] - [perform-action-button (toggle-archived catalogue-item)]] - [manage-categories-button]])]) + [perform-action-button (toggle-archived catalogue-item)] + [manage-categories-button]]])]) (defn catalogue-item-page [] (let [catalogue-item (rf/subscribe [::catalogue-item]) diff --git a/src/cljs/rems/administration/catalogue_items.cljs b/src/cljs/rems/administration/catalogue_items.cljs index 54aa920769..cd1570877e 100644 --- a/src/cljs/rems/administration/catalogue_items.cljs +++ b/src/cljs/rems/administration/catalogue_items.cljs @@ -87,7 +87,9 @@ [atoms/rate-limited-action-button {:id :update-catalogue-item :class "btn-primary" - :disabled (when (empty? items) :disabled) + :disabled (when (or (empty? items) + (not-every? roles/can-modify-organization-item? items)) + :disabled) :on-click (fn [] (rf/dispatch [:rems.administration.update-catalogue-item/enter-page items]) (navigate! "/administration/catalogue-items/update-catalogue-item")) diff --git a/src/cljs/rems/administration/form.cljs b/src/cljs/rems/administration/form.cljs index 90394b31b8..bfd8bd3e0b 100644 --- a/src/cljs/rems/administration/form.cljs +++ b/src/cljs/rems/administration/form.cljs @@ -91,8 +91,9 @@ [:div.col.commands [administration/back-button "/administration/forms"] [roles/show-when roles/+admin-write-roles+ + [copy-as-new-button id]] + [roles/show-when-can-modify-organization-item form [edit-button id] - [copy-as-new-button id] [perform-action-button (toggle-enabled form)] [perform-action-button (toggle-archived form)]]]) (when-let [errors (:form/errors form)] diff --git a/src/cljs/rems/administration/license.cljs b/src/cljs/rems/administration/license.cljs index 8bb4dc65b0..a996598971 100644 --- a/src/cljs/rems/administration/license.cljs +++ b/src/cljs/rems/administration/license.cljs @@ -87,7 +87,7 @@ [:div.col.commands [administration/back-button "/administration/licenses"] - [roles/show-when roles/+admin-write-roles+ + [roles/show-when-can-modify-organization-item license [perform-action-button (toggle-enabled license)] [perform-action-button (toggle-archived license)]]]]) diff --git a/src/cljs/rems/administration/organizations.cljs b/src/cljs/rems/administration/organizations.cljs index 01c5bdbc02..97f6de3cf6 100644 --- a/src/cljs/rems/administration/organizations.cljs +++ b/src/cljs/rems/administration/organizations.cljs @@ -76,21 +76,18 @@ (str "/administration/organizations/" organization-id) (text :t.administration/view)]) -(defn modify-organization-dropdown [organization] - (let [id (:organization/id organization) - org-owner? (->> @(rf/subscribe [:owned-organizations]) - (some (comp #{id} :organization/id)))] - [atoms/commands-group-button - {:label (text :t.actions/modify)} +(defn modify-organization-dropdown [{id :organization/id :as organization}] + [atoms/commands-group-button + {:label (text :t.actions/modify)} - (when org-owner? - (edit-action id)) + (when (roles/can-modify-organization-item? {:organization organization}) + (edit-action id)) ;; XXX: organization owner cannot use these actions currently - (when (roles/has-roles? :owner) - (list - (status-flags/enabled-toggle-action {:on-change #(rf/dispatch [::set-organization-enabled %1 %2 [::fetch-organizations]])} organization) - (status-flags/archived-toggle-action {:on-change #(rf/dispatch [::set-organization-archived %1 %2 [::fetch-organizations]])} organization)))])) + (when (roles/has-roles? :owner) + (list + (status-flags/enabled-toggle-action {:on-change #(rf/dispatch [::set-organization-enabled %1 %2 [::fetch-organizations]])} organization) + (status-flags/archived-toggle-action {:on-change #(rf/dispatch [::set-organization-archived %1 %2 [::fetch-organizations]])} organization)))]) (rf/reg-sub ::organizations-table-rows diff --git a/src/cljs/rems/administration/resource.cljs b/src/cljs/rems/administration/resource.cljs index 1f66442fa6..7fca2fd848 100644 --- a/src/cljs/rems/administration/resource.cljs +++ b/src/cljs/rems/administration/resource.cljs @@ -89,7 +89,7 @@ [resource-blacklist] [:div.col.commands [administration/back-button "/administration/resources"] - [roles/show-when roles/+admin-write-roles+ + [roles/show-when-can-modify-organization-item resource [perform-action-button (toggle-enabled resource)] [perform-action-button (toggle-archived resource)]]]])) diff --git a/src/cljs/rems/administration/workflow.cljs b/src/cljs/rems/administration/workflow.cljs index e650f51305..bcb6f1740a 100644 --- a/src/cljs/rems/administration/workflow.cljs +++ b/src/cljs/rems/administration/workflow.cljs @@ -230,10 +230,9 @@ [voting-fields] [disable-commands-fields] [processing-states-fields] - [:div.col.commands [administration/back-button "/administration/workflows"] - [roles/show-when roles/+admin-write-roles+ + [roles/show-when-can-modify-organization-item workflow [atoms/action-button (edit-workflow-action (:id workflow))] [perform-action-button (toggle-enabled workflow)] [perform-action-button (toggle-archived workflow)]]]])]]) diff --git a/src/cljs/rems/atoms.cljs b/src/cljs/rems/atoms.cljs old mode 100644 new mode 100755 index a81773a914..76e0354d54 --- a/src/cljs/rems/atoms.cljs +++ b/src/cljs/rems/atoms.cljs @@ -222,10 +222,11 @@ " <" (:email email) ">")])) (defn- set-document-title! [s] - (r/with-let [title (r/reaction ; changes when text and text-format change - (if-not (str/blank? s) - (text-format :t.label/dash s (text :t.header/title)) - (text :t.header/title)))] + (r/with-let [app-title (text :t.header/title) + title (r/reaction ; changes when text and text-format change + (if-not (str/blank? app-title) + (text-format :t.label/dash s app-title) + s))] (set! (.-title js/document) @title))) (defn document-title [title & [{:keys [heading?] :or {heading? true}}]] diff --git a/src/cljs/rems/catalogue.cljs b/src/cljs/rems/catalogue.cljs index 654c5d2a3e..14663369d8 100644 --- a/src/cljs/rems/catalogue.cljs +++ b/src/cljs/rems/catalogue.cljs @@ -112,7 +112,8 @@ :name {:value (get-localized-title item)} :commands {:display-value [:div.commands.flex-nowrap.justify-content-end [catalogue-item-more-info item] - [row-command item cart-item-ids]]}}) + (when @roles/logged-in? + [row-command item cart-item-ids])]}}) catalogue)))) (defn draft-application-list [] diff --git a/test-config.edn b/test-config.edn index e5b816d6af..dcc52ce84f 100644 --- a/test-config.edn +++ b/test-config.edn @@ -42,7 +42,8 @@ :heading false :translations {:fi {:title "Mixed" :filename "mixed-fi.md"} - :en {:url "https://example.org/en/mixed"}} ; missing sv + :en {:title "Mixed" + :url "https://example.org/en/mixed"}} ; missing sv :show-menu false :show-footer false} {:id "unlocalized" diff --git a/test/clj/rems/api/test_over_http.clj b/test/clj/rems/api/test_over_http.clj index 45883c5ac0..10553596fc 100644 --- a/test/clj/rems/api/test_over_http.clj +++ b/test/clj/rems/api/test_over_http.clj @@ -119,3 +119,25 @@ :keys first :kid)))) + +(deftest test-api-audit-log + (create-test-data) + (test-helpers/create-user! {:userid "reporter"} :reporter) + + (testing "populate audit log with a 404 not found" + (is (thrown-with-msg? clojure.lang.ExceptionInfo + #"clj-http: status 404" + (http/get "http://localhost:3001/api/unknown")))) + + (testing "response status is 200 OK" + (let [{:keys [status body]} (http/get (str (:public-url rems.config/env) "/api/audit-log") + {:as :json + :headers {"x-rems-api-key" "42" + "x-rems-user-id" "reporter"}})] + (is (= 200 status)) + (is (= [{:apikey nil + :method "get" + :path "/api/unknown" + :status "404" + :userid nil}] + (mapv #(dissoc % :time) body)))))) diff --git a/test/clj/rems/db/test_attachments.clj b/test/clj/rems/db/test_attachments.clj new file mode 100644 index 0000000000..0469a2ab34 --- /dev/null +++ b/test/clj/rems/db/test_attachments.clj @@ -0,0 +1,66 @@ +(ns ^:integration rems.db.test-attachments + (:require [clojure.test :refer [deftest is testing use-fixtures]] + [rems.cache :as cache] + [rems.db.attachments] + [rems.db.test-data-helpers :as test-helpers] + [rems.db.testing :refer [rollback-db-fixture test-db-fixture]] + [rems.util :refer [to-bytes]])) + +(use-fixtures :once test-db-fixture) +(use-fixtures :each rollback-db-fixture) + +(deftest test-attachments-cache + (testing "cache reload works" + (let [applicant (test-helpers/create-user! {:userid "alice"}) + cat-id (test-helpers/create-catalogue-item! {}) + app-id (test-helpers/create-application! {:catalogue-item-ids [cat-id] + :actor applicant}) + attachment-1 (test-helpers/create-attachment! {:application-id app-id + :actor applicant}) + attachment-2 (test-helpers/create-attachment! {:application-id app-id + :actor applicant})] + + ;; force cache reload + (cache/set-uninitialized! rems.db.attachments/attachment-cache) + + (is (= {attachment-1 {:application/id app-id + :attachment/id attachment-1 + :attachment/filename "attachment.pdf" + :attachment/type "application/pdf" + :attachment/user applicant} + attachment-2 {:application/id app-id + :attachment/id attachment-2 + :attachment/filename "attachment (1).pdf" + :attachment/type "application/pdf" + :attachment/user applicant}} + (into {} (cache/entries! rems.db.attachments/attachment-cache)))) + + (testing "dependent caches" + ;; verify that by-application-id is cached correctly (depends on attachment-cache) + (is (= {app-id [{:application/id app-id + :attachment/id attachment-1 + :attachment/filename "attachment.pdf" + :attachment/type "application/pdf" + :attachment/user applicant} + {:application/id app-id + :attachment/id attachment-2 + :attachment/filename "attachment (1).pdf" + :attachment/type "application/pdf" + :attachment/user applicant}]} + (into {} (cache/entries! @#'rems.db.attachments/by-application-id)))))))) + +(deftest test-license-attachments-cache + (testing "cache reload works" + (let [user-id (test-helpers/create-user! {:userid "alice"}) + attachment-id (rems.db.attachments/create-license-attachment! + {:user-id user-id + :filename "attachment.pdf" + :content-type "application/pdf" + :data (to-bytes "data")})] + (cache/set-uninitialized! rems.db.attachments/license-attachments-cache) + + (is (= {attachment-id {:attachment/id attachment-id + :attachment/user user-id + :attachment/filename "attachment.pdf" + :attachment/type "application/pdf"}} + (into {} (cache/entries! rems.db.attachments/license-attachments-cache))))))) diff --git a/test/clj/rems/db/test_blacklist.clj b/test/clj/rems/db/test_blacklist.clj index ad1530ae41..664fb31406 100644 --- a/test/clj/rems/db/test_blacklist.clj +++ b/test/clj/rems/db/test_blacklist.clj @@ -1,6 +1,7 @@ (ns ^:integration rems.db.test-blacklist (:require [clj-time.core :as time] [clojure.test :refer [deftest is testing use-fixtures]] + [rems.cache :as cache] [rems.db.blacklist] [rems.db.test-data-helpers :as test-helpers] [rems.db.testing :refer [test-db-fixture rollback-db-fixture]])) @@ -15,24 +16,48 @@ (test-helpers/create-resource! {:resource-ext-id "urn.fi/123"}) (test-helpers/create-resource! {:resource-ext-id "urn.fi/124"}) - (rems.db.blacklist/add-event! {:event/type :blacklist.event/add - :event/actor "handler" - :event/time (time/date-time 2019 1 2 8 0 0) - :userid "user1" - :resource/ext-id "urn.fi/123" - :event/comment "1"}) - (rems.db.blacklist/add-event! {:event/type :blacklist.event/remove - :event/actor "handler" - :event/time (time/date-time 2019 2 3 9 0 0) - :userid "user1" - :resource/ext-id "urn.fi/123" - :event/comment "2"}) - (rems.db.blacklist/add-event! {:event/type :blacklist.event/add - :event/actor "handler" - :event/time (time/date-time 2019 1 1 1 0 0) - :userid "user2" - :resource/ext-id "urn.fi/124" - :event/comment "3"}) + (let [event-1 (rems.db.blacklist/add-event! {:event/type :blacklist.event/add + :event/actor "handler" + :event/time (time/date-time 2019 1 2 8 0 0) + :userid "user1" + :resource/ext-id "urn.fi/123" + :event/comment "1"}) + event-2 (rems.db.blacklist/add-event! {:event/type :blacklist.event/remove + :event/actor "handler" + :event/time (time/date-time 2019 2 3 9 0 0) + :userid "user1" + :resource/ext-id "urn.fi/123" + :event/comment "2"}) + event-3 (rems.db.blacklist/add-event! {:event/type :blacklist.event/add + :event/actor "handler" + :event/time (time/date-time 2019 1 1 1 0 0) + :userid "user2" + :resource/ext-id "urn.fi/124" + :event/comment "3"})] + (testing "cache reload works" + (cache/set-uninitialized! rems.db.blacklist/blacklist-event-cache) + (is (= {event-1 {:event/id event-1 + :event/type :blacklist.event/add + :event/actor "handler" + :event/time (time/date-time 2019 1 2 8 0 0) + :userid "user1" + :resource/ext-id "urn.fi/123" + :event/comment "1"} + event-2 {:event/id event-2 + :event/type :blacklist.event/remove + :event/actor "handler" + :event/time (time/date-time 2019 2 3 9 0 0) + :userid "user1" + :resource/ext-id "urn.fi/123" + :event/comment "2"} + event-3 {:event/id event-3 + :event/type :blacklist.event/add + :event/actor "handler" + :event/time (time/date-time 2019 1 1 1 0 0) + :userid "user2" + :resource/ext-id "urn.fi/124" + :event/comment "3"}} + (into {} (cache/entries! rems.db.blacklist/blacklist-event-cache)))))) (let [events (rems.db.blacklist/get-events {:resource/ext-id "urn.fi/123"})] ;; event id sequence numbers aren't predictable since even diff --git a/test/clj/rems/db/test_catalogue.clj b/test/clj/rems/db/test_catalogue.clj index ccba04c618..39c1dc3419 100644 --- a/test/clj/rems/db/test_catalogue.clj +++ b/test/clj/rems/db/test_catalogue.clj @@ -1,8 +1,14 @@ -(ns rems.db.test-catalogue +(ns ^:integration rems.db.test-catalogue (:require [clj-time.core :as time] - [clojure.test :refer :all] + [clojure.test :refer [deftest is testing use-fixtures]] + [rems.cache :as cache] [rems.common.util :refer [apply-filters]] - [rems.db.catalogue])) + [rems.db.catalogue] + [rems.db.test-data-helpers :as test-helpers] + [rems.db.testing :refer [test-db-fixture rollback-db-fixture]])) + +(use-fixtures :once test-db-fixture) +(use-fixtures :each rollback-db-fixture) (deftest test-now-active? (let [t0 (time/epoch) @@ -75,3 +81,48 @@ (testing "calculates :active property" (is (every? #(contains? % :expired) (get-items {})))))) + +(deftest test-catalogue-cache + (testing "cache reload works" + (let [org-id (test-helpers/create-organization! {}) + form-id (test-helpers/create-form! {:form/external-title {:en "form" :fi "form" :sv "form"} + :form/internal-name "test-form"}) + workflow-id (test-helpers/create-workflow! {:title "workflow"}) + resource-id (test-helpers/create-resource! {:resource-ext-id "test-resource"}) + fixed-time (time/date-time 2020 1 1 12 0) + item-id (test-helpers/create-catalogue-item! {:resource-id resource-id + :form-id form-id + :workflow-id workflow-id + :organization {:organization/id org-id} + :title {:en "Test Item" + :fi "Testiasia"} + :infourl {:en "http://test.com" + :fi "http://test.fi"} + :start fixed-time})] + + ;; force cache reload + (cache/set-uninitialized! rems.db.catalogue/catalogue-item-cache) + (cache/set-uninitialized! rems.db.catalogue/catalogue-item-localizations-cache) + + (is (= {item-id {:archived false + :enabled true + :end nil + :formid form-id + :id item-id + :localizations {} ; joined from cache. see rems.db.catalogue/localize-catalogue-item + :organization {:organization/id org-id} + :resource-id resource-id + :start fixed-time + :wfid workflow-id}} + (into {} (cache/entries! rems.db.catalogue/catalogue-item-cache)))) + + (testing "localizations cache" + (is (= {item-id {:en {:id item-id + :infourl "http://test.com" + :langcode :en + :title "Test Item"} + :fi {:id item-id + :infourl "http://test.fi" + :langcode :fi + :title "Testiasia"}}} + (into {} (cache/entries! rems.db.catalogue/catalogue-item-localizations-cache)))))))) diff --git a/test/clj/rems/db/test_form.clj b/test/clj/rems/db/test_form.clj new file mode 100644 index 0000000000..1773cda9c0 --- /dev/null +++ b/test/clj/rems/db/test_form.clj @@ -0,0 +1,42 @@ +(ns ^:integration rems.db.test-form + (:require [clojure.test :refer [deftest is testing use-fixtures]] + [rems.cache :as cache] + [rems.db.form :as form] + [rems.db.test-data-helpers :as test-helpers] + [rems.db.testing :refer [test-db-fixture rollback-db-fixture]])) + +(use-fixtures :once test-db-fixture) +(use-fixtures :each rollback-db-fixture) + +(deftest test-form-template-cache + (testing "cache reload works" + (let [org-id (test-helpers/create-organization! {}) + form-id (test-helpers/create-form! {:organization {:organization/id org-id} + :form/internal-name "test-form" + :form/external-title {:en "Test Form" + :fi "Testilomake" + :sv "Testformulär"} + :form/fields [{:field/type :text + :field/title {:en "Text field" + :fi "Tekstikenttä" + :sv "Textfält"} + :field/optional false}]})] + ;; force cache reload + (cache/set-uninitialized! rems.db.form/form-template-cache) + + (is (= {form-id {:form/id form-id + :organization {:organization/id org-id} + :form/internal-name "test-form" + :form/external-title {:fi "Testilomake" + :en "Test Form" + :sv "Testformulär"} + :form/title "test-form" ; deprecated + :form/fields [{:field/title {:fi "Tekstikenttä" + :en "Text field" + :sv "Textfält"} + :field/type :text + :field/id "fld1" + :field/optional false}] + :enabled true + :archived false}} + (into {} (cache/entries! rems.db.form/form-template-cache))))))) \ No newline at end of file diff --git a/test/clj/rems/db/test_licenses.clj b/test/clj/rems/db/test_licenses.clj new file mode 100644 index 0000000000..ccabf3bf7f --- /dev/null +++ b/test/clj/rems/db/test_licenses.clj @@ -0,0 +1,47 @@ +(ns ^:integration rems.db.test-licenses + (:require [clojure.test :refer [deftest is testing use-fixtures]] + [rems.cache :as cache] + [rems.db.licenses] + [rems.db.test-data-helpers :as test-helpers] + [rems.db.testing :refer [test-db-fixture rollback-db-fixture]])) + +(use-fixtures :once test-db-fixture) +(use-fixtures :each rollback-db-fixture) + +(deftest test-license-cache + (testing "cache reload works" + (let [org-id (test-helpers/create-organization! {}) + license-id (test-helpers/create-license! {:license/type :link + :organization {:organization/id org-id} + :license/title {:en "Test License" + :fi "Testilisenssi" + :sv "Testlicens"} + :license/link {:en "http://example.com/license/en" + :fi "http://example.com/license/fi" + :sv "http://example.com/license/sv"}})] + ;; force cache reload + (cache/set-uninitialized! rems.db.licenses/license-cache) + (cache/set-uninitialized! rems.db.licenses/license-localizations-cache) + + ;; verify license is cached correctly + (is (= {license-id {:id license-id + :licensetype "link" + :organization org-id + :enabled true + :archived false}} + (into {} (cache/entries! rems.db.licenses/license-cache)))) + + (testing "localizations cache" + (is (= {license-id {:en {:licid license-id + :langcode :en + :title "Test License" + :textcontent "http://example.com/license/en"} + :fi {:licid license-id + :langcode :fi + :title "Testilisenssi" + :textcontent "http://example.com/license/fi"} + :sv {:licid license-id + :langcode :sv + :title "Testlicens" + :textcontent "http://example.com/license/sv"}}} + (into {} (cache/entries! rems.db.licenses/license-localizations-cache)))))))) \ No newline at end of file diff --git a/test/clj/rems/db/test_organizations.clj b/test/clj/rems/db/test_organizations.clj new file mode 100644 index 0000000000..51268e2d0e --- /dev/null +++ b/test/clj/rems/db/test_organizations.clj @@ -0,0 +1,35 @@ +(ns ^:integration rems.db.test-organizations + (:require [clojure.test :refer [deftest is testing use-fixtures]] + [rems.cache :as cache] + [rems.db.organizations] + [rems.db.test-data-helpers :as test-helpers] + [rems.db.testing :refer [test-db-fixture rollback-db-fixture]])) + +(use-fixtures :once test-db-fixture) +(use-fixtures :each rollback-db-fixture) + +(deftest test-organization-cache + (testing "cache reload works" + (let [org-id (test-helpers/create-organization! {:organization/id "test-org" + :organization/name {:fi "Testiorganisaatio" + :en "Test Organization" + :sv "Testorganisation"} + :organization/short-name {:fi "Testi" + :en "Test" + :sv "Test"} + :organization/owners [{:userid "owner"}]})] + ;; force cache reload + (cache/set-uninitialized! rems.db.organizations/organization-cache) + + (is (= {org-id {:organization/id org-id + :organization/name {:fi "Testiorganisaatio" + :en "Test Organization" + :sv "Testorganisation"} + :organization/short-name {:fi "Testi" + :en "Test" + :sv "Test"} + :organization/owners [{:userid "owner"}] + :organization/review-emails [] + :enabled true + :archived false}} + (into {} (cache/entries! rems.db.organizations/organization-cache))))))) \ No newline at end of file diff --git a/test/clj/rems/db/test_resource.clj b/test/clj/rems/db/test_resource.clj new file mode 100644 index 0000000000..446c341456 --- /dev/null +++ b/test/clj/rems/db/test_resource.clj @@ -0,0 +1,25 @@ +(ns ^:integration rems.db.test-resource + (:require [clojure.test :refer [deftest is testing use-fixtures]] + [rems.cache :as cache] + [rems.db.resource] + [rems.db.test-data-helpers :as test-helpers] + [rems.db.testing :refer [test-db-fixture rollback-db-fixture]])) + +(use-fixtures :once test-db-fixture) +(use-fixtures :each rollback-db-fixture) + +(deftest test-resource-cache + (testing "cache reload works" + (let [org-id (test-helpers/create-organization! {}) + res-id (test-helpers/create-resource! {:resource-ext-id "test-resource" + :organization {:organization/id org-id} + :resid "https://example.org/resource"})] + ;; force cache reload + (cache/set-uninitialized! rems.db.resource/resource-cache) + + (is (= {res-id {:id res-id + :organization {:organization/id org-id} + :resid "test-resource" + :enabled true + :archived false}} + (into {} (cache/entries! rems.db.resource/resource-cache))))))) \ No newline at end of file diff --git a/test/clj/rems/db/test_roles.clj b/test/clj/rems/db/test_roles.clj index 14462a8848..38c58ccbbd 100644 --- a/test/clj/rems/db/test_roles.clj +++ b/test/clj/rems/db/test_roles.clj @@ -1,5 +1,6 @@ (ns ^:integration rems.db.test-roles (:require [clojure.test :refer [deftest is testing use-fixtures]] + [rems.cache :as cache] [rems.db.roles] [rems.db.testing :refer [rollback-db-fixture test-db-fixture]] [rems.db.users])) @@ -41,6 +42,17 @@ (is (= #{"user"} (rems.db.roles/get-users-with-role :owner))) (is (= #{"user"} (rems.db.roles/get-users-with-role :reporter)))) + (testing "cache reload works" + ;; force cache reload + (cache/set-uninitialized! rems.db.roles/role-cache) + (is (= {"user" #{:owner :reporter}} + (into {} (cache/entries! rems.db.roles/role-cache)))) + + (testing "dependent caches" + (is (= {:owner #{"user"} + :reporter #{"user"}} + (into {} (cache/entries! @#'rems.db.roles/users-by-role)))))) + (testing "remove owner role" (rems.db.roles/remove-role! "user" :owner) (is (= #{:logged-in :reporter} (rems.db.roles/get-roles "user"))) diff --git a/test/clj/rems/db/test_transactions.clj b/test/clj/rems/db/test_transactions.clj index 9d9eee1754..8bdf01e220 100644 --- a/test/clj/rems/db/test_transactions.clj +++ b/test/clj/rems/db/test_transactions.clj @@ -137,7 +137,7 @@ (fn [] (cache-invalidater) ;; give some mercy - (Thread/sleep (rand-int 50))))))) + (Thread/sleep (long (rand-int 50)))))))) writes-count (atom 0) progress-count (atom -1) ; start below writes-count for the first round writers (submit-all thread-pool (for [app-id app-ids @@ -155,7 +155,7 @@ ;; if there is no progress within 100ms ;; something is wrong, like deadlock (reset! progress-count @writes-count) - (Thread/sleep 100)) + (Thread/sleep (long 100))) (log/info "Finished with " @writes-count "of" target-writes "writes") diff --git a/test/clj/rems/db/test_user_mappings.clj b/test/clj/rems/db/test_user_mappings.clj new file mode 100644 index 0000000000..0578a7e1f4 --- /dev/null +++ b/test/clj/rems/db/test_user_mappings.clj @@ -0,0 +1,62 @@ +(ns ^:integration rems.db.test-user-mappings + (:require [clojure.test :refer [deftest is testing use-fixtures]] + [rems.cache :as cache] + [rems.db.testing :refer [rollback-db-fixture test-db-fixture]] + [rems.db.user-mappings] + [rems.db.test-data-helpers :as test-helpers])) + +(use-fixtures :once test-db-fixture) +(use-fixtures :each rollback-db-fixture) + +(deftest test-user-mapping-caches + (testing "cache reload works" + (let [user-1 (test-helpers/create-user! {:userid "alice"}) + user-2 (test-helpers/create-user! {:userid "bob"}) + _ (rems.db.user-mappings/create-user-mapping! {:userid user-1 + :ext-id-attribute "elixirId" + :ext-id-value "elixir-alice"}) + _ (rems.db.user-mappings/create-user-mapping! {:userid user-2 + :ext-id-attribute "elixirId" + :ext-id-value "elixir-bob"}) + _ (rems.db.user-mappings/create-user-mapping! {:userid user-2 + :ext-id-attribute "altId" + :ext-id-value "bob-alt"})] + ;; force cache reload + (cache/set-uninitialized! rems.db.user-mappings/user-mappings-cache) + + ;; verify user-mappings-cache is cached correctly + (is (= {"alice" [{:userid "alice" + :ext-id-attribute "elixirId" + :ext-id-value "elixir-alice"}] + "bob" [{:userid "bob" + :ext-id-attribute "elixirId" + :ext-id-value "elixir-bob"} + {:userid "bob" + :ext-id-attribute "altId" + :ext-id-value "bob-alt"}]} + (into {} (cache/entries! rems.db.user-mappings/user-mappings-cache)))) + + (testing "dependent caches" + ;; verify by-extidattribute is cached correctly (depends on user-mappings-cache) + (is (= {"elixirId" [{:userid "alice" + :ext-id-attribute "elixirId" + :ext-id-value "elixir-alice"} + {:userid "bob" + :ext-id-attribute "elixirId" + :ext-id-value "elixir-bob"}] + "altId" [{:userid "bob" + :ext-id-attribute "altId" + :ext-id-value "bob-alt"}]} + (into {} (cache/entries! @#'rems.db.user-mappings/by-extidattribute)))) + + ;; verify by-extidvalue is cached correctly (depends on user-mappings-cache) + (is (= {"elixir-alice" [{:userid "alice" + :ext-id-attribute "elixirId" + :ext-id-value "elixir-alice"}] + "elixir-bob" [{:userid "bob" + :ext-id-attribute "elixirId" + :ext-id-value "elixir-bob"}] + "bob-alt" [{:userid "bob" + :ext-id-attribute "altId" + :ext-id-value "bob-alt"}]} + (into {} (cache/entries! @#'rems.db.user-mappings/by-extidvalue)))))))) \ No newline at end of file diff --git a/test/clj/rems/db/test_users.clj b/test/clj/rems/db/test_users.clj index 9fbfc610e9..0991413817 100644 --- a/test/clj/rems/db/test_users.clj +++ b/test/clj/rems/db/test_users.clj @@ -30,6 +30,19 @@ :organizations [{:organization/id "org"}]} (rems.db.users/get-user "user-with-org"))) + (testing "cache reload works" + ;; force cache reload + (cache/set-uninitialized! rems.db.users/user-cache) + (is (= {"user-with-org" {:userid "user-with-org" + :name "User Org" + :email "user@org" + :organizations [{:organization/id "org"}]} + "user1" {:userid "user1" + :name "What Ever" + :email nil + :some-attr "some value"}} + (into {} (cache/entries! rems.db.users/user-cache))))) + (testing "user has different userid in userattrs" ;; NB: raw db call due testing backwards compatible behavior (db/add-user! {:user "different-userid" :userattrs (json/generate-string {:userid "bad"})}) diff --git a/test/clj/rems/db/test_workflow.clj b/test/clj/rems/db/test_workflow.clj index 4c8c8e9f85..cd8df2f94c 100644 --- a/test/clj/rems/db/test_workflow.clj +++ b/test/clj/rems/db/test_workflow.clj @@ -1,8 +1,9 @@ (ns ^:integration rems.db.test-workflow - (:require [clojure.test :refer :all] + (:require [clojure.test :refer [deftest is testing use-fixtures]] [rems.db.testing :refer [rollback-db-fixture test-db-fixture]] [rems.db.workflow] - [rems.db.test-data-helpers :as test-helpers])) + [rems.db.test-data-helpers :as test-helpers] + [rems.cache :as cache])) (use-fixtures :once test-db-fixture) (use-fixtures :each rollback-db-fixture) @@ -16,39 +17,65 @@ (is (= #{:handler} (rems.db.workflow/get-all-workflow-roles "handler-user"))))) (deftest test-crud-workflow - (testing "creating" - (let [id (rems.db.workflow/create-workflow! {:userid "owner" - :organization {:organization/id "abc"} - :type :workflow/default - :title "test-crud-workflow-title" - :handlers ["bob" "handler"] - :forms [] - :licenses []})] - (is (number? id)) - (is (= {:archived false - :organization {:organization/id "abc"} - :title "test-crud-workflow-title" - :workflow {:type :workflow/default - :handlers [{:userid "bob"} - {:userid "handler"}] - :forms [] - :licenses []} - :id id - :enabled true} - (rems.db.workflow/get-workflow id))) + (let [expected-id (atom nil)] - (testing "editing" - (rems.db.workflow/edit-workflow! {:id id - :handlers ["bob" "handler" "alice"]}) + (testing "creating" + (let [id (rems.db.workflow/create-workflow! {:userid "owner" + :organization {:organization/id "abc"} + :type :workflow/default + :title "test-crud-workflow-title" + :handlers ["bob" "handler"] + :forms [] + :licenses []}) + _ (reset! expected-id id)] + (is (number? id)) (is (= {:archived false :organization {:organization/id "abc"} :title "test-crud-workflow-title" :workflow {:type :workflow/default :handlers [{:userid "bob"} - {:userid "handler"} - {:userid "alice"}] + {:userid "handler"}] :forms [] :licenses []} :id id :enabled true} - (rems.db.workflow/get-workflow id))))))) + (rems.db.workflow/get-workflow id))))) + + (testing "cache reload works" + (cache/set-uninitialized! rems.db.workflow/workflow-cache) + (is (= {@expected-id {:archived false + :organization {:organization/id "abc"} + :title "test-crud-workflow-title" + :workflow {:type :workflow/default + :handlers [{:userid "bob"} + {:userid "handler"}] + :forms [] + :licenses []} + :id @expected-id + :enabled true}} + (into {} (cache/entries! rems.db.workflow/workflow-cache))) + "cache contains the workflow")) + + (testing "editing" + (rems.db.workflow/edit-workflow! {:id @expected-id + :handlers ["bob" "handler" "alice"]}) + (is (= {:archived false + :organization {:organization/id "abc"} + :title "test-crud-workflow-title" + :workflow {:type :workflow/default + :handlers [{:userid "bob"} + {:userid "handler"} + {:userid "alice"}] + :forms [] + :licenses []} + :id @expected-id + :enabled true} + (rems.db.workflow/get-workflow @expected-id))) + + (testing "archive status" + (rems.db.workflow/set-archived! @expected-id true) + (is (:archived (rems.db.workflow/get-workflow @expected-id)))) + + (testing "enabled status" + (rems.db.workflow/set-enabled! @expected-id false) + (is (not (:enabled (rems.db.workflow/get-workflow @expected-id)))))))) diff --git a/test/clj/rems/kaocha.clj b/test/clj/rems/kaocha.clj index 9e9292f618..3350df213e 100644 --- a/test/clj/rems/kaocha.clj +++ b/test/clj/rems/kaocha.clj @@ -1,6 +1,8 @@ (ns rems.kaocha "Namespace for various Kaocha test runner helpers." - (:require [clojure.java.shell :as sh] + (:require [better-cond.core :as b] + [clojure.java.io :as io] + [clojure.java.shell :as sh] [clojure.string :as str] [kaocha.plugin :as p] [kaocha.result] @@ -10,6 +12,10 @@ [rems.markdown] [rems.service.caches])) +;; directories for storing plugin output +(def cache-statistics-plugin-dir "target/cache-statistics-plugin") +(def circleci-parallel-plugin-dir "target/circleci-parallel-plugin") + (def is-kaocha-var (comp #{:kaocha.type/var} :kaocha.testable/type)) (defn parse-keyword [s] @@ -18,13 +24,22 @@ :always (keyword))) (defn split-test-ids [test-plan] - (->> (sh/sh "circleci" "tests" "split" "--split-by=timings" - :in (str/join "\n" (for [test (test-seq test-plan) - :when (is-kaocha-var test)] - (:kaocha.testable/id test)))) - :out - (str/split-lines) - (into #{} (map parse-keyword)))) + (let [test-ids (for [test (test-seq test-plan) + :when (is-kaocha-var test)] + (:kaocha.testable/id test)) + test-ids-batch (->> (sh/sh "circleci" "tests" "split" "--split-by=timings" "--timings-type=testname" + :in (str/join "\n" test-ids)) + :out + (str/split-lines) + (into #{} (map parse-keyword))) + test-ids-report-file (io/file circleci-parallel-plugin-dir "test-ids.txt") + split-test-ids-report-file (io/file circleci-parallel-plugin-dir "split-test-ids.txt")] + + (io/make-parents test-ids-report-file) ; shared parent, need only once + (spit test-ids-report-file (str/join "\n" (sort test-ids))) + (spit split-test-ids-report-file (str/join "\n" (sort test-ids-batch))) + + test-ids-batch)) (defn walk-kaocha-tests [m f] (letfn [(recurse [tests] @@ -43,7 +58,7 @@ ;; Plugin that performs runtime test filtering in CircleCI using test splitting. ;; Skips excluded test ids (not in split test batch) before test run. ;; Heavily inspired by https://andreacrotti.github.io/2020-07-28-parallel-ci-kaocha/ -(defmethod p/-register :rems.kaocha/circleci-plugin [_name plugins] +(defmethod p/-register :rems.kaocha/circleci-parallel-plugin [_name plugins] (conj plugins {:kaocha.hooks/post-load (fn [test-plan] ; skip tests that are not included in set of test ids returned by circle ci @@ -66,7 +81,7 @@ :when (some? c)] {id {test-id c}})))) -(defn- sum-total [x] (+ (:get x 0) (:reload x 0) (:upsert x 0) (:evict x 0))) +(defn- sum-total [x] (+ (:reload x 0) (:upsert x 0) (:evict x 0))) (defn- enrich-statistics [[cache-id by-test-id]] (let [total-stats (apply merge-with + (vals by-test-id)) @@ -106,15 +121,17 @@ {:kaocha.hooks/post-summary (fn [result] (when-not (kaocha.result/failed? result) ; skip performance summary if tests fail - (when-let [stats (->> (rems.db.testing/get-cache-statistics) - group-cache-stats-by-test - (keep enrich-statistics) - seq)] - (println "") - (println (format "Top %d cache users grouped by cache:" top-n-results)) - (println "") - (run! println (rems.markdown/markdown-table - {:header [:id "%" :get :upsert :evict :reload] - :rows (get-tabular-data stats) - :row-fn (juxt :id :% :get :upsert :evict :reload)})))) + (b/when-let [stats (->> (rems.db.testing/get-cache-statistics) + group-cache-stats-by-test + (keep enrich-statistics) + seq) + report-file (io/file cache-statistics-plugin-dir "cache-report.md")] + (io/make-parents report-file) + (spit report-file + (str/join "\n" + (cons (format "Top %d cache users grouped by cache:\n" top-n-results) + (rems.markdown/markdown-table + {:header [:id "%" :upsert :evict :reload] + :rows (get-tabular-data stats) + :row-fn (juxt :id :% :upsert :evict :reload)})))))) result)})) diff --git a/test/clj/rems/test_browser.clj b/test/clj/rems/test_browser.clj index 24a14df162..ceb5902db4 100644 --- a/test/clj/rems/test_browser.clj +++ b/test/clj/rems/test_browser.clj @@ -27,6 +27,7 @@ [rems.db.test-data-users :as test-users] [rems.db.user-settings] [rems.handler] + [rems.main] [rems.service.application] [rems.service.catalogue] [rems.service.form] @@ -2167,11 +2168,15 @@ (btu/context-assoc! :workflow1 (test-helpers/create-workflow! {:title "test-update-catalogue-item workflow 1"})) (btu/context-assoc! :workflow2 (test-helpers/create-workflow! {:title "test-update-catalogue-item workflow 2"})) (btu/context-assoc! :workflow3 (test-helpers/create-workflow! {:title "test-update-catalogue-item workflow 3"})) + (btu/context-assoc! :workflow4 (test-helpers/create-workflow! {:title "test-update-catalogue-item workflow 4" + :organization {:organization/id "nbn"}})) (btu/context-assoc! :form1 (test-helpers/create-form! {:form/internal-name "test-update-catalogue-item form 1" :form/title {:en "test-update-catalogue-item form 1 EN" :fi "test-update-catalogue-item form 1 FI" :sv "test-update-catalogue-item form 1 SV"}})) (btu/context-assoc! :form2 (test-helpers/create-form! {:form/internal-name "test-update-catalogue-item form 2"})) + (btu/context-assoc! :form4 (test-helpers/create-form! {:form/internal-name "test-update-catalogue-item form 4" + :organization {:organization/id "nbn"}})) (btu/context-assoc! :catalogue-item1 (test-helpers/create-catalogue-item! {:title {:en "test-update-catalogue-item 1 EN" :fi "test-update-catalogue-item 1 FI" :sv "test-update-catalogue-item 1 SV"} @@ -2187,6 +2192,12 @@ :sv "test-update-catalogue-item 3 SV"} :form-id (btu/context-getx :form2) :workflow-id (btu/context-getx :workflow2)})) + (btu/context-assoc! :catalogue-item4 (test-helpers/create-catalogue-item! {:title {:en "test-update-catalogue-item 4 EN" + :fi "test-update-catalogue-item 4 FI" + :sv "test-update-catalogue-item 4 SV"} + :form-id (btu/context-getx :form4) + :workflow-id (btu/context-getx :workflow4) + :organization {:organization/id "nbn"}})) (login-as "owner") (go-to-admin "Catalogue items") (btu/wait-page-loaded) @@ -2270,7 +2281,63 @@ "active" true "organization" "Default"}] (->> (slurp-tds [:catalogue {:css "tr:has(td.selection *[aria-checked=true])"}]) - (mapv #(dissoc % "resource" "created" "commands")))))))) + (mapv #(dissoc % "resource" "created" "commands")))))) + + (testing "modify actions are not enabled when not organization owner" + (logout) + (login-as "organization-owner2") + (go-to-admin "Catalogue items") + (btu/wait-page-loaded) + (is (not (btu/visible? {:fn/text "test-update-catalogue-item 1 EN"}))) + (is (not (btu/visible? {:fn/text "test-update-catalogue-item 2 EN"}))) + (is (not (btu/visible? {:fn/text "test-update-catalogue-item 3 EN"}))) + (is (btu/eventually-visible? {:fn/text "test-update-catalogue-item 4 EN"})) + (btu/scroll-and-click {:fn/text "Own organization only"}) + (is (btu/eventually-visible? {:fn/text "test-update-catalogue-item 1 EN"})) + (is (btu/eventually-visible? {:fn/text "test-update-catalogue-item 2 EN"})) + (is (btu/eventually-visible? {:fn/text "test-update-catalogue-item 3 EN"})) + (is (->> (slurp-rows :catalogue) + (filter #(= "Default" (get % "organization"))) + (every? #(= "View" (get % "commands")))))) + + (testing "update is disabled when not owner of selected items" + (btu/wait-disabled {:tag :button :fn/text "Update catalogue item"}) + (btu/scroll-and-click {:fn/text "test-update-catalogue-item 4 EN"}) + (btu/scroll-and-click {:fn/text "test-update-catalogue-item 1 EN"}) + (btu/wait-disabled {:tag :button :fn/text "Update catalogue item"}) + (btu/scroll-and-click {:fn/text "test-update-catalogue-item 1 EN"}) + (btu/wait-enabled {:tag :button :fn/text "Update catalogue item"}) + + (testing "update remains disabled when selecting all items with toggle-all" + (btu/scroll-and-click {:id ":rems.administration.catalogue-items/catalogue-selection-toggle-all"}) + (btu/wait-page-loaded) + (is (empty? (slurp-tds [:catalogue {:css "tr:has(td.selection *[aria-checked=true])"}]))) + (btu/scroll-and-click {:id ":rems.administration.catalogue-items/catalogue-selection-toggle-all"}) + (btu/wait-page-loaded) + (is (not-empty (slurp-tds [:catalogue {:css "tr:has(td.selection *[aria-checked=true])"}]))) + (btu/wait-disabled {:tag :button :fn/text "Update catalogue item"})) + + (testing "update is enabled when selecting all items under the user's organization with toggle-all" + (btu/scroll-and-click {:id ":rems.administration.catalogue-items/catalogue-selection-toggle-all"}) + (btu/scroll-and-click {:fn/text "Own organization only"}) + (btu/scroll-and-click {:id ":rems.administration.catalogue-items/catalogue-selection-toggle-all"}) + (btu/wait-page-loaded) + (is (= ["test-update-catalogue-item 4 EN"] + (mapv #(get % "name") + (slurp-tds [:catalogue {:css "tr:has(td.selection *[aria-checked=true])"}])))) + (btu/wait-enabled {:tag :button :fn/text "Update catalogue item"}))) + + (testing "edit buttons are not visible" + (btu/scroll-and-click {:fn/text "Own organization only"}) + (btu/wait-page-loaded) + (click-row-action [:catalogue] + {:fn/text "test-update-catalogue-item 1 EN"} + (select-button-by-label "View")) + (is (btu/eventually-visible? :back)) + (is (not (btu/visible? :edit))) + (is (not (btu/visible? :disable))) + (is (not (btu/visible? :archive))) + (is (not (btu/visible? :manage-categories)))))) ;;; form editor test utilities @@ -2334,7 +2401,7 @@ (testing "create form" (btu/scroll-and-click {:fn/text "Create form"}) (wait-page-title "Create form – REMS") - (select-option "Organization" "nbn") + (select-option "Organization" "Default") (fill-form-field "Name" "Form editor test") (fill-form-field "EN" "Form Editor Test (EN)") (fill-form-field "FI" "Form Editor Test (FI)") @@ -2443,6 +2510,7 @@ :let [column-id (keyword (format "columns-%s-key" i)) label-id (comp keyword (partial format "columns-%s-label-%s" i) name)]] (btu/scroll-and-click (field-component :add-column)) + (btu/wait-visible (field-component column-id)) (btu/fill-human (field-component column-id) (format "table-column-%s" i)) (btu/fill-human (field-component (label-id :en)) (format "Table column %s (EN)" i)) (btu/fill-human (field-component (label-id :fi)) (format "Table column %s (FI)" i)) @@ -2525,7 +2593,7 @@ (testing "view form" (wait-page-title "Form – REMS") (btu/wait-page-loaded) - (is (= {"Organization" "NBN" + (is (= {"Organization" "The Default Organization" "Name" "Form editor test" "Title (EN)" "Form Editor Test (EN)" "Title (FI)" "Form Editor Test (FI)" @@ -2619,10 +2687,32 @@ (btu/wait-page-loaded) (wait-page-title "Form – REMS"))) + (testing "edit buttons are not visible when not organization owner" + (logout) + (login-as "organization-owner2") + (go-to-admin "Forms") + (btu/wait-page-loaded) + (btu/scroll-query :forms) + (is (not (btu/visible? {:fn/text "Form editor test"}))) + (btu/scroll-and-click {:fn/text "Own organization only"}) + (btu/eventually-visible? {:fn/text "Form editor test"}) + (is (= "View\nCopy as new" + (->> (slurp-table :forms) + (some #(when (= "Default" (get % "organization")) + (get % "commands")))))) + (click-row-action [:forms] + {:fn/text "Form editor test"} + (select-button-by-label "View")) + (is (btu/eventually-visible? :back)) + (is (btu/visible? {:fn/has-class :btn :fn/has-text "Copy as new"})) + (is (not (btu/visible? :edit))) + (is (not (btu/visible? :disable))) + (is (not (btu/visible? :archive)))) + (testing "fetch form via api" (let [form-id (Integer/parseInt (last (str/split (btu/get-url) #"/")))] (is (= {:form/id form-id - :organization {:organization/id "nbn" :organization/name {:fi "NBN" :en "NBN" :sv "NBN"} :organization/short-name {:fi "NBN" :en "NBN" :sv "NBN"}} + :organization {:organization/id "default" :organization/name {:fi "Oletusorganisaatio" :en "The Default Organization" :sv "Standardorganisationen"} :organization/short-name {:fi "Oletus" :en "Default" :sv "Standard"}} :form/internal-name "Form editor test" :form/external-title {:en "Form Editor Test (EN)" :fi "Form Editor Test (FI)" @@ -2913,6 +3003,7 @@ (btu/with-postmortem (login-as "owner") (go-to-admin "Workflows") + (testing "create workflow" (btu/context-assoc! :workflow-title (str "test-workflow-create-edit " (btu/get-seed))) (btu/scroll-and-click :create-workflow) @@ -2930,6 +3021,7 @@ (select-option "Licenses" "General Terms of Use") (btu/screenshot "test-workflow-create-edit-1") (btu/scroll-and-click :save)) + (testing "view workflow" (wait-page-title "Workflow – REMS") (btu/wait-page-loaded) @@ -2957,6 +3049,7 @@ (is (= "General Terms of Use" (btu/get-element-text {:tag :div :id :workflow-licenses}))) ; readonly field (btu/screenshot "test-workflow-create-edit-4") (btu/scroll-and-click :save)) + (testing "view workflow again" (wait-page-title "Workflow – REMS") (btu/wait-page-loaded) @@ -2970,7 +3063,27 @@ "Licenses" "General Terms of Use" "Active" true} (slurp-fields :workflow-common-fields))) - (is (btu/visible? {:tag :a :fn/text "Simple form"}))))) + (is (btu/visible? {:tag :a :fn/text "Simple form"}))) + + (testing "edit buttons are not visible when not organization owner" + (logout) + (login-as "organization-owner2") + (go-to-admin "Workflows") + (btu/wait-page-loaded) + (is (not (btu/visible? {:fn/text (str (btu/context-getx :workflow-title) " v2")}))) + (btu/scroll-and-click {:fn/text "Own organization only"}) + (btu/eventually-visible? {:fn/text (str (btu/context-getx :workflow-title) " v2")}) + (is (= "View" + (->> (slurp-table :workflows) + (some #(when (= "Default" (get % "organization")) + (get % "commands")))))) + (click-row-action [:workflows] + {:fn/text (str (btu/context-getx :workflow-title) " v2")} + (select-button-by-label "View")) + (is (btu/eventually-visible? :back)) + (is (not (btu/visible? :edit))) + (is (not (btu/visible? :disable))) + (is (not (btu/visible? :archive)))))) (deftest test-blacklist (btu/with-postmortem @@ -3291,7 +3404,14 @@ (->> (slurp-table :organizations) (some #(when (= "SNEN" (get % "short-name")) (get % "commands"))))) - "organization actions should not be visible for non organization owner")))))))) + "organization actions should not be visible for non organization owner") + (testing "edit button is not visible when not organization owner" + (click-row-action [:organizations] + {:fn/text (str (btu/context-getx :organization-name) " EN")} + (select-button-by-label "View")) + (btu/wait-page-loaded) + (is (btu/eventually-visible? :back)) + (is (not (btu/visible? :edit))))))))))) (deftest test-small-navbar (testing "create a test application with the API to have another page to navigate to" @@ -3601,7 +3721,32 @@ "Bilaga (FI)" "Ladda ner fil\nE2E license with attachments (FI)" "Bilaga (SV)" "Ladda ner fil\nE2E license with attachments (SV)" "Aktiv" true} - (slurp-fields :license)))))))) + (slurp-fields :license)))))) + (testing "edit buttons are not visible when not organization owner" + (with-change-language :en + (test-helpers/create-license! {:license/title {:en "License-EN" + :fi "License-FI" + :sv "License-SV"} + :license/text {:en "License text EN" + :fi "License text FI" + :sv "License text SV"}}) + (logout) + (login-as "organization-owner2") + (go-to-admin "Licenses") + (btu/wait-visible [:licenses {:fn/text "E2E license with external links (EN)"}]) + (is (not (btu/visible? [:licenses {:fn/text "License-EN"}]))) + (btu/scroll-and-click {:fn/text "Own organization only"}) + (btu/wait-visible [:licenses {:fn/text "License-EN"}]) + (is (= "View" + (->> (slurp-table :licenses) + (some #(when (= "Default" (get % "organization")) + (get % "commands")))))) + (click-row-action [:licenses] + {:fn/text "Default"} + (select-button-by-label "View")) + (is (btu/eventually-visible? :back)) + (is (not (btu/visible? :disable))) + (is (not (btu/visible? :archive))))))) (deftest test-extra-pages (btu/with-postmortem @@ -3680,7 +3825,7 @@ (btu/go (str (btu/get-server-url) "extra-pages/mixed")) ;; no header, only default translation in document title (is (not (btu/visible? {:tag :h1}))) - (is (= "REMS" (btu/get-title))) + (is (= "Mixed – REMS" (btu/get-title))) (is (btu/eventually-visible? {:tag :a :fn/has-text "https://example.org/en/mixed"})))) (btu/go (btu/get-server-url)) diff --git a/test/clj/rems/test_cache.clj b/test/clj/rems/test_cache.clj index 4a8cbb9601..4b128ee902 100644 --- a/test/clj/rems/test_cache.clj +++ b/test/clj/rems/test_cache.clj @@ -3,7 +3,6 @@ [clojure.test :refer [deftest is testing]] [clojure.tools.logging.readable :as logr] [clojure.tools.logging.test :as log-test] - [clojure.walk] [medley.core :refer [map-vals]] [rems.cache :as cache] [rems.common.dependency :as dep] @@ -22,12 +21,12 @@ (defn- get-cache-entries "Returns cache as hash map that can be asserted with =" [c] - (clojure.walk/keywordize-keys (cache/entries! c))) + (into {} (cache/entries! c))) (defn- get-cache-raw "Like get-cache-entries, but does not trigger cache readyness mechanisms." [c] - (clojure.walk/keywordize-keys @(get c :the-cache))) + (into {} @(get c :the-cache))) (def ^:private miss-fn (constantly {:value true})) (def ^:private reload-fn (constantly {:always {:value :special}})) @@ -36,9 +35,15 @@ (with-redefs [rems.cache/caches (doto caches (reset! nil)) rems.cache/caches-dag (doto caches-dag (reset! (dep/make-graph))) rems.config/env {:dev true}] - (let [c (cache/basic {:id ::test-cache - :miss-fn (fn [id] (miss-fn id)) - :reload-fn (fn [] (reload-fn))})] + (let [miss-fn-calls (atom []) + reload-fn-call-count (atom 0) + c (cache/basic {:id ::test-cache + :miss-fn (fn [id] + (swap! miss-fn-calls conj id) + (miss-fn id)) + :reload-fn (fn [] + (swap! reload-fn-call-count inc) + (reload-fn))})] (is (= c (get @caches ::test-cache))) (is (= [] @@ -50,38 +55,40 @@ (get-cache-raw c))) (is (= false (deref (:initialized? c)))) - (is (= {:evict 0 :get 0 :reload 0 :upsert 0} + (is (= {:evict 0 :reload 0 :upsert 0} (cache/export-statistics! c)))) (testing "entries reloads cache" (is (= {:always {:value :special}} (get-cache-entries c))) - (is (= {:evict 0 :get 1 :reload 1 :upsert 0} - (cache/export-statistics! c))))) + (is (= {:evict 0 :reload 1 :upsert 0} + (cache/export-statistics! c))) + (is (= 1 + @reload-fn-call-count)))) (testing "lookup" (is (= nil (cache/lookup! c :a))) (is (= {:value :special} (cache/lookup! c :always))) - (is (= {:evict 0 :get 2 :reload 0 :upsert 0} - (cache/export-statistics! c))) - (is (= {:always {:value :special}} - (get-cache-raw c)))) + (is (= {:evict 0 :reload 0 :upsert 0} + (cache/export-statistics! c)))) (testing "lookup-or-miss" (testing "existing entry should not trigger cache miss" (is (= {:value :special} (cache/lookup-or-miss! c :always))) - (is (= {:evict 0 :get 1 :reload 0 :upsert 0} - (cache/export-statistics! c))) - (is (= {:always {:value :special}} - (get-cache-raw c)))) + (is (= [] + @miss-fn-calls)) + (is (= {:evict 0 :reload 0 :upsert 0} + (cache/export-statistics! c)))) (testing "non-existing entry should be added on cache miss" (is (= {:value true} (cache/lookup-or-miss! c :a))) - (is (= {:evict 0 :get 2 :reload 0 :upsert 1} + (is (= [:a] + @miss-fn-calls)) + (is (= {:evict 0 :reload 0 :upsert 1} (cache/export-statistics! c))) (is (= {:a {:value true} :always {:value :special}} @@ -90,8 +97,10 @@ (testing "absent value skips cache entry" (with-redefs [miss-fn (constantly cache/absent)] (is (= nil - (cache/lookup-or-miss! c :test-skip)))) - (is (= {:evict 0 :get 1 :reload 0 :upsert 0} + (cache/lookup-or-miss! c :test-skip))) + (is (= [:a :test-skip] + @miss-fn-calls))) + (is (= {:evict 0 :reload 0 :upsert 0} (cache/export-statistics! c))) (is (= {:a {:value true} :always {:value :special}} @@ -101,29 +110,38 @@ (cache/evict! c :a) (is (= nil (cache/lookup! c :a))) - (is (= {:evict 1 :get 3 :reload 0 :upsert 0} + (is (= {:evict 1 :reload 0 :upsert 0} (cache/export-statistics! c))) (is (= {:always {:value :special}} (get-cache-raw c))) (testing "non-existing entry does nothing" (cache/evict! c :does-not-exist) - (is (= {:evict 0 :get 1 :reload 0 :upsert 0} + (is (= {:evict 0 :reload 0 :upsert 0} (cache/export-statistics! c))) (is (= {:always {:value :special}} (get-cache-raw c))))) (testing "miss" (cache/miss! c :new-entry) + (is (= [:a :test-skip :new-entry] + @miss-fn-calls)) (is (= {:value true} (cache/lookup! c :new-entry))) + (is (= {:value true} (cache/lookup-or-miss! c :new-entry))) - (is (= {:evict 0 :get 4 :reload 0 :upsert 1} + (is (= [:a :test-skip :new-entry] + @miss-fn-calls)) + + (is (= {:evict 0 :reload 0 :upsert 1} (cache/export-statistics! c))) (is (= {:always {:value :special} :new-entry {:value true}} - (get-cache-raw c))))))) + (get-cache-raw c)))) + + (is (= 1 @reload-fn-call-count) + "cache has been reloaded exactly once")))) (deftest test-cache-dependencies (testing "cannot create caches with circular dependencies"