diff --git a/CHANGELOG.md b/CHANGELOG.md index 5dadd7ca08..72e12ced6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ Changes since v2.11 ### Additions - `api/applications/:id/attachments` api for downloading all attachments as a zip file (#2075) +- Event notifications over HTTP. See [docs/event-notification.md](docs/event-notification.md) for details. (#2095) ## v2.11 "Kotitontuntie" 2020-04-07 diff --git a/docs/event-notification.md b/docs/event-notification.md new file mode 100644 index 0000000000..8ec97e7a25 --- /dev/null +++ b/docs/event-notification.md @@ -0,0 +1,33 @@ +# Event notification + +REMS can be configured to send events notifications over HTTP. + +Event notifications are performed _one at a time_, waiting for a HTTP +200 response from the notification endpoint. Everything except a HTTP +200 response counts as a failure. Failed notifications are retried +with exponential backoff for up to 12 hours. + +Due to retries, the event notification endpoint _should be_ idempotent. + +Event notifications are _not guaranteed to be ordered_ by event +creation order, especially when retries occur. + +Event notification failures are logged. You can also inspect the +`outbox` db table for the retry state of notifications. + +## Configuration + +See `:event-notification-targets` in [config-defaults.edn](../resources/config-defaults.edn). + +## Schema + +The body of the HTTP PUT request will be a JSON object that contains: + +- `"event/type"`: the type of the event, a string +- `"event/actor"`: who caused this event +- `"event/time"`: when the event occured +- `"application/id"`: the id of the application +- `"event/application"`: the state of the application, in the same format as the `/api/applications/:id/raw` endpoint returns (see Swagger docs) + +Other keys may also be present depending on the event type. + diff --git a/project.clj b/project.clj index e59fc2840f..f48b1efe63 100644 --- a/project.clj +++ b/project.clj @@ -169,7 +169,7 @@ [figwheel-sidecar "0.5.19" :exclusions [org.clojure/tools.nrepl com.fasterxml.jackson.core/jackson-core]] [re-frisk "0.5.4.1"] [ring/ring-mock "0.4.0" :exclusions [cheshire]] - [se.haleby/stub-http "0.2.7"]] + [se.haleby/stub-http "0.2.8"]] :plugins [[lein-ancient "0.6.15"] [lein-doo "0.1.11"] diff --git a/resources/config-defaults.edn b/resources/config-defaults.edn index dd1022838f..caaedeb568 100644 --- a/resources/config-defaults.edn +++ b/resources/config-defaults.edn @@ -54,6 +54,24 @@ :remove nil :ga4gh nil} ;; Url where entitlements are pushed in ga4gh format, see https://github.com/ga4gh-duri/ga4gh-duri.github.io/ + ;; URLs to notify about new events. An array of targets. Targets can have keys: + ;; :url (mandatory) - the url to send HTTP PUT requests to + ;; :event-types (optional) - an array of event types to send. A missing value means "send everything". + ;; :timeout (optional) - timeout for the PUT in seconds. Defaults to 60s. + ;; :headers (optional) - a map of additional HTTP headers to send. + ;; + ;; See also: docs/event-notification.md + ;; + ;; Example: + ;; + ;; :event-notification-targets [{:url "http://events/everything"} + ;; {:url "http://events/filtered" + ;; :event-types [:application.event/created :application.event/submitted] + ;; :timeout 120 + ;; :headers {"Authorization" "abc123" + ;; "X-Header" "value"}}] + :event-notification-targets [] + ;; Which database column to show as the application id. ;; Options: :id, :external-id :application-id-column :external-id diff --git a/resources/sql/queries.sql b/resources/sql/queries.sql index 50c3e6dab4..ba3cc8e0c3 100644 --- a/resources/sql/queries.sql +++ b/resources/sql/queries.sql @@ -552,7 +552,7 @@ WHERE 1 = 1 /*~ (when (:ids params) */ AND id IN (:v*:ids) /*~ ) ~*/ -; +ORDER BY id ASC; -- :name update-outbox! :! UPDATE outbox diff --git a/src/clj/rems/api/applications.clj b/src/clj/rems/api/applications.clj index e12749543b..851ae4ef96 100644 --- a/src/clj/rems/api/applications.clj +++ b/src/clj/rems/api/applications.clj @@ -245,12 +245,24 @@ ;; the path parameter matches also non-numeric paths, so this route must be after all overlapping routes (GET "/:application-id" [] - :summary "Get application by `application-id`" + :summary "Get application by `application-id`. Application is customized for the requesting user (e.g. event visibility, permissions, etc)." :roles #{:logged-in} :path-params [application-id :- (describe s/Int "application id")] :responses {200 {:schema Application} 404 {:schema s/Str :description "Not found"}} - (if-let [app (applications/get-application (getx-user-id) application-id)] + (if-let [app (applications/get-application-for-user (getx-user-id) application-id)] + (ok app) + (api-util/not-found-json-response))) + + (GET "/:application-id/raw" [] + :summary (str "Get application by `application-id`. Unlike the /api/applicaitons/:application-id endpoint, " + "the data here isn't customized for the requesting user (see schema for details). Suitable " + "for integrations and exporting applications.") + :roles #{:reporter :owner} + :path-params [application-id :- (describe s/Int "application id")] + :responses {200 {:schema ApplicationRaw} + 404 {:schema s/Str :description "Not found"}} + (if-let [app (applications/get-application application-id)] (ok app) (api-util/not-found-json-response))) @@ -260,7 +272,7 @@ :path-params [application-id :- (describe s/Int "application id")] :responses {200 {} 404 {:schema s/Str :description "Not found"}} - (if-let [app (applications/get-application (getx-user-id) application-id)] + (if-let [app (applications/get-application-for-user (getx-user-id) application-id)] (attachment/zip-attachments app) (api-util/not-found-json-response))) @@ -282,7 +294,7 @@ :roles #{:logged-in} :path-params [application-id :- (describe s/Int "application id")] :produces ["application/pdf"] - (if-let [app (applications/get-application (getx-user-id) application-id)] + (if-let [app (applications/get-application-for-user (getx-user-id) application-id)] (with-language context/*lang* #(-> app (pdf/application-to-pdf-bytes) diff --git a/src/clj/rems/api/schema.clj b/src/clj/rems/api/schema.clj index 7be2651a80..d9da16b87b 100644 --- a/src/clj/rems/api/schema.clj +++ b/src/clj/rems/api/schema.clj @@ -61,6 +61,7 @@ (s/defschema Event (assoc events/EventBase + :event/actor-attributes UserWithAttributes s/Keyword s/Any)) (s/defschema Entitlement @@ -257,6 +258,13 @@ :application/permissions Permissions :application/attachments [ApplicationAttachment]}) +(s/defschema ApplicationRaw + (-> Application + (dissoc :application/permissions + :application/roles) + (assoc :application/role-permissions {s/Keyword #{s/Keyword}} + :application/user-roles {s/Str #{s/Keyword}}))) + (s/defschema ApplicationOverview (dissoc Application :application/events diff --git a/src/clj/rems/api/services/attachment.clj b/src/clj/rems/api/services/attachment.clj index 1454113397..221e76b875 100644 --- a/src/clj/rems/api/services/attachment.clj +++ b/src/clj/rems/api/services/attachment.clj @@ -30,7 +30,7 @@ (= user-id (:attachment/user attachment)) attachment - (contains-attachment? (applications/get-application user-id (:application/id attachment)) + (contains-attachment? (applications/get-application-for-user user-id (:application/id attachment)) attachment-id) attachment @@ -38,7 +38,7 @@ (throw-forbidden)))) (defn add-application-attachment [user-id application-id file] - (let [application (applications/get-application user-id application-id)] + (let [application (applications/get-application-for-user user-id application-id)] (when-not (some (set/union commands/commands-with-comments #{:application.command/save-draft}) (:application/permissions application)) diff --git a/src/clj/rems/api/services/command.clj b/src/clj/rems/api/services/command.clj index 240bc9a94a..27447aff60 100644 --- a/src/clj/rems/api/services/command.clj +++ b/src/clj/rems/api/services/command.clj @@ -15,6 +15,7 @@ [rems.db.users :as users] [rems.db.workflow :as workflow] [rems.email.core :as email] + [rems.event-notification :as event-notification] [rems.form-validation :as form-validation] [rems.util :refer [secure-token]]) (:import rems.TryAgainException)) @@ -27,7 +28,7 @@ (defn- revokes-to-blacklist [new-events] (doseq [event new-events] (when (= :application.event/revoked (:event/type event)) - (let [application (applications/get-unrestricted-application (:application/id event))] + (let [application (applications/get-application-internal (:application/id event))] (doseq [resource (:application/resources application)] (blacklist/add-users-to-blacklist! {:users (application-util/applicant-and-members application) :resource/ext-id (:resource/ext-id resource) @@ -41,7 +42,8 @@ (email/generate-event-emails! new-events) (run-entitlements new-events) (rejecter-bot/run-rejecter-bot new-events) - (approver-bot/run-approver-bot new-events))) + (approver-bot/run-approver-bot new-events) + (event-notification/queue-notifications! new-events))) (def ^:private command-injections {:valid-user? users/user-exists? @@ -75,7 +77,7 @@ (throw (TryAgainException. e)) (throw e)))) (let [app (when-let [app-id (:application-id cmd)] - (applications/get-unrestricted-application app-id)) + (applications/get-application-internal app-id)) result (commands/handle-command cmd app command-injections)] (when-not (:errors result) (doseq [event (:events result)] diff --git a/src/clj/rems/api/services/licenses.clj b/src/clj/rems/api/services/licenses.clj index 0b555757c2..5d51d1f702 100644 --- a/src/clj/rems/api/services/licenses.clj +++ b/src/clj/rems/api/services/licenses.clj @@ -50,7 +50,7 @@ :attachment/type (:type attachment)})) (defn get-application-license-attachment [user-id application-id license-id language] - (when-let [app (applications/get-application user-id application-id)] + (when-let [app (applications/get-application-for-user user-id application-id)] (when-let [license (some #(when (= license-id (:license/id %)) %) (:application/licenses app))] (when-let [attachment-id (get-in license [:license/attachment-id language])] diff --git a/src/clj/rems/application/approver_bot.clj b/src/clj/rems/application/approver_bot.clj index 9085ea7667..1572cb4218 100644 --- a/src/clj/rems/application/approver_bot.clj +++ b/src/clj/rems/application/approver_bot.clj @@ -19,5 +19,5 @@ :comment ""}])) (defn run-approver-bot [new-events] - (doall (mapcat #(generate-commands % (applications/get-unrestricted-application (:application/id %))) + (doall (mapcat #(generate-commands % (applications/get-application (:application/id %))) new-events))) diff --git a/src/clj/rems/application/events.clj b/src/clj/rems/application/events.clj index 537012842a..0f70ad8daf 100644 --- a/src/clj/rems/application/events.clj +++ b/src/clj/rems/application/events.clj @@ -168,6 +168,9 @@ :application.event/revoked RevokedEvent :application.event/submitted SubmittedEvent}) +(def event-types + (keys event-schemas)) + (s/defschema Event (apply r/dispatch-on :event/type (flatten (seq event-schemas)))) diff --git a/src/clj/rems/application/model.clj b/src/clj/rems/application/model.clj index 02f0a8b718..2df20faed5 100644 --- a/src/clj/rems/application/model.clj +++ b/src/clj/rems/application/model.clj @@ -604,7 +604,7 @@ (defn apply-privacy [application roles] (transform [:application/forms ALL :form/fields ALL] #(apply-field-privacy % roles) application)) -(defn- hide-non-public-information [application] +(defn hide-non-public-information [application] (-> application hide-invitation-tokens ;; these are not used by the UI, so no need to expose them (especially the user IDs) diff --git a/src/clj/rems/application/rejecter_bot.clj b/src/clj/rems/application/rejecter_bot.clj index 534d2ec201..c131304f5d 100644 --- a/src/clj/rems/application/rejecter_bot.clj +++ b/src/clj/rems/application/rejecter_bot.clj @@ -19,5 +19,5 @@ :actor bot-userid}])) (defn run-rejecter-bot [new-events] - (doall (mapcat #(generate-commands % (applications/get-unrestricted-application (:application/id %))) + (doall (mapcat #(generate-commands % (applications/get-application (:application/id %))) new-events))) diff --git a/src/clj/rems/application/search.clj b/src/clj/rems/application/search.clj index 48aa91799b..b5ece3843c 100644 --- a/src/clj/rems/application/search.clj +++ b/src/clj/rems/application/search.clj @@ -104,7 +104,7 @@ (let [app-ids (distinct (map :application/id events))] (log/info "Start indexing" (count app-ids) "applications...") (doseq [app-id app-ids] - (index-application! writer (applications/get-unrestricted-application app-id))) + (index-application! writer (applications/get-application app-id))) (log/info "Finished indexing" (count app-ids) "applications"))) (.maybeRefresh searcher-manager) (swap! search-index assoc ::last-processed-event-id (:event/id (last events))))))) diff --git a/src/clj/rems/config.clj b/src/clj/rems/config.clj index a179852c1a..0cc53db0c9 100644 --- a/src/clj/rems/config.clj +++ b/src/clj/rems/config.clj @@ -8,6 +8,7 @@ [cprop.tools :refer [merge-maps]] [mount.core :refer [defstate]] [rems.application.commands :as commands] + [rems.application.events :as events] [rems.json :as json]) (:import [java.io FileNotFoundException] [org.joda.time Period])) @@ -52,8 +53,15 @@ (assert (.endsWith url "/") (str ":public-url should end with /:" (pr-str url)))) (when-let [invalid-commands (seq (remove (set commands/command-names) (:disable-commands config)))] - (log/warn "Unrecognized values in :disable-commands : " (pr-str invalid-commands)) - (log/warn "Supported-values: " (pr-str commands/command-names))) + (log/warn "Unrecognized values in :disable-commands :" (pr-str invalid-commands)) + (log/warn "Supported-values:" (pr-str commands/command-names))) + (doseq [target (:event-notification-targets config)] + (when-let [invalid-events (seq (remove (set events/event-types) (:event-types target)))] + (log/warn "Unrecognized event types in event notification target" + (pr-str target) + ":" + (pr-str invalid-events)) + (log/warn "Supported event types:" (pr-str events/event-types)))) (assert (not (empty? (:organizations config))) ":organizations can not be empty") (when-let [invalid-keys (seq (remove known-config-keys (keys config)))] diff --git a/src/clj/rems/db/applications.clj b/src/clj/rems/db/applications.clj index 8268fc94cd..f96374b8b2 100644 --- a/src/clj/rems/db/applications.clj +++ b/src/clj/rems/db/applications.clj @@ -91,7 +91,7 @@ :blacklisted? #(cache/lookup-or-miss blacklist-cache [%1 %2] (fn [[userid resource]] (blacklist/blacklisted? userid resource)))}) -(defn get-unrestricted-application +(defn get-application-internal "Returns the full application state without any user permission checks and filtering of sensitive information. Don't expose via APIs." [application-id] @@ -101,10 +101,18 @@ (model/build-application-view events fetcher-injections)))) (defn get-application + "Full application state with internal information hidden. Not personalized for any users. Suitable for public APIs." + [application-id] + (when-let [application (get-application-internal application-id)] + (-> application + (model/hide-non-public-information) + (model/apply-privacy #{:reporter})))) ;; to populate required :field/private attributes + +(defn get-application-for-user "Returns the part of application state which the specified user is allowed to see. Suitable for returning from public APIs as-is." [user-id application-id] - (when-let [application (get-unrestricted-application application-id)] + (when-let [application (get-application-internal application-id)] (or (model/apply-user-permissions application user-id) (throw-forbidden)))) @@ -144,7 +152,7 @@ (defn- group-apps-by-user [apps] (->> apps (mapcat (fn [app] - (for [user (keys (:rems.permissions/user-roles app))] + (for [user (keys (:application/user-roles app))] (when-let [app (model/apply-user-permissions app user)] [user app])))) (reduce (fn [apps-by-user [user app]] @@ -166,7 +174,7 @@ (defn- group-roles-by-user [apps] (->> apps - (mapcat (fn [app] (:rems.permissions/user-roles app))) + (mapcat (fn [app] (:application/user-roles app))) (reduce (fn [roles-by-user [user roles]] (update roles-by-user user set/union roles)) {}))) @@ -183,7 +191,7 @@ (defn- group-users-by-role [apps] (->> apps (mapcat (fn [app] - (for [[user roles] (:rems.permissions/user-roles app) + (for [[user roles] (:application/user-roles app) role roles] [user role]))) (reduce (fn [users-by-role [user role]] diff --git a/src/clj/rems/db/entitlements.clj b/src/clj/rems/db/entitlements.clj index ed29b531d2..d2770c9713 100644 --- a/src/clj/rems/db/entitlements.clj +++ b/src/clj/rems/db/entitlements.clj @@ -95,7 +95,7 @@ (defn process-outbox! [] (doseq [entry (mapv fix-entry-from-db - (outbox/get-entries {:type :entitlement-post :due-now? true}))] + (outbox/get-due-entries :entitlement-post))] ;; TODO could send multiple entitlements at once instead of one outbox entry at a time (if-let [error (post-entitlements! (:outbox/entitlement-post entry))] (let [entry (outbox/attempt-failed! entry error)] @@ -177,7 +177,7 @@ (when (seq members-to-update) (log/info "updating entitlements on application" application-id) (doseq [[userid resource-ids] entitlements-to-add] - (grant-entitlements! application-id userid resource-ids actor)) + (grant-entitlements! application-id userid resource-ids actor)) (doseq [[userid resource-ids] entitlements-to-remove] (revoke-entitlements! application-id userid resource-ids actor))))) @@ -190,5 +190,5 @@ :application.event/resources-changed :application.event/revoked} (:event/type event)) - (let [application (applications/get-unrestricted-application (:application/id event))] + (let [application (applications/get-application-internal (:application/id event))] (update-entitlements-for-application application (:event/actor event))))) diff --git a/src/clj/rems/db/outbox.clj b/src/clj/rems/db/outbox.clj index 138f728207..3d026f8d6d 100644 --- a/src/clj/rems/db/outbox.clj +++ b/src/clj/rems/db/outbox.clj @@ -11,7 +11,7 @@ (def OutboxData {(s/optional-key :outbox/id) s/Int - :outbox/type (s/enum :email :entitlement-post) + :outbox/type (s/enum :email :entitlement-post :event-notification) :outbox/backoff Duration :outbox/created DateTime :outbox/deadline DateTime @@ -19,7 +19,8 @@ :outbox/latest-attempt (s/maybe DateTime) :outbox/latest-error (s/maybe s/Str) (s/optional-key :outbox/email) s/Any - (s/optional-key :outbox/entitlement-post) s/Any}) + (s/optional-key :outbox/entitlement-post) s/Any + (s/optional-key :outbox/event-notification) s/Any}) (def ^Duration initial-backoff (Duration/standardSeconds 10)) (def ^Duration max-backoff (Duration/standardHours 12)) @@ -73,6 +74,9 @@ due-now? (filter (partial next-attempt-now? (DateTime/now))) ;; TODO move to db? type (filter #(= type (:outbox/type %)))))) +(defn get-due-entries [type] + (get-entries {:type type :due-now? true})) + (defn get-entry-by-id [id] (first (get-entries {:ids [id]}))) diff --git a/src/clj/rems/db/test_data.clj b/src/clj/rems/db/test_data.clj index 895d5bf9d8..acfe7f24e5 100644 --- a/src/clj/rems/db/test_data.clj +++ b/src/clj/rems/db/test_data.clj @@ -245,7 +245,7 @@ :time (or time (time/now))}) (defn fill-form! [{:keys [application-id actor field-value optional-fields attachment] :as command}] - (let [app (applications/get-application actor application-id)] + (let [app (applications/get-application-for-user actor application-id)] (command! (assoc (base-command command) :type :application.command/save-draft :field-values (for [form (:application/forms app) @@ -263,7 +263,7 @@ (or field-value "x"))}))))) (defn accept-licenses! [{:keys [application-id actor] :as command}] - (let [app (applications/get-application actor application-id)] + (let [app (applications/get-application-for-user actor application-id)] (command! (assoc (base-command command) :type :application.command/accept-licenses :accepted-licenses (map :license/id (:application/licenses app)))))) diff --git a/src/clj/rems/email/core.clj b/src/clj/rems/email/core.clj index c0e19e3ba8..24de45f10e 100644 --- a/src/clj/rems/email/core.clj +++ b/src/clj/rems/email/core.clj @@ -21,7 +21,7 @@ (defn- event-to-emails [event] (when-let [app-id (:application/id event)] (template/event-to-emails (rems.application.model/enrich-event event users/get-user (constantly nil)) - (applications/get-unrestricted-application app-id)))) + (applications/get-application app-id)))) (defn- enqueue-email! [email] (outbox/put! {:outbox/type :email @@ -111,7 +111,7 @@ (str "failed sending email: " e))))))) (defn try-send-emails! [] - (doseq [email (outbox/get-entries {:type :email :due-now? true})] + (doseq [email (outbox/get-due-entries :email)] (if-let [error (send-email! (:outbox/email email))] (let [email (outbox/attempt-failed! email error)] (when (not (:outbox/next-attempt email)) diff --git a/src/clj/rems/event_notification.clj b/src/clj/rems/event_notification.clj new file mode 100644 index 0000000000..f066c2a1f1 --- /dev/null +++ b/src/clj/rems/event_notification.clj @@ -0,0 +1,80 @@ +(ns rems.event-notification + (:require [clj-http.client :as http] + [clj-time.core :as time] + [clojure.test :refer :all] + [clojure.tools.logging :as log] + [mount.core :as mount] + [rems.api.schema :as schema] + [rems.config] + [rems.db.applications :as applications] + [rems.db.outbox :as outbox] + [rems.json :as json] + [rems.scheduler :as scheduler] + [rems.util :refer [getx]])) + +(def ^:private default-timeout 60) + +(defn- notify! [target body] + (log/info "Sending event notification for event" (select-keys body [:application/id :event/type :event/time]) + "to" (:url target)) + (try + (let [timeout-ms (* 1000 (get target :timeout default-timeout)) + response (http/put (getx target :url) + {:body (json/generate-string body) + :throw-exceptions false + :content-type :json + :headers (get target :headers) + :socket-timeout timeout-ms + :conn-timeout timeout-ms}) + status (:status response)] + (when-not (= 200 status) + (log/error "Event notification response status" status) + (str "failed: " status))) + (catch Exception e + (log/error "Event notification failed" e) + "failed: exception"))) + +(defn process-outbox! [] + (doseq [entry (outbox/get-due-entries :event-notification)] + (if-let [error (notify! (get-in entry [:outbox/event-notification :target]) + (get-in entry [:outbox/event-notification :body]))] + (let [entry (outbox/attempt-failed! entry error)] + (when-not (:outbox/next-attempt entry) + (log/warn "all attempts to send event notification id " (:outbox/id entry) "failed"))) + (outbox/attempt-succeeded! (:outbox/id entry))))) + +(mount/defstate event-notification-poller + :start (scheduler/start! process-outbox! (.toStandardDuration (time/seconds 10))) + :stop (scheduler/stop! event-notification-poller)) + +(defn- add-to-outbox! [target body] + (outbox/put! {:outbox/type :event-notification + :outbox/deadline (time/plus (time/now) (time/days 1)) ;; hardcoded for now + :outbox/event-notification {:target target + :body body}})) + +(defn wants? [target event] + (let [whitelist (:event-types target)] + (or (empty? whitelist) + (some? (some #{(:event/type event)} whitelist))))) + +(deftest test-wants? + (let [target {:url "whatever" :event-types [:application.event/submitted :application.event/approved]}] + (is (false? (wants? target {:event/type :application.event/created}))) + (is (true? (wants? target {:event/type :application.event/submitted}))) + (is (true? (wants? target {:event/type :application.event/approved})))) + (let [target {:url "whatever" :event-types []}] + (is (true? (wants? target {:event/type :application.event/created}))) + (is (true? (wants? target {:event/type :application.event/submitted}))) + (is (true? (wants? target {:event/type :application.event/approved}))))) + +(defn- notification-body [event] + (assoc event :event/application (applications/get-application (:application/id event)))) + +(defn queue-notifications! [events] + (when-let [targets (seq (get rems.config/env :event-notification-targets))] + (doseq [event events + :let [body (notification-body event)] + target targets + :when (wants? target event)] + (add-to-outbox! target body)))) diff --git a/src/clj/rems/permissions.clj b/src/clj/rems/permissions.clj index 6bf8142e1f..53ae9da47e 100644 --- a/src/clj/rems/permissions.clj +++ b/src/clj/rems/permissions.clj @@ -7,7 +7,7 @@ (defn- give-role-to-user [application role user] (assert (keyword? role) {:role role}) (assert (string? user) {:user user}) - (update-in application [::user-roles user] conj-set role)) + (update-in application [:application/user-roles user] conj-set role)) (defn give-role-to-users [application role users] (reduce (fn [app user] @@ -24,39 +24,39 @@ (assert (keyword? role) {:role role}) (assert (string? user) {:user user}) (-> application - (update-in [::user-roles user] disj role) - (update ::user-roles dissoc-if-empty user))) + (update-in [:application/user-roles user] disj role) + (update :application/user-roles dissoc-if-empty user))) (defn user-roles [application user] - (let [specific-roles (set (get-in application [::user-roles user]))] + (let [specific-roles (set (get-in application [:application/user-roles user]))] (if (seq specific-roles) specific-roles #{:everyone-else}))) (deftest test-user-roles (testing "give first role" - (is (= {::user-roles {"user" #{:role-1}}} + (is (= {:application/user-roles {"user" #{:role-1}}} (-> {} (give-role-to-user :role-1 "user"))))) (testing "give more roles" - (is (= {::user-roles {"user" #{:role-1 :role-2}}} + (is (= {:application/user-roles {"user" #{:role-1 :role-2}}} (-> {} (give-role-to-user :role-1 "user") (give-role-to-user :role-2 "user"))))) (testing "remove some roles" - (is (= {::user-roles {"user" #{:role-1}}} + (is (= {:application/user-roles {"user" #{:role-1}}} (-> {} (give-role-to-user :role-1 "user") (give-role-to-user :role-2 "user") (remove-role-from-user :role-2 "user"))))) (testing "remove all roles" - (is (= {::user-roles {}} + (is (= {:application/user-roles {}} (-> {} (give-role-to-user :role-1 "user") (remove-role-from-user :role-1 "user"))))) (testing "give a role to multiple users" - (is (= {::user-roles {"user-1" #{:role-1} - "user-2" #{:role-1}}} + (is (= {:application/user-roles {"user-1" #{:role-1} + "user-2" #{:role-1}}} (-> {} (give-role-to-users :role-1 ["user-1" "user-2"]))))) (testing "multiple users, get the roles of a single user" @@ -78,39 +78,39 @@ [application permission-map] (reduce (fn [application [role permissions]] (assert (keyword? role) {:role role}) - (assoc-in application [::role-permissions role] (set permissions))) + (assoc-in application [:application/role-permissions role] (set permissions))) application permission-map)) (deftest test-update-role-permissions (testing "adding" - (is (= {::role-permissions {:role #{:foo :bar}}} + (is (= {:application/role-permissions {:role #{:foo :bar}}} (-> {} (update-role-permissions {:role [:foo :bar]}))))) (testing "updating" - (is (= {::role-permissions {:role #{:gazonk}}} + (is (= {:application/role-permissions {:role #{:gazonk}}} (-> {} (update-role-permissions {:role [:foo :bar]}) (update-role-permissions {:role [:gazonk]}))))) (testing "removing" - (is (= {::role-permissions {:role #{}}} + (is (= {:application/role-permissions {:role #{}}} (-> {} (update-role-permissions {:role [:foo :bar]}) (update-role-permissions {:role []})))) - (is (= {::role-permissions {:role #{}}} + (is (= {:application/role-permissions {:role #{}}} (-> {} (update-role-permissions {:role [:foo :bar]}) (update-role-permissions {:role nil}))))) (testing "can set permissions for multiple roles" - (is (= {::role-permissions {:role-1 #{:foo} - :role-2 #{:bar}}} + (is (= {:application/role-permissions {:role-1 #{:foo} + :role-2 #{:bar}}} (-> {} (update-role-permissions {:role-1 [:foo] :role-2 [:bar]}))))) (testing "does not alter unrelated roles" - (is (= {::role-permissions {:unrelated #{:foo} - :role #{:gazonk}}} + (is (= {:application/role-permissions {:unrelated #{:foo} + :role #{:gazonk}}} (-> {} (update-role-permissions {:unrelated [:foo] :role [:bar]}) @@ -136,7 +136,7 @@ (set (map :permission rules)))))) (defn- map-permissions [application f] - (update application ::role-permissions #(map-kv-vals f %))) + (update application :application/role-permissions #(map-kv-vals f %))) (defn- permissions-for-role [rules role] (set/union (get rules role #{}) @@ -154,16 +154,16 @@ (update-role-permissions {:role-1 [:foo :bar]}) (update-role-permissions {:role-2 [:foo :bar]}))] (testing "disallow a permission for all roles" - (is (= {::role-permissions {:role-1 #{:bar} - :role-2 #{:bar}}} + (is (= {:application/role-permissions {:role-1 #{:bar} + :role-2 #{:bar}}} (blacklist app (compile-rules [{:permission :foo}]))))) (testing "disallow a permission for a single role" - (is (= {::role-permissions {:role-1 #{:bar} - :role-2 #{:foo :bar}}} + (is (= {:application/role-permissions {:role-1 #{:bar} + :role-2 #{:foo :bar}}} (blacklist app (compile-rules [{:role :role-1 :permission :foo}]))))) (testing "multiple rules" - (is (= {::role-permissions {:role-1 #{:bar} - :role-2 #{:foo}}} + (is (= {:application/role-permissions {:role-1 #{:bar} + :role-2 #{:foo}}} (blacklist app (compile-rules [{:role :role-1 :permission :foo} {:role :role-2 :permission :bar}]))))))) @@ -179,16 +179,16 @@ (update-role-permissions {:role-1 [:foo :bar]}) (update-role-permissions {:role-2 [:foo :bar]}))] (testing "allow a permission for all roles" - (is (= {::role-permissions {:role-1 #{:foo} - :role-2 #{:foo}}} + (is (= {:application/role-permissions {:role-1 #{:foo} + :role-2 #{:foo}}} (whitelist app (compile-rules [{:permission :foo}]))))) (testing "allow a permission for a single role" - (is (= {::role-permissions {:role-1 #{:foo} - :role-2 #{}}} + (is (= {:application/role-permissions {:role-1 #{:foo} + :role-2 #{}}} (whitelist app (compile-rules [{:role :role-1 :permission :foo}]))))) (testing "multiple rules" - (is (= {::role-permissions {:role-1 #{:foo} - :role-2 #{:bar}}} + (is (= {:application/role-permissions {:role-1 #{:foo} + :role-2 #{:bar}}} (whitelist app (compile-rules [{:role :role-1 :permission :foo} {:role :role-2 :permission :bar}]))))))) @@ -199,7 +199,7 @@ [application user] (->> (user-roles application user) (mapcat (fn [role] - (get-in application [::role-permissions role]))) + (get-in application [:application/role-permissions role]))) set)) (deftest test-user-permissions @@ -222,4 +222,4 @@ (user-permissions "user")))))) (defn cleanup [application] - (dissoc application ::user-roles ::role-permissions)) + (dissoc application :application/user-roles :application/role-permissions)) diff --git a/test/clj/rems/api/test_applications.clj b/test/clj/rems/api/test_applications.clj index 25032bf3e2..e01cf33812 100644 --- a/test/clj/rems/api/test_applications.clj +++ b/test/clj/rems/api/test_applications.clj @@ -1,5 +1,6 @@ (ns ^:integration rems.api.test-applications - (:require [clojure.java.io :as io] + (:require [clj-time.core :as time] + [clojure.java.io :as io] [clojure.string :as str] [clojure.test :refer :all] [rems.api.services.catalogue :as catalogue] @@ -50,7 +51,7 @@ handler read-ok-body)) -(defn- get-application [app-id user-id] +(defn- get-application-for-user [app-id user-id] (-> (request :get (str "/api/applications/" app-id)) (authenticate "42" user-id) handler @@ -174,7 +175,7 @@ :application-id application-id})))) (testing "getting application as applicant" - (let [application (get-application application-id user-id)] + (let [application (get-application-for-user application-id user-id)] (is (= "workflow/master" (get-in application [:application/workflow :workflow/type]))) (is (= ["application.event/created" "application.event/licenses-accepted" @@ -188,7 +189,7 @@ (set (get application :application/permissions)))))) (testing "getting application as handler" - (let [application (get-application application-id handler-id)] + (let [application (get-application-for-user application-id handler-id)] (is (= "workflow/master" (get-in application [:application/workflow :workflow/type]))) (is (= #{"application.command/request-review" "application.command/request-decision" @@ -210,7 +211,7 @@ (testing "disabling a command" (with-redefs [rems.config/env (assoc rems.config/env :disable-commands [:application.command/remark])] (testing "handler doesn't see hidden command" - (let [application (get-application application-id handler-id)] + (let [application (get-application-for-user application-id handler-id)] (is (= "workflow/master" (get-in application [:application/workflow :workflow/type]))) (is (= #{"application.command/request-review" "application.command/request-decision" @@ -257,7 +258,7 @@ {:type :application.command/assign-external-id :application-id application-id :external-id "abc123"}))) - (let [application (get-application application-id handler-id)] + (let [application (get-application-for-user application-id handler-id)] (is (= "abc123" (:application/external-id application))))) (testing "application can be returned" @@ -285,7 +286,7 @@ :application-id application-id :comment "What am I commenting on?"})))) (testing "review with request" - (let [eventcount (count (get (get-application application-id handler-id) :events))] + (let [eventcount (count (get (get-application-for-user application-id handler-id) :events))] (testing "requesting review" (is (= {:success true} (send-command handler-id {:type :application.command/request-review @@ -298,7 +299,7 @@ :application-id application-id :comment "Yeah, I dunno"})))) (testing "review was linked to request" - (let [application (get-application application-id handler-id) + (let [application (get-application-for-user application-id handler-id) request-event (get-in application [:application/events eventcount]) review-event (get-in application [:application/events (inc eventcount)])] (is (= (:application/request-id request-event) @@ -306,14 +307,14 @@ (testing "adding and then accepting additional licenses" (testing "add licenses" - (let [application (get-application application-id user-id)] + (let [application (get-application-for-user application-id user-id)] (is (= #{license-id1 license-id2} (license-ids-for-application application))) (is (= {:success true} (send-command handler-id {:type :application.command/add-licenses :application-id application-id :licenses [license-id4] :comment "Please approve these new terms"}))) - (let [application (get-application application-id user-id)] + (let [application (get-application-for-user application-id user-id)] (is (= #{license-id1 license-id2 license-id4} (license-ids-for-application application)))))) (testing "applicant accepts the additional licenses" (is (= {:success true} (send-command user-id @@ -322,7 +323,7 @@ :accepted-licenses [license-id4]}))))) (testing "changing resources as handler" - (let [application (get-application application-id user-id)] + (let [application (get-application-for-user application-id user-id)] (is (= #{cat-item-id2} (catalogue-item-ids-for-application application))) (is (= #{license-id1 license-id2 license-id4} (license-ids-for-application application))) (is (= {:success true} (send-command handler-id @@ -330,7 +331,7 @@ :application-id application-id :catalogue-item-ids [cat-item-id3] :comment "Here are the correct resources"}))) - (let [application (get-application application-id user-id)] + (let [application (get-application-for-user application-id user-id)] (is (= #{cat-item-id3} (catalogue-item-ids-for-application application))) ;; TODO: The previously added licenses should probably be retained in the licenses after changing resources. (is (= #{license-id3} (license-ids-for-application application)))))) @@ -340,7 +341,7 @@ {:type :application.command/change-resources :application-id application-id :catalogue-item-ids [cat-item-id2]}))) - (let [application (get-application application-id user-id)] + (let [application (get-application-for-user application-id user-id)] (is (= #{cat-item-id2} (catalogue-item-ids-for-application application))) (is (= #{license-id1 license-id2} (license-ids-for-application application))))) @@ -370,9 +371,9 @@ (is (= {:success true} (send-command handler-id {:type :application.command/approve :application-id application-id :comment ""}))) - (let [handler-data (get-application application-id handler-id) + (let [handler-data (get-application-for-user application-id handler-id) handler-event-types (map :event/type (get handler-data :application/events)) - applicant-data (get-application application-id user-id) + applicant-data (get-application-for-user application-id user-id) applicant-event-types (map :event/type (get applicant-data :application/events))] (testing "handler can see all events" (is (= {:application/id application-id @@ -437,7 +438,7 @@ (testing "creating" (is (some? application-id)) - (let [created (get-application application-id user-id)] + (let [created (get-application-for-user application-id user-id)] (is (= "application.state/draft" (get created :application/state))))) (testing "getting application as other user is forbidden" @@ -456,7 +457,7 @@ (is (= {:success true} (send-command user-id {:type :application.command/submit :application-id application-id}))) - (let [submitted (get-application application-id user-id)] + (let [submitted (get-application-for-user application-id user-id)] (is (= "application.state/submitted" (get submitted :application/state))) (is (= ["application.event/created" "application.event/submitted"] @@ -470,7 +471,7 @@ :application-id application-id :comment ""}))) (is (= "application.state/closed" - (:application/state (get-application application-id user-id)))))) + (:application/state (get-application-for-user application-id user-id)))))) (deftest test-application-submit (let [owner "owner" @@ -581,7 +582,7 @@ "application.command/save-draft" "application.command/submit" "application.command/uninvite-member"} - (set (:application/permissions (get-application app-id applicant)))))) + (set (:application/permissions (get-application-for-user app-id applicant)))))) (testing "submit" (is (= {:success true} (send-command applicant {:type :application.command/submit @@ -591,7 +592,7 @@ "application.command/copy-as-new" "application.command/remove-member" "application.command/uninvite-member"} - (set (:application/permissions (get-application app-id applicant)))))) + (set (:application/permissions (get-application-for-user app-id applicant)))))) (testing "handler's commands" (is (= #{"application.command/add-licenses" "application.command/add-member" @@ -606,7 +607,7 @@ "application.command/return" "application.command/uninvite-member" "see-everything"} - (set (:application/permissions (get-application app-id handler)))))) + (set (:application/permissions (get-application-for-user app-id handler)))))) (testing "request decision" (is (= {:success true} (send-command handler {:type :application.command/request-decision @@ -618,7 +619,7 @@ "application.command/reject" "application.command/remark" "see-everything"} - (set (:application/permissions (get-application app-id decider)))))) + (set (:application/permissions (get-application-for-user app-id decider)))))) (testing "approve" (is (= {:success true} (send-command decider {:type :application.command/approve @@ -789,7 +790,7 @@ (is (:success response)) (is (number? new-app-id)) (testing "and fetching the copied attachent" - (let [new-app (get-application new-app-id user-id) + (let [new-app (get-application-for-user new-app-id user-id) new-id (get-in new-app [:application/attachments 0 :attachment/id])] (is (number? new-id)) (is (not= id new-id)) @@ -874,7 +875,7 @@ :attachments [{:attachment/id attachment-id}]})))))) (testing "applicant can see attachment" - (let [app (get-application application-id applicant-id) + (let [app (get-application-for-user application-id applicant-id) remark-event (last (:application/events app)) attachment-id (:attachment/id (first (:event/attachments remark-event)))] (is (number? attachment-id)) @@ -957,7 +958,7 @@ {:attachment/id id2}]}))))) (testing "applicant can see the three new attachments" - (let [app (get-application application-id applicant-id) + (let [app (get-application-for-user application-id applicant-id) [close-event approve-event] (reverse (:application/events app)) [close-id1 close-id2] (map :attachment/id (:event/attachments close-event)) [approve-id] (map :attachment/id (:event/attachments approve-event))] @@ -979,7 +980,7 @@ "handler-approve.txt" "handler-close.txt" "handler-close (1).txt"] - (mapv :attachment/filename (:application/attachments (get-application application-id applicant-id)))))) + (mapv :attachment/filename (:application/attachments (get-application-for-user application-id applicant-id)))))) (testing "handler" (is (= ["handler-public-remark.txt" "reviewer-review.txt" @@ -987,7 +988,7 @@ "handler-approve.txt" "handler-close.txt" "handler-close (1).txt"] - (mapv :attachment/filename (:application/attachments (get-application application-id handler-id))))))))) + (mapv :attachment/filename (:application/attachments (get-application-for-user application-id handler-id))))))))) (deftest test-application-attachment-zip (let [api-key "42" @@ -1334,3 +1335,115 @@ app-id))) (is (contains? (get-ids (get-handled-todos decider)) app-id))))) + +(deftest test-application-raw + (let [api-key "42" + applicant "alice" + handler "handler" + reporter "reporter" + form-id (test-data/create-form! {:form/title "notifications" + :form/fields [{:field/type :text + :field/id "field-1" + :field/title {:en "text field"} + :field/optional false}]}) + workflow-id (test-data/create-workflow! {:title "wf" + :handlers [handler] + :type :workflow/default}) + ext-id "resres" + res-id (test-data/create-resource! {:resource-ext-id ext-id}) + cat-id (test-data/create-catalogue-item! {:form-id form-id + :resource-id res-id + :workflow-id workflow-id}) + app-id (test-data/create-draft! applicant [cat-id] "raw test" (time/date-time 2010))] + (test-data/create-user! {:eppn applicant :mail "alice@example.com" :commonName "Alice Applicant"}) + (test-data/create-user! {:eppn handler :mail "handler@example.com" :commonName "Hannah Handler"}) + (test-data/create-user! {:eppn reporter :mail "reporter@example.com" :commonName "Robbie Reporter"}) + (testing "applicant can't get raw application" + (is (response-is-forbidden? (api-response :get (str "/api/applications/" app-id "/raw") nil + api-key applicant)))) + (testing "reporter can get raw application" + (is (= {:application/description "" + :application/invited-members [] + :application/last-activity "2010-01-01T00:00:00.000Z" + :application/attachments [] + :application/licenses [] + :application/created "2010-01-01T00:00:00.000Z" + :application/state "application.state/draft" + :application/role-permissions + {:everyone-else ["application.command/accept-invitation"] + :member ["application.command/copy-as-new" + "application.command/accept-licenses"] + :reporter ["see-everything"] + :applicant ["application.command/copy-as-new" + "application.command/invite-member" + "application.command/submit" + "application.command/remove-member" + "application.command/accept-licenses" + "application.command/uninvite-member" + "application.command/save-draft" + "application.command/close" + "application.command/change-resources"]} + :application/modified "2010-01-01T00:00:00.000Z" + :application/user-roles {:alice ["applicant"] :handler ["handler"] :reporter ["reporter"]} + :application/external-id "2010/1" + :application/workflow {:workflow/type "workflow/default" + :workflow/id workflow-id + :workflow.dynamic/handlers + [{:email "handler@example.com" :userid "handler" :name "Hannah Handler"}]} + :application/blacklist [] + :application/id app-id + :application/todo nil + :application/applicant {:email "alice@example.com" :userid "alice" :name "Alice Applicant"} + :application/members [] + :application/resources [{:catalogue-item/start "REDACTED" + :catalogue-item/end nil + :catalogue-item/expired false + :catalogue-item/enabled true + :resource/id res-id + :catalogue-item/title {} + :catalogue-item/infourl {} + :resource/ext-id ext-id + :catalogue-item/archived false + :catalogue-item/id cat-id}] + :application/accepted-licenses {:alice []} + :application/forms [{:form/fields [{:field/value "raw test" + :field/type "text" + :field/title {:en "text field"} + :field/id "field-1" + :field/optional false + :field/visible true + :field/private false}] + :form/title "notifications" + :form/id form-id}] + :application/events [{:application/external-id "2010/1" + :event/actor-attributes {:userid "alice" :name "Alice Applicant" :email "alice@example.com"} + :application/id app-id + :event/time "2010-01-01T00:00:00.000Z" + :workflow/type "workflow/default" + :application/resources [{:catalogue-item/id cat-id :resource/ext-id ext-id}] + :application/forms [{:form/id form-id}] + :workflow/id workflow-id + :event/actor "alice" + :event/type "application.event/created" + :event/id 100 + :application/licenses []} + {:event/id 100 + :event/type "application.event/draft-saved" + :event/time "2010-01-01T00:00:00.000Z" + :event/actor "alice" + :application/id app-id + :event/actor-attributes {:userid "alice" :name "Alice Applicant" :email "alice@example.com"} + :application/field-values [{:form form-id :field "field-1" :value "raw test"}]} + {:event/id 100 + :event/type "application.event/licenses-accepted" + :event/time "2010-01-01T00:00:00.000Z" + :event/actor "alice" + :application/id app-id + :event/actor-attributes {:userid "alice" :name "Alice Applicant" :email "alice@example.com"} + :application/accepted-licenses []}]} + (-> (api-call :get (str "/api/applications/" app-id "/raw") nil + api-key reporter) + ;; start is set by the db not easy to mock + (assoc-in [:application/resources 0 :catalogue-item/start] "REDACTED") + ;; event ids are unpredictable + (update :application/events (partial map #(update % :event/id (constantly 100)))))))))) diff --git a/test/clj/rems/api/test_blacklist.clj b/test/clj/rems/api/test_blacklist.clj index 7e715d0068..f68e607cf5 100644 --- a/test/clj/rems/api/test_blacklist.clj +++ b/test/clj/rems/api/test_blacklist.clj @@ -55,7 +55,7 @@ cat-id (test-data/create-catalogue-item! {:resource-id res-id-2}) app-id (test-data/create-application! {:catalogue-item-ids [cat-id] :actor "user2"}) - get-app #(applications/get-unrestricted-application app-id)] + get-app #(applications/get-application app-id)] (testing "initially no blacklist" (is (= [] (fetch {}))) (is (= [] diff --git a/test/clj/rems/api/test_catalogue_items.clj b/test/clj/rems/api/test_catalogue_items.clj index 52c2c2b7ff..7c3d3ec88a 100644 --- a/test/clj/rems/api/test_catalogue_items.clj +++ b/test/clj/rems/api/test_catalogue_items.clj @@ -93,7 +93,7 @@ (is (:success create)) (let [app-id (test-data/create-application! {:catalogue-item-ids [id] :actor "alice"}) - get-app #(applications/get-unrestricted-application app-id)] + get-app #(applications/get-application app-id)] (is (= {:sv "http://info.se"} (:catalogue-item/infourl (first (:application/resources (get-app)))))) diff --git a/test/clj/rems/api/test_end_to_end.clj b/test/clj/rems/api/test_end_to_end.clj index bb0e04c3a2..4f8087ffba 100644 --- a/test/clj/rems/api/test_end_to_end.clj +++ b/test/clj/rems/api/test_end_to_end.clj @@ -6,6 +6,7 @@ [rems.application.rejecter-bot :as rejecter-bot] [rems.db.entitlements :as entitlements] [rems.email.core :as email] + [rems.event-notification :as event-notification] [rems.json :as json] [stub-http.core :as stub])) @@ -20,9 +21,11 @@ (deftest test-end-to-end (testing "clear poller backlog" (email/try-send-emails!) - (entitlements/process-outbox!)) + (entitlements/process-outbox!) + (event-notification/process-outbox!)) (with-open [entitlements-server (stub/start! {"/add" {:status 200} - "/remove" {:status 200}})] + "/remove" {:status 200}}) + event-server (stub/start! {"/event" {:status 200}})] ;; TODO should test emails with a mock smtp server (let [email-atom (atom [])] (with-redefs [rems.config/env (assoc rems.config/env @@ -31,7 +34,11 @@ :languages [:en] :mail-from "rems@rems.rems" :entitlements-target {:add (str (:uri entitlements-server) "/add") - :remove (str (:uri entitlements-server) "/remove")}) + :remove (str (:uri entitlements-server) "/remove")} + :event-notification-targets [{:url (str (:uri event-server) "/event") + :event-types [:application.event/created + :application.event/submitted + :application.event/approved]}]) postal.core/send-message (fn [_host message] (swap! email-atom conj message))] (let [api-key "42" owner-id "owner" @@ -267,7 +274,41 @@ (testing "fetch application as applicant" (let [application (api-call :get (str "/api/applications/" application-id) nil api-key applicant-id)] - (is (= "application.state/closed" (:application/state application))))))))))) + (is (= "application.state/closed" (:application/state application))))) + + (event-notification/process-outbox!) + + (testing "event notifications" + (let [requests (stub/recorded-requests event-server) + events (for [r requests] + (-> r + :body + (get "content") + json/parse-string + (select-keys [:application/id :event/type :event/actor + :application/resources :application/forms + :event/application]) + (update :event/application select-keys [:application/id :application/state])))] + (is (every? (comp #{"PUT"} :method) requests)) + (is (= [{:application/id application-id + :event/type "application.event/created" + :event/actor applicant-id + :application/resources [{:resource/ext-id resource-ext-id :catalogue-item/id catalogue-item-id} + {:resource/ext-id resource-ext-id2 :catalogue-item/id catalogue-item-id2}] + :application/forms [{:form/id form-id} {:form/id form-id2}] + :event/application {:application/id application-id + :application/state "application.state/draft"}} + {:application/id application-id + :event/type "application.event/submitted" + :event/actor applicant-id + :event/application {:application/id application-id + :application/state "application.state/submitted"}} + {:application/id application-id + :event/type "application.event/approved" + :event/actor handler-id + :event/application {:application/id application-id + :application/state "application.state/approved"}}] + events)))))))))) (deftest test-approver-rejecter-bots (let [api-key "42" diff --git a/test/clj/rems/api/test_over_http.clj b/test/clj/rems/api/test_over_http.clj index caf40114c8..f2922b078d 100644 --- a/test/clj/rems/api/test_over_http.clj +++ b/test/clj/rems/api/test_over_http.clj @@ -4,9 +4,12 @@ [clojure.test :refer :all] [rems.api.testing :refer [standalone-fixture]] [rems.config] - [rems.db.test-data :as test-data])) + [rems.db.test-data :as test-data] + [rems.json :as json] + [rems.event-notification :as event-notification] + [stub-http.core :as stub])) -(use-fixtures :once standalone-fixture) +(use-fixtures :each standalone-fixture) (deftest test-api-sql-timeouts (let [api-key "42" @@ -16,7 +19,7 @@ save-draft! #(-> (http/post (str (:public-url rems.config/env) "/api/applications/save-draft") {:throw-exceptions false :as :json - :headers {"x-rems-api-key" "42" + :headers {"x-rems-api-key" api-key "x-rems-user-id" user-id} :content-type :json :form-params {:application-id application-id @@ -49,3 +52,43 @@ (reset! sleep-time nil) (is (= {:status 200 :body {:success true}} (save-draft!)))))))) + +(deftest test-allocate-external-id + ;; this test mimics an external id number service + (with-open [server (stub/start! {"/" (fn [r] + (let [event (json/parse-string (get-in r [:body "content"])) + app-id (:application/id event) + response (http/post (str (:public-url rems.config/env) "/api/applications/assign-external-id") + {:as :json + :headers {"x-rems-api-key" "42" + "x-rems-user-id" "developer"} + :content-type :json + :form-params {:application-id app-id + :external-id "new-id"}})] + (assert (get-in response [:body :success]))) + {:status 200})})] + (with-redefs [rems.config/env (assoc rems.config/env + :event-notification-targets [{:url (:uri server) + :event-types [:application.event/submitted]}])] + (let [api-key "42" + applicant "alice" + cat-id (test-data/create-catalogue-item! {}) + app-id (test-data/create-draft! applicant [cat-id] "value") + get-ext-id #(-> (http/get (str (:public-url rems.config/env) "/api/applications/" app-id) + {:as :json + :headers {"x-rems-api-key" api-key + "x-rems-user-id" applicant}}) + (get-in [:body :application/external-id]))] + (event-notification/process-outbox!) + (is (empty? (stub/recorded-requests server))) + (is (not= "new-id" (get-ext-id))) + (is (-> (http/post (str (:public-url rems.config/env) "/api/applications/submit") + {:as :json + :headers {"x-rems-api-key" api-key + "x-rems-user-id" applicant} + :content-type :json + :form-params {:application-id app-id}}) + (get-in [:body :success]))) + (event-notification/process-outbox!) + (is (= 1 (count (stub/recorded-responses server)))) + (is (= "new-id" (get-ext-id))))))) diff --git a/test/clj/rems/api/test_workflows.clj b/test/clj/rems/api/test_workflows.clj index 30493ad734..a9389b9e95 100644 --- a/test/clj/rems/api/test_workflows.clj +++ b/test/clj/rems/api/test_workflows.clj @@ -166,7 +166,7 @@ (fn [app] (set (mapv :userid (get-in app [:application/workflow :workflow.dynamic/handlers]))))] (sync-with-database-time) (testing "application is initialized with the correct set of handlers" - (let [app (applications/get-unrestricted-application app-id)] + (let [app (applications/get-application app-id)] (is (= #{"handler" "carl"} (application->handler-user-ids app))))) @@ -207,7 +207,7 @@ handler)))) (testing "application is updated when handlers are changed" - (let [app (applications/get-unrestricted-application app-id)] + (let [app (applications/get-application app-id)] (is (= #{"owner" "alice"} (application->handler-user-ids app))))))) diff --git a/test/clj/rems/application/test_model.clj b/test/clj/rems/application/test_model.clj index 816f2fac75..835d17b526 100644 --- a/test/clj/rems/application/test_model.clj +++ b/test/clj/rems/application/test_model.clj @@ -263,7 +263,7 @@ events) (defn state-role-permissions [application] - (->> (:rems.permissions/role-permissions application) + (->> (:application/role-permissions application) (map (fn [[role permissions]] {:state (:application/state application) :role role @@ -1142,8 +1142,8 @@ :event/actor "handler" :application/comment "looks good" :event/actor-attributes {:userid "handler" :email "handler@example.com" :name "Handler"}}] - :rems.permissions/user-roles {"handler" #{:handler}, "reporter1" #{:reporter}} - :rems.permissions/role-permissions nil + :application/user-roles {"handler" #{:handler}, "reporter1" #{:reporter}} + :application/role-permissions nil :application/description "foo" :application/forms [{:form/id 40 :form/title "form title" diff --git a/test/clj/rems/application/test_search.clj b/test/clj/rems/application/test_search.clj index 40606d74df..3ef5880598 100644 --- a/test/clj/rems/application/test_search.clj +++ b/test/clj/rems/application/test_search.clj @@ -39,7 +39,7 @@ (testing "find by ID" (let [app-id (test-data/create-application! {:actor "alice"}) - app (applications/get-unrestricted-application app-id)] + app (applications/get-application app-id)] (is (= #{app-id} (search/find-applications (str app-id))) "app ID, any field") (is (= #{app-id} (search/find-applications (str "id:" app-id))) "app ID") (is (= #{app-id} (search/find-applications (str "id:\"" (:application/external-id app) "\""))) "external ID"))) diff --git a/test/clj/rems/db/test_csv.clj b/test/clj/rems/db/test_csv.clj index d57a398265..5676592606 100644 --- a/test/clj/rems/db/test_csv.clj +++ b/test/clj/rems/db/test_csv.clj @@ -85,8 +85,8 @@ :form-id form-id}) app-id (test-data/create-application! {:catalogue-item-ids [cat-id] :actor applicant}) - external-id (:application/external-id (applications/get-unrestricted-application app-id)) - get-application #(applications/get-unrestricted-application app-id)] + external-id (:application/external-id (applications/get-application app-id)) + get-application #(applications/get-application app-id)] (testing "draft applications not included as default" (is (= "" diff --git a/test/clj/rems/db/test_entitlements.clj b/test/clj/rems/db/test_entitlements.clj index 61cb1a55d3..000283aa96 100644 --- a/test/clj/rems/db/test_entitlements.clj +++ b/test/clj/rems/db/test_entitlements.clj @@ -157,7 +157,7 @@ :accepted-licenses [lic-id1]}) ; only accept some licenses (is (= {applicant #{lic-id1 lic-id2} member #{lic-id1}} - (:application/accepted-licenses (applications/get-unrestricted-application app-id)))) + (:application/accepted-licenses (applications/get-application app-id)))) (entitlements/process-outbox!) diff --git a/test/clj/rems/test_db.clj b/test/clj/rems/test_db.clj index 2c199fc592..8827bb1b0b 100644 --- a/test/clj/rems/test_db.clj +++ b/test/clj/rems/test_db.clj @@ -49,7 +49,7 @@ :application-id app-id :actor "handler" :comment ""}) - (is (= :application.state/approved (:application/state (applications/get-application applicant app-id)))) + (is (= :application.state/approved (:application/state (applications/get-application-for-user applicant app-id)))) (is (= ["resid111" "resid222"] (sort (map :resid (db/get-entitlements {:application app-id})))) "should create entitlements for both resources"))) diff --git a/test/clj/rems/test_event_notification.clj b/test/clj/rems/test_event_notification.clj new file mode 100644 index 0000000000..93a6149ef3 --- /dev/null +++ b/test/clj/rems/test_event_notification.clj @@ -0,0 +1,116 @@ +(ns ^:integration rems.test-event-notification + (:require [clj-time.core :as time] + [clojure.test :refer :all] + [medley.core :refer [dissoc-in]] + [rems.config] + [rems.api.services.command :as command] + [rems.api.testing :refer [api-fixture api-call]] + [rems.db.test-data :as test-data] + [rems.event-notification :as event-notification] + [rems.json :as json] + [stub-http.core :as stub])) + +(use-fixtures + :once + api-fixture) + +(deftest test-notify! + (with-open [server (stub/start! {"/ok" {:status 200} + "/broken" {:status 500} + "/timeout" {:status 200 :delay 5000}})] + (let [body {:value 1}] + (testing "success" + (is (nil? (#'event-notification/notify! {:url (str (:uri server) "/ok") + :headers {"additional-header" "value"}} + body))) + (let [[req & more] (stub/recorded-requests server)] + (is (empty? more)) + (is (= {:method "PUT" + :path "/ok" + :body {"content" (json/generate-string body)}} + (select-keys req [:method :path :body]))) + (is (= "value" (get-in req [:headers :additional-header]))) + )) + (testing "error code" + (is (= "failed: 500" (#'event-notification/notify! {:url (str (:uri server) "/broken")} + body)))) + (testing "timeout" + (is (= "failed: exception" (#'event-notification/notify! {:url (str (:uri server) "/timeout") + :timeout 1} + body)))) + (testing "invalid url" + (is (= "failed: exception" (#'event-notification/notify! {:url "http://invalid/lol"} + body))))))) + +(deftest test-event-notification + ;; this is an integration test from commands to notifications + (with-open [server (stub/start! {"/created" {:status 200} + "/all" {:status 200}})] + (with-redefs [rems.config/env (assoc rems.config/env + :event-notification-targets [{:url (str (:uri server) "/created") + :event-types [:application.event/created]} + {:url (str (:uri server) "/all")}])] + (let [get-notifications #(doall + (for [r (stub/recorded-requests server)] + {:path (:path r) + :data (-> r + :body + (get "content") + json/parse-string)})) + form-id (test-data/create-form! {:form/title "notifications" + :form/fields [{:field/type :text + :field/id "field-1" + :field/title {:en "text field"} + :field/optional false}]}) + handler "handler" + workflow-id (test-data/create-workflow! {:title "wf" + :handlers [handler] + :type :workflow/default}) + ext-id "resres" + res-id (test-data/create-resource! {:resource-ext-id ext-id}) + cat-id (test-data/create-catalogue-item! {:form-id form-id + :resource-id res-id + :workflow-id workflow-id}) + applicant "alice" + app-id (:application-id (command/command! {:type :application.command/create + :actor applicant + :time (time/date-time 2001) + :catalogue-item-ids [cat-id]}))] + (testing "no notifications before outbox is processed" + (is (empty? (stub/recorded-requests server)))) + (event-notification/process-outbox!) + (testing "created event gets sent to both endpoints" + (let [notifications (get-notifications) + app-from-raw-api (api-call :get (str "/api/applications/" app-id "/raw") nil + "42" "reporter")] + (is (= 2 (count notifications))) + (is (= #{"/created" "/all"} + (set (map :path notifications)))) + (is (= {:application/external-id "2001/1" + :application/id app-id + :event/time "2001-01-01T00:00:00.000Z" + :workflow/type "workflow/default" + :application/resources [{:resource/ext-id ext-id + :catalogue-item/id cat-id}] + :application/forms [{:form/id form-id}] + :workflow/id workflow-id + :event/actor applicant + :event/type "application.event/created" + :application/licenses [] + :event/application app-from-raw-api} + (:data (first notifications)))) + (is (= (:data (first notifications)) + (:data (second notifications)))))) + (command/command! {:application-id app-id + :type :application.command/save-draft + :actor applicant + :time (time/date-time 2001) + :field-values [{:form form-id :field "field-1" :value "my value"}]}) + (event-notification/process-outbox!) + (testing "draft-saved event gets sent only to /all" + (let [requests (get-notifications) + req (last requests)] + (is (= 3 (count requests))) + (is (= "/all" (:path req))) + (is (= "application.event/draft-saved" + (:event/type (:data req)))))))))) diff --git a/test/clj/rems/test_pdf.clj b/test/clj/rems/test_pdf.clj index 9ab731d5a0..d08e1adc1e 100644 --- a/test/clj/rems/test_pdf.clj +++ b/test/clj/rems/test_pdf.clj @@ -146,11 +146,11 @@ (fn [] (with-fixed-time (time/date-time 2010) (fn [] - (#'pdf/render-application (applications/get-application handler application-id))))))))) + (#'pdf/render-application (applications/get-application-for-user handler application-id))))))))) (testing "pdf rendering succeeds" (is (some? (with-language :en #(do ;; uncomment this to get a pdf file to look at - #_(pdf/application-to-pdf (applications/get-application handler application-id) "/tmp/example-application.pdf") - (pdf/application-to-pdf-bytes (applications/get-application handler application-id))))))))) + #_(pdf/application-to-pdf (applications/get-application-for-user handler application-id) "/tmp/example-application.pdf") + (pdf/application-to-pdf-bytes (applications/get-application-for-user handler application-id))))))))) diff --git a/test/clj/rems/test_performance.clj b/test/clj/rems/test_performance.clj index 2d39cab7eb..a3d050aa76 100644 --- a/test/clj/rems/test_performance.clj +++ b/test/clj/rems/test_performance.clj @@ -79,7 +79,7 @@ (println "cache size" (mm/measure applications/all-applications-cache)))) (defn benchmark-get-application [] - (let [test-get-application #(applications/get-application "developer" 12)] + (let [test-get-application #(applications/get-application-for-user "developer" 12)] (run-benchmarks [{:name "get-application" :benchmark test-get-application}])))