diff --git a/CHANGELOG.md b/CHANGELOG.md index c49babe..b637393 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,18 @@ file. This change log follows the conventions of ## [Unreleased] +## [0.4.0] - 2023-06-03 +### Added +- `update` command to update rotation's list of mentions and + description. Watch out! The command overrides current on-call person + even if the new list of mentions is the same as in original + rotation. Consider the command to be a shortcut to a sequence of + `delete` and `create` commands for the same rotation name. + ([#66](https://github.com/pilosus/dienstplan/issues/66)) + +### Changed +- Local development migrated to `docker-compose` v2. + ## [0.3.0] - 2022-10-30 ### Changed - Base docker images for production and testing moved to Linux @@ -162,7 +174,8 @@ file. This change log follows the conventions of ### Added - Bot app MVP -[Unreleased]: https://github.com/pilosus/dienstplan/compare/0.3.0...HEAD +[Unreleased]: https://github.com/pilosus/dienstplan/compare/0.4.0...HEAD +[0.4.0]: https://github.com/pilosus/dienstplan/compare/0.3.0...0.4.0 [0.3.0]: https://github.com/pilosus/dienstplan/compare/0.2.12...0.3.0 [0.2.12]: https://github.com/pilosus/dienstplan/compare/0.2.11...0.2.12 [0.2.11]: https://github.com/pilosus/dienstplan/compare/0.2.10...0.2.11 diff --git a/Dockerfile b/Dockerfile index 9f7a161..374d6da 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ ### Build stage ### ################### -FROM clojure:temurin-17-lein-alpine@sha256:37968e7afb62937499c3773e9b713400da4abb358e6beb0ec9bae41a59715111 AS build +FROM clojure:temurin-17-lein-alpine AS build # Create a working directory RUN mkdir -p /usr/src/app @@ -28,7 +28,7 @@ RUN mv "$(lein uberjar | sed -n 's/^Created \(.*standalone\.jar\)/\1/p')" app.ja ### Run stage ### ################# -FROM eclipse-temurin:17-jre-alpine@sha256:e1506ba20f0cb2af6f23e24c7f8855b417f0b085708acd9b85344a884ba77767 AS run +FROM eclipse-temurin:17-jre-alpine AS run # Create app directory for unpriviledged user RUN mkdir -p /usr/src/app diff --git a/Dockerfile-test b/Dockerfile-test index a950784..55276f2 100644 --- a/Dockerfile-test +++ b/Dockerfile-test @@ -1,4 +1,4 @@ -FROM clojure:temurin-17-lein-alpine@sha256:37968e7afb62937499c3773e9b713400da4abb358e6beb0ec9bae41a59715111 +FROM clojure:temurin-17-lein-alpine RUN mkdir -p /usr/src/app COPY project.clj /usr/src/app/ WORKDIR /usr/src/app diff --git a/Makefile b/Makefile index b7049df..53d0e6c 100644 --- a/Makefile +++ b/Makefile @@ -3,25 +3,25 @@ all: build up migrate lint test build: - docker-compose build + docker compose build up: - docker-compose up -d + docker compose up -d down: - docker-compose down + docker compose down lint: - docker-compose run --rm dienstplan lein cljfmt fix + docker compose run --rm dienstplan lein cljfmt fix test: - docker-compose run --rm --no-deps dienstplan lein test ${TEST_ARGS} + docker compose run --rm --no-deps dienstplan lein test ${TEST_ARGS} cloverage: - docker-compose run --rm dienstplan lein cloverage ${TEST_ARGS} + docker compose run --rm dienstplan lein cloverage ${TEST_ARGS} migrate: - docker-compose run --rm --no-deps dienstplan lein run --mode migrate + docker compose run --rm --no-deps dienstplan lein run --mode migrate rollback: - docker-compose run --rm --no-deps dienstplan lein run --mode rollback + docker compose run --rm --no-deps dienstplan lein run --mode rollback diff --git a/README.md b/README.md index 33be4ce..3a806b7 100644 --- a/README.md +++ b/README.md @@ -112,12 +112,17 @@ Commands: @dienstplan delete ``` -8. List channel's rotations +8. Update a rotation +``` +@dienstplan update +``` + +9. List channel's rotations ``` @dienstplan list ``` -9. Show a help message +10. Show a help message ``` @dienstplan help ``` @@ -127,8 +132,8 @@ Commands: ### Server requirements -- Any server with Java 17 or higher (tested with [Eclipse Temurin 17.0.4.1](https://whichjdk.com/#adoptium-eclipse-temurin)) -- PostgreSQL 9.4 to 14.5 (as of 2022-10-29 PostgreSQL 15 is not tested yet) +- Any server with Java 17 or higher +- PostgreSQL 9.4 to 15.3 - (Optionally) [Sentry account](https://sentry.io/) for error tracking ### Environment variables diff --git a/docker-compose.yml b/docker-compose.yml index 57ea0fb..71fb4df 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,9 @@ version: "3.9" services: postgres: - image: postgres:14.5-alpine3.16@sha256:db802f226b620fc0b8adbeca7859eb203c8d3c9ce5d84870fadee05dea8f50ce + image: postgres:15.3-alpine3.18 + ports: + - "15432:5432" volumes: - "/tmp/diestplan-db-data:/var/lib/postgresql/data" env_file: diff --git a/project.clj b/project.clj index 79e8f52..aaf0c64 100644 --- a/project.clj +++ b/project.clj @@ -1,50 +1,56 @@ -(defproject dienstplan "0.3.0" +(defproject dienstplan "0.4.0" :description "Duty rotation slack bot" :url "https://github.com/pilosus/dienstplan" :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0" :url "https://www.eclipse.org/legal/epl-2.0/"} :dependencies [ ;; Clojure - [org.clojure/clojure "1.10.3"] - [org.clojure/tools.cli "1.0.206"] + [org.clojure/clojure "1.11.1"] + [org.clojure/tools.cli "1.0.219"] ;; Web Framework - [ring/ring-core "1.9.4"] - [ring/ring-jetty-adapter "1.9.4"] + [ring/ring-core "1.10.0"] + [ring/ring-jetty-adapter "1.10.0"] [ring/ring-json "0.5.1"] ;; Routing [bidi "2.1.6"] ;; Logging - [org.clojure/tools.logging "1.2.1"] - [ch.qos.logback/logback-classic "1.2.6"] + [org.clojure/tools.logging "1.2.4"] + [ch.qos.logback/logback-classic "1.4.7"] ;; Alerts - [io.sentry/sentry-clj "5.2.158"] + ;; FIXME version 6 and its underlying Java SDK seem to rely on jdk.unsupported + ;; that aren't in temurin JRE 17 + [io.sentry/sentry-clj "5.7.180"] + ;; In sentry-clj 5.5.165 dsn string must be non-blank + ;; in the presense of context map: + ;; https://github.com/getsentry/sentry-clj/compare/5.5.164...5.5.165#diff-e3b4ff05b98b80c7fa2082652c8510acf99a4bbf293d3e035b703b9720f4ca90R253 + ;; [io.sentry/sentry-clj "5.5.164"] ;; Validation - [expound "0.8.10"] + [expound "0.9.0"] ;; Config managements [exoscale/yummy "0.2.11"] - [mount "0.1.16"] + [mount "0.1.17"] ;; HTTP client [clj-http "3.12.3"] ;; JSON parsing - [cheshire "5.10.1"] + [cheshire "5.11.0"] ;; DB [org.clojure/java.jdbc "0.7.12"] - [org.postgresql/postgresql "42.2.20.jre7"] - [hikari-cp "2.13.0"] - [dev.weavejester/ragtime "0.9.0"] - [com.github.seancorfield/honeysql "2.2.861"] + [org.postgresql/postgresql "42.6.0"] + [hikari-cp "3.0.1"] + [dev.weavejester/ragtime "0.9.3"] + [com.github.seancorfield/honeysql "2.4.1026"] ;; Cryptography - [buddy/buddy-core "1.10.1"]] + [buddy/buddy-core "1.11.418"]] :plugins [[lein-cljfmt "0.8.0"] [lein-cloverage "1.2.2"] [lein-licenses "0.2.2"] diff --git a/src/dienstplan/commands.clj b/src/dienstplan/commands.clj index 7547f20..05e2151 100644 --- a/src/dienstplan/commands.clj +++ b/src/dienstplan/commands.clj @@ -73,12 +73,17 @@ Commands: @dienstplan delete ``` -8. List channel's rotations +8. Update a rotation +``` +@dienstplan update +``` + +9. List channel's rotations ``` @dienstplan list ``` -9. Show a help message +10. Show a help message ``` @dienstplan help ``` @@ -127,6 +132,17 @@ On-call engineer's duties: - Follow the boy scout rule: always leave the campground cleaner than you found it ```") +(def help-cmd-update + "Usage: +``` +@dienstplan update +``` + +Example: +``` +@dienstplan update my-rota @user1 @user2 @user3 My updated description of the existing rota +```") + (def help-cmd-rotate "Usage: ``` @@ -214,8 +230,10 @@ Example: #"^[^<@]*(?s)(?<@[^>]+>)[\u00A0|\u2007|\u202F|\s]+(?\w+)[\u00A0|\u2007|\u202F|\s]*(?.*)") (def commands->data - {:create {:spec ::spec/bot-cmd-create + {:create {:spec ::spec/bot-cmd-create-or-update :help help-cmd-create} + :update {:spec ::spec/bot-cmd-create-or-update + :help help-cmd-update} :rotate {:spec ::spec/bot-cmd-default :help help-cmd-rotate} :assign {:spec ::spec/bot-cmd-assign @@ -350,19 +368,29 @@ Example: "Parse command arguments for app mention" (fn [command-parsed] (get command-parsed :command))) -(defmethod parse-args :create [command-parsed] +(defn parse-args-create-or-update-cmd + [command-parsed] (let [args (get-command-args command-parsed) splitted (string/split args regex-user-mention) rotation (-> (first splitted) str-trim nilify) - description (->> (last splitted) str-trim) - users (parse-user-mentions args)] + users (parse-user-mentions args) + ;; without user mentions description will be erroneously + ;; matched against rota name. + ;; description without mentions doesn't make any sense + description (when users (->> (last splitted) str-trim))] {:rotation rotation :users users :description description})) +(defmethod parse-args :create [command-parsed] + (parse-args-create-or-update-cmd command-parsed)) + +(defmethod parse-args :update [command-parsed] + (parse-args-create-or-update-cmd command-parsed)) + (defmethod parse-args :assign [command-parsed] (let [args (get-command-args command-parsed) splitted (string/split args regex-user-mention) @@ -442,7 +470,7 @@ Example: (defmethod command-exec! :shout [command-map] (let [{:keys [channel rotation]} (get-channel-rotation command-map) rota (first (db/duty-get channel rotation)) - {:keys [duty description]} rota + {:keys [duty]} rota text (or duty @@ -512,6 +540,30 @@ Example: rotation channel-formatted))] result)) +(defmethod command-exec! :update [command-map] + (let [now (get-now-ts) + {:keys [channel rotation]} (get-channel-rotation command-map) + channel-formatted (slack-mention-channel channel) + users (get-in command-map [:args :users]) + mentions (users->mention-table-rows users) + rota-params {:channel channel + :name rotation + :description (get-in command-map [:args :description]) + :updated_on now} + updated (db/rota-update! {:rota rota-params :mention mentions}) + error-msg (get-in updated [:error :message]) + result + (if (:ok updated) + (format + "Rotation `%s` for channel %s updated successfully" + rotation channel-formatted) + (do + (log/error error-msg) + (format + "Cannot update rotation `%s` for channel %s: %s" + rotation channel-formatted error-msg)))] + result)) + (defmethod command-exec! :list [command-map] (let [{:keys [channel _]} (get-channel-rotation command-map) channel-formatted (slack-mention-channel channel) diff --git a/src/dienstplan/core.clj b/src/dienstplan/core.clj index bb96813..a7cacd5 100644 --- a/src/dienstplan/core.clj +++ b/src/dienstplan/core.clj @@ -71,7 +71,8 @@ app-name (get-in config [:application :name]) version (get-in config [:application :version]) release (format "%s:%s" app-name version)] - (sentry/init! dsn {:environment env :debug debug :release release})) + (when (not debug) + (sentry/init! dsn {:environment env :debug debug :release release}))) :stop (sentry/close!)) (defstate server diff --git a/src/dienstplan/db.clj b/src/dienstplan/db.clj index 66e6533..3e307c5 100644 --- a/src/dienstplan/db.clj +++ b/src/dienstplan/db.clj @@ -169,6 +169,32 @@ error)))] result))) +(defn rota-update! + [params] + (jdbc/with-db-transaction [conn db] + (try + (let + [rota + (jdbc/query + conn + (sql/format + {:select [[:r/id :rota_id]] + :from [[:rota :r]] + :where [:and + [:= :r/channel (get-in params [:rota :channel])] + [:= :r/name (get-in params [:rota :name])]] + :for [:update]} + sql-params)) + rota-id (-> rota first :rota_id) + mention-params (map #(assoc % :rota_id rota-id) (:mention params))] + (when (not rota-id) (throw (ex-info "Rotation not found" {}))) + (jdbc/update! conn :rota (:rota params) ["id = ?" rota-id]) + (jdbc/delete! db :mention ["rota_id = ?" rota-id]) + (jdbc/insert-multi! conn :mention mention-params) + {:ok true}) + (catch Exception e + {:ok false :error {:message (.getMessage e)}})))) + (defn- assign [users next-duty] (if (not (some #{next-duty} users)) diff --git a/src/dienstplan/spec.clj b/src/dienstplan/spec.clj index f1ef4b6..7e381ed 100644 --- a/src/dienstplan/spec.clj +++ b/src/dienstplan/spec.clj @@ -185,6 +185,7 @@ (s/def :bot-cmd-args/user ::non-empty-str) (s/def :bot-cmd-args/description ::nillable-str) (s/def :bot-cmd-args/users (s/nilable (s/+ ::non-empty-str))) +(s/def :bot-cmd-args-non-empty/users (s/+ ::non-empty-str)) (s/def :bot-cmd-default/args (s/keys @@ -204,11 +205,11 @@ [:bot-cmd-common/context :bot-cmd-common/command])) -(s/def :bot-cmd-create/args +(s/def :bot-cmd-create-or-update/args (s/keys :req-un [:bot-cmd-args/rotation - :bot-cmd-args/users + :bot-cmd-args-non-empty/users :bot-cmd-args/description])) (s/def :bot-cmd-assign/args @@ -217,12 +218,12 @@ [:bot-cmd-args/rotation :bot-cmd-args/user])) -(s/def ::bot-cmd-create +(s/def ::bot-cmd-create-or-update (s/keys :req-un [:bot-cmd-common/context :bot-cmd-common/command - :bot-cmd-create/args])) + :bot-cmd-create-or-update/args])) (s/def ::bot-cmd-assign (s/keys diff --git a/test/dienstplan/api_test.clj b/test/dienstplan/api_test.clj index b99457f..9a4326d 100644 --- a/test/dienstplan/api_test.clj +++ b/test/dienstplan/api_test.clj @@ -18,58 +18,14 @@ (:require [cheshire.core :as json] [clj-http.client :as http] - [clojure.java.jdbc :as jdbc] [clojure.test :refer [deftest is testing use-fixtures]] - [clojure.tools.logging :as log] - [dienstplan.config :refer [config]] [dienstplan.core] [dienstplan.commands :as cmd] [dienstplan.db] - [dienstplan.slack :as slack] - [hikari-cp.core :as cp] - [mount.core :as mount :refer [defstate]])) + [dienstplan.fixtures-test :as fix])) -;; Fixtures - -(defn fix-run-server - [test] - (log/info "[fix-run-server] start") - (mount/start) - (test) - (mount/stop) - (log/info "[fix-run-server] stop")) - -(defn fix-mock-slack-api-request - [test] - (with-redefs - [slack/slack-api-request (constantly {:ok? true :status 200 :date nil})] - (test))) - -(defstate db-test - "Separate DB component with transaction rollback for tests" - :start - (let [db-opts (:db config) - datasource (cp/make-datasource db-opts)] - {:datasource datasource}) - :stop - (-> db-test :datasource cp/close-datasource)) - -(defn fix-db-rollback - [test] - (log/info "[fix-db-rollback] start") - (mount/start #'db-test) - (jdbc/with-db-transaction [txn db-test] - (jdbc/db-set-rollback-only! txn) - (-> (mount/only [#'dienstplan.db/db]) - (mount/swap {#'dienstplan.db/db txn}) - (mount/start)) - (test) - (mount/stop #'dienstplan.db/db)) - (mount/stop #'db-test) - (log/info "[fix-db-rollback] stop")) - -(use-fixtures :once fix-run-server fix-mock-slack-api-request) -(use-fixtures :each fix-db-rollback) +(use-fixtures :once fix/fix-run-server fix/fix-mock-slack-api-request) +(use-fixtures :each fix/fix-db-rollback) ;; Tests @@ -397,6 +353,63 @@ :body (json/parse-string true))))))) +(deftest test-events-update-existing-rota + (testing "Create and update the rota" + (let [create-request + (merge + events-request-base + {:body (json/generate-string + {:event + {:text "<@U001> create my-rota <@U123> <@U456> <@U789> Test description" + :ts "1640250011.000100" + :team "T123" + :channel "C123"}})}) + _ (http/request create-request) + who-created-response + (http/request + (merge + events-request-base + {:body + (json/generate-string + {:event + {:text "<@U001> who my-rota" + :ts "1640250011.000100" + :team "T123" + :channel "C123"}})})) + update-request + (merge + events-request-base + {:body (json/generate-string + {:event + {:text "<@U001> update my-rota <@U456> <@U789> <@U321> New description" + :ts "1640250011.000100" + :team "T123" + :channel "C123"}})}) + _ (http/request update-request) + who-updated-response + (http/request + (merge + events-request-base + {:body + (json/generate-string + {:event + {:text "<@U001> who my-rota" + :ts "1640250011.000100" + :team "T123" + :channel "C123"}})}))] + (is (= + {:channel "C123" + :text "Hey <@U123>, you are an on-call person for `my-rota` rotation.\nTest description"} + (-> who-created-response + :body + (json/parse-string true)))) + (is (= + {:channel "C123" + :text "Hey <@U456>, you are an on-call person for `my-rota` rotation.\nNew description"} + (-> who-updated-response + :body + (json/parse-string true))))))) + (deftest test-api-unhandled-exception (testing "API unhandled exception" (with-redefs diff --git a/test/dienstplan/commands_test.clj b/test/dienstplan/commands_test.clj index 8895b5d..df00ec0 100644 --- a/test/dienstplan/commands_test.clj +++ b/test/dienstplan/commands_test.clj @@ -43,6 +43,9 @@ ["<@U001>\u00a0create backend rotation\u00a0<@U123>\u00a0<@U456>\u00a0<@U789>\u00a0On-call backend engineer's duty:\n- Check support questions\n- Check alerts\n- Check metrics" {:command :create :rest "backend rotation\u00a0<@U123>\u00a0<@U456>\u00a0<@U789>\u00a0On-call backend engineer's duty:\n- Check support questions\n- Check alerts\n- Check metrics"} "Unicode whitespaces"] + ["<@U02HXENLLPN> create rota" + {:command :create :rest "rota"} + "Create command with no mentions and no description"] [" <@U123> rotate " {:command :rotate :rest nil} "No args parsed"] @@ -76,49 +79,70 @@ (is (= expected (cmd/parse-user-mentions text))))))) (def params-parse-args - [[{:user-id "U02HXENLLPN" :command :create :rest "backend-rota <@U1KF3FG75> <@U01NT7XLST0> <@U01P02NDVSN>\nOn-call backend engineer's duty \n- Check <#C02PJGR5LLB>\n- Check Sentry alerts\n- Check Grafana metrics"} + [["<@U02HXENLLPN> create backend-rota <@U1KF3FG75> <@U01NT7XLST0> <@U01P02NDVSN>\nOn-call backend engineer's duty \n- Check <#C02PJGR5LLB>\n- Check Sentry alerts\n- Check Grafana metrics" {:rotation "backend-rota" :users ["<@U1KF3FG75>" "<@U01NT7XLST0>" "<@U01P02NDVSN>"] :description "On-call backend engineer's duty \n- Check <#C02PJGR5LLB>\n- Check Sentry alerts\n- Check Grafana metrics"} "Create"] - [{:command :create :rest "backend rotation\u00a0<@U123>\u00a0<@U456>\u00a0<@U789>\u00a0On-call backend engineer's duty:\n- Check support questions\n- Check alerts\n- Check metrics"} + ["<@U02HXENLLPN> update backend-rota <@U01NT7XLST0> <@U01P02NDVSN>\nBrand new description" + {:rotation "backend-rota" + :users ["<@U01NT7XLST0>" "<@U01P02NDVSN>"] + :description "Brand new description"} + "Update"] + ["<@U02HXENLLPN> update backend-rota <@U01NT7XLST0> <@U01P02NDVSN> " + {:rotation "backend-rota" + :users ["<@U01NT7XLST0>" "<@U01P02NDVSN>"] + :description ""} + "Update with missing description"] + ["<@U02HXENLLPN> update backend-rota description" + {:rotation "backend-rota description" + :users nil + :description nil} + "Update with missing mentions, rota name detection fails"] + ["<@U02HXENLLPN> update rota" + {:rotation "rota" + :users nil + :description nil} + "Update with missing mentions and description"] + ["<@U02HXENLLPN> create backend rotation\u00a0<@U123>\u00a0<@U456>\u00a0<@U789>\u00a0On-call backend engineer's duty:\n- Check support questions\n- Check alerts\n- Check metrics" {:rotation "backend rotation" :users ["<@U123>" "<@U456>" "<@U789>"] :description "On-call backend engineer's duty:\n- Check support questions\n- Check alerts\n- Check metrics"} "Unicode whitespaces"] - [{:user-id "U123" :command :help :rest nil} + ["<@U02HXENLLPN> help" {:description cmd/help-msg} "Help"] - [{:user-id "U123" :command :delete :rest " backend-rota "} + ["<@U02HXENLLPN> delete backend-rota " {:rotation "backend-rota"} "Delete"] - [{:user-id "U123" :command :about :rest " backend-rota "} + ["<@U02HXENLLPN> about backend-rota " {:rotation "backend-rota"} "About"] - [{:user-id "U123" :command :rotate :rest " backend-rota "} + ["<@U02HXENLLPN> rotate backend-rota " {:rotation "backend-rota"} "Rotate"] - [{:user-id "U123" :command :assign :rest " backend-rota <@U456>"} + ["<@U02HXENLLPN> assign backend-rota <@U456>" {:rotation "backend-rota" :user "<@U456>"} "Assign"] - [{:user-id "U123" :command :assign :rest "<@U456>"} + ["<@U02HXENLLPN> assign <@U456>" {:rotation nil :user "<@U456>"} "Assign with no rota name"] - [{:user-id "U123" :command :who :rest " backend-rota "} + ["<@U02HXENLLPN> who backend-rota " {:rotation "backend-rota"} "Who"] - [{:user-id "U123" :command :shout :rest " backend-rota "} + ["<@U02HXENLLPN> shout backend-rota " {:rotation "backend-rota"} "Shout"] - [{:user-id "U123" :command :something :rest " backend-rota "} + ["<@U02HXENLLPN> something backend-rota " nil "Unrecognized command"]]) (deftest test-parse-args (testing "Parse command arguments" - (doseq [[args expected description] params-parse-args] + (doseq [[message expected description] params-parse-args] (testing description - (is (= expected (cmd/parse-args args))))))) + (let [parsed (-> message cmd/parse-command cmd/parse-args)] + (is (= expected parsed))))))) (def request-app-mention {:request-id "1469a826-8221-498d-a8ac-39621f36a9c6" @@ -252,6 +276,33 @@ :description "test"} :error cmd/help-cmd-create} "Create with no rotation name"] + [{:params {:event {:text "<@U001> create backend-rota <@U123> <@U456>" + :ts "1640250011.000100" + :team "T123" + :channel "C123"}}} + {:context + {:ts "1640250011.000100" + :team "T123" + :channel "C123"} + :command :create + :args {:rotation "backend-rota" + :users ["<@U123>" "<@U456>"] + :description ""}} + "Create with no description"] + [{:params {:event {:text "<@U001> create rota" + :ts "1640250011.000100" + :team "T123" + :channel "C123"}}} + {:context + {:ts "1640250011.000100" + :team "T123" + :channel "C123"} + :command :create + :args {:rotation "rota" + :users nil + :description nil} + :error cmd/help-cmd-create} + "Create with no user mentions and description"] [{:params {:event {:text " <@UNX01> who backend-rota" :ts "1640250011.000100" :team "T123" diff --git a/test/dienstplan/db_test.clj b/test/dienstplan/db_test.clj index 4ba875f..a08b7b1 100644 --- a/test/dienstplan/db_test.clj +++ b/test/dienstplan/db_test.clj @@ -15,8 +15,12 @@ (ns dienstplan.db-test (:require - [clojure.test :refer [deftest is testing]] - [dienstplan.db :as db])) + [clojure.test :refer [deftest is testing use-fixtures]] + [dienstplan.db :as db] + [dienstplan.fixtures-test :as fix])) + +(use-fixtures :once fix/fix-run-server) +(use-fixtures :each fix/fix-db-rollback) (def params-rotate-users [[[] [] "Empty list"] @@ -158,3 +162,45 @@ (doseq [[users expected description] params-get-duty-user-name] (testing description (is (= expected (db/get-duty-user-name users))))))) + +(def rota-channel "my-channel") +(def rota-name "my-rota") +(def timestamp-1 (java.sql.Timestamp. 1495636054438)) +(def timestamp-2 (java.sql.Timestamp. 1595636054438)) + +(def param-rota-1 + {:rota + {:channel rota-channel + :name rota-name + :description "my-description-1" + :created_on timestamp-1 + :updated_on timestamp-1} + :mention [{:name "user-a", :duty false} + {:name "user-b", :duty true} + {:name "user-c", :duty false}]}) + +(def param-rota-2 + {:rota + {:channel rota-channel + :name rota-name + :description "my-description-2" + :created_on timestamp-2 + :updated_on timestamp-2} + :mention [{:name "user-x", :duty true} + {:name "user-y", :duty false}]}) + +(def params-rota-update! + [[param-rota-1 + param-rota-2 + {:description "my-description-2" + :duty "user-x"} + "Description and mentions updated"]]) + +(deftest test-rota-update! + (testing "Update rota" + (doseq [[before after expected description] params-rota-update!] + (testing description + (db/rota-insert! before) + (db/rota-update! after) + (let [rota (first (db/duty-get rota-channel rota-name))] + (is (= expected (dissoc rota :rota_id)))))))) diff --git a/test/dienstplan/fixtures_test.clj b/test/dienstplan/fixtures_test.clj new file mode 100644 index 0000000..cfff0d6 --- /dev/null +++ b/test/dienstplan/fixtures_test.clj @@ -0,0 +1,64 @@ +;; Copyright (c) Vitaly Samigullin and contributors. All rights reserved. +;; +;; This program and the accompanying materials are made available under the +;; terms of the Eclipse Public License 2.0 which is available at +;; http://www.eclipse.org/legal/epl-2.0. +;; +;; This Source Code may also be made available under the following Secondary +;; Licenses when the conditions for such availability set forth in the Eclipse +;; Public License, v. 2.0 are satisfied: GNU General Public License as published by +;; the Free Software Foundation, either version 2 of the License, or (at your +;; option) any later version, with the GNU Classpath Exception which is available +;; at https://www.gnu.org/software/classpath/license.html. +;; +;; SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +(ns dienstplan.fixtures-test + "Reusable test fixtures" + (:require + [clojure.java.jdbc :as jdbc] + [clojure.tools.logging :as log] + [dienstplan.config :refer [config]] + [dienstplan.slack :as slack] + [dienstplan.verify :as verify] + [hikari-cp.core :as cp] + [mount.core :as mount :refer [defstate]])) + +(defn fix-run-server + [test] + (log/info "[fix-run-server] start") + (mount/start) + (test) + (mount/stop) + (log/info "[fix-run-server] stop")) + +(defn fix-mock-slack-api-request + [test] + (with-redefs + [slack/slack-api-request (constantly {:ok? true :status 200 :date nil}) + verify/request-verified? (constantly true)] + (test))) + +(defstate db-test + "Separate DB component with transaction rollback for tests" + :start + (let [db-opts (:db config) + datasource (cp/make-datasource db-opts)] + {:datasource datasource}) + :stop + (-> db-test :datasource cp/close-datasource)) + +;; FIXME rollback won't work for test parametization via doseq +(defn fix-db-rollback + [test] + (log/info "[fix-db-rollback] start") + (mount/start #'db-test) + (jdbc/with-db-transaction [txn db-test] + (jdbc/db-set-rollback-only! txn) + (-> (mount/only [#'dienstplan.db/db]) + (mount/swap {#'dienstplan.db/db txn}) + (mount/start)) + (test) + (mount/stop #'dienstplan.db/db)) + (mount/stop #'db-test) + (log/info "[fix-db-rollback] stop"))