Repository: CSCfi/rems
Pull request #2653 (2021-04-29 02:52:47)

feat: ip address in form
A rewrite of https://github.com/CSCfi/rems/pull/2612

## Reviewability
- [ ] Link to issue
- [ ] Note if PR is on top of other PR
- [ ] Note if related change in rems-deploy repo
- [ ] Consider adding screenshots for ease of review

## Backwards compatibility
- [ ] API is backwards compatible or completely new
- [ ] Events are backwards compatible _or_ have migrations
- [ ] Config is backwards compatible

## Documentation
- [ ] Update changelog if necessary
- [ ] API is documented and shows up in Swagger UI
- [ ] Components are added to guide page
- [ ] Update docs/ (if applicable)
- [ ] Update manual/ (if applicable)
- [ ] ADR for major architectural decisions or experiments
- [ ] New config options in config-defaults.edn

## Testing
- [ ] Complex logic is unit tested
- [ ] Valuable features are integration / browser / acceptance tested automatically

## Follow-up
- [ ] New tasks are created for pending or remaining tasks



Patch:
diff --git a/resources/translations/en.edn b/resources/translations/en.edn
index efb21e701..1a2f7d163 100644
--- a/resources/translations/en.edn
+++ b/resources/translations/en.edn
@@ -291,6 +291,7 @@
                 :type-description "Application title"
                 :type-email "Email address"
                 :type-header "Header"
+                :type-ip-address "IP address"
                 :type-label "Label"
                 :type-multiselect "Multi-select list"
                 :type-option "Option list"
@@ -440,6 +441,8 @@
                       :columns-required "At least one column is required."
                       :invalid-email "Invalid email address."
                       :invalid-phone-number "Invalid phone number."
+                      :invalid-ip-address "Invalid IP address."
+                      :invalid-ip-address-private "This is a private IP address and cannot be used in an application."
                       :invalid-user "Invalid user."
                       :invalid-value "Invalid value."
                       :options-required "At least one option is required."
diff --git a/resources/translations/fi.edn b/resources/translations/fi.edn
index 32f958c81..b10cfeb87 100644
--- a/resources/translations/fi.edn
+++ b/resources/translations/fi.edn
@@ -266,6 +266,7 @@
                 :type-description "Hakemuksen otsikko"
                 :type-email "Sähköpostiosoite"
                 :type-header "Otsikko"
+                :type-ip-address "IP-osoite"
                 :type-label "Lisätietokenttä"
                 :type-multiselect "Monivalintalista"
                 :type-option "Valintalista"
@@ -397,6 +398,8 @@
                       :columns-required "Vähintään yksi sarake vaaditaan."
                       :invalid-email "Sähköpostiosoite ei kelpaa."
                       :invalid-phone-number "Puhelinnumero ei kelpaa."
+                      :invalid-ip-address "IP-osoite ei kelpaa."
+                      :invalid-ip-address-private "Tämä on yksityinen IP-osoite, eikä sitä voi käyttää hakemuksessa."
                       :invalid-user "Käyttäjä ei kelpaa."
                       :invalid-value "Arvo ei kelpaa."
                       :options-required "Vähintään yksi vaihtoehto vaaditaan."
diff --git a/resources/translations/sv.edn b/resources/translations/sv.edn
index 9e7b3e3d4..8ed14a964 100644
--- a/resources/translations/sv.edn
+++ b/resources/translations/sv.edn
@@ -266,6 +266,7 @@
                 :type-description "Ansökans rubrik"
                 :type-email "E-postadress"
                 :type-header "Titel"
+                :type-ip-address "IP-adress"
                 :type-label "Informationsfält"
                 :type-multiselect "Flervalsmeny"
                 :type-option "Rullgardinsmeny"
@@ -397,6 +398,8 @@
                       :columns-required "Kräv minst en kolumn." ; TODO check
                       :invalid-email "Ogiltig e-postadress."
                       :invalid-phone-number "Ogiltig telefonnummer."
+                      :invalid-ip-address "Ogiltig IP-adress."
+                      :invalid-ip-address-private "Detta är en privat IP-adress och kan inte användas i en ansökning."
                       :invalid-user "Ogiltig användare."
                       :invalid-value "Ogiltigt värde."
                       :options-required "Kräv minst ett alternativ." ; TODO check
diff --git a/src/clj/rems/api/schema.clj b/src/clj/rems/api/schema.clj
index d710543ce..4d2184032 100644
--- a/src/clj/rems/api/schema.clj
+++ b/src/clj/rems/api/schema.clj
@@ -129,7 +129,7 @@
 ;;; template for a form field, before answering
 (s/defschema FieldTemplate
   {:field/id schema-base/FieldId
-   :field/type (s/enum :attachment :date :description :email :header :label :multiselect :option :phone-number :text :texta :table)
+   :field/type (s/enum :attachment :date :description :email :header :ip-address :label :multiselect :option :phone-number :text :texta :table)
    :field/title schema-base/LocalizedString
    (s/optional-key :field/placeholder) schema-base/LocalizedString
    :field/optional s/Bool
diff --git a/src/clj/rems/form_validation.clj b/src/clj/rems/form_validation.clj
index da1c4f23c..cdd4b9d30 100644
--- a/src/clj/rems/form_validation.clj
+++ b/src/clj/rems/form_validation.clj
@@ -2,7 +2,12 @@
   "Pure functions for form validation logic"
   (:require [clojure.string :as str]
             [rems.common.form :as form]
-            [rems.common.util :refer [+email-regex+ +phone-number-regex+]]))
+            [rems.common.util :refer [+email-regex+
+                                      +phone-number-regex+
+                                      +valid-ip-address-regex+
+                                      +valid-ip-address-regex-version-six+
+                                      +reserved-ip-address-range-regex+
+                                      +reserved-ip-address-range-regex-version-six+]]))
 
 (defn- all-columns-set? [field]
   (let [valid-row? #(not-any? str/blank? (map :value %))]
@@ -52,6 +57,23 @@
       {:field-id (:field/id field)
        :type     :t.form.validation/invalid-phone-number})))
 
+(defn- invalid-ip-address-error [field]
+  (when (and (= (:field/type field) :ip-address)
+             (not (str/blank? (:field/value field))))
+    (cond
+      (or
+       (and (first (re-matches +valid-ip-address-regex+ (:field/value field)))
+            (first (re-matches +reserved-ip-address-range-regex+ (:field/value field))))
+       (and (first (re-matches +valid-ip-address-regex-version-six+ (:field/value field)))
+            (first (re-matches +reserved-ip-address-range-regex-version-six+ (:field/value field)))))
+      {:field-id (:field/id field)
+       :type     :t.form.validation/invalid-ip-address-private}
+      (and
+       (not (first (re-matches +valid-ip-address-regex+ (:field/value field))))
+       (not (first (re-matches +valid-ip-address-regex-version-six+ (:field/value field)))))
+      {:field-id (:field/id field)
+       :type     :t.form.validation/invalid-ip-address}
+      :else nil)))
 
 (defn- option-value-valid? [field]
   (let [allowed-values (set (conj (map :key (:field/options field)) ""))]
@@ -101,6 +123,7 @@
   (or (wrong-value-type-error field)
       (invalid-email-address-error field)
       (invalid-phone-number-error field)
+      (invalid-ip-address-error field)
       (too-long-error field)
       (invalid-option-error field)
       (missing-columns-error field)
diff --git a/src/cljc/rems/common/form.cljc b/src/cljc/rems/common/form.cljc
index 7c1b7512e..dafd356e3 100644
--- a/src/cljc/rems/common/form.cljc
+++ b/src/cljc/rems/common/form.cljc
@@ -12,7 +12,7 @@
   (not (contains? #{:label :header} (:field/type field))))
 
 (defn supports-placeholder? [field]
-  (contains? #{:text :texta :description :phone-number} (:field/type field)))
+  (contains? #{:text :texta :description :phone-number :ip-address} (:field/type field)))
 
 (defn supports-max-length? [field]
   (contains? #{:description :text :texta} (:field/type field)))
@@ -102,7 +102,7 @@
               (str/blank? (:organization/id organization)))
       {:organization :t.form.validation/required})))
 
-(def field-types #{:attachment :date :description :email :header :label :multiselect :phone-number :option :text :texta :table})
+(def field-types #{:attachment :date :description :email :header :ip-address :label :multiselect :phone-number :option :text :texta :table})
 
 (defn- validate-field-type [m]
   (let [type (keyword (get m :field/type))]
@@ -477,6 +477,19 @@
                  (validate-form-template (assoc-in form [:form/fields 0 :field/title :en] "") languages)
                  (validate-form-template (assoc-in form [:form/fields 0 :field/title :en] nil) languages))))))
 
+    (testing "ip address"
+      (let [form (assoc form :form/fields
+                        [{:field/type :ip-address
+                          :field/title {:en "en" :fi "fi"}
+                          :field/optional false}])]
+        (testing "valid form"
+          (is (empty? (validate-form-template form languages))))
+
+        (testing "missing title localization"
+          (is (= {:form/fields {0 {:field/title {:en :t.form.validation/required}}}}
+                 (validate-form-template (assoc-in form [:form/fields 0 :field/title :en] "") languages)
+                 (validate-form-template (assoc-in form [:form/fields 0 :field/title :en] nil) languages))))))
+
     (testing "table"
       (let [form (assoc form :form/fields
                         [{:field/type :table
diff --git a/src/cljc/rems/common/util.cljc b/src/cljc/rems/common/util.cljc
index 547ed2108..91cab71d0 100644
--- a/src/cljc/rems/common/util.cljc
+++ b/src/cljc/rems/common/util.cljc
@@ -19,6 +19,167 @@
   (is (= nil
          (re-matches +phone-number-regex+ "+35845000000000000000000000000000"))))
 
+;; regex from https://stackoverflow.com/questions/5284147/validating-ipv4-addresses-with-regexp
+(def +valid-ip-address-regex+ #"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$")
+
+(def +valid-ip-address-regex-version-six+ #"(([0-9]|[a-f]){1,4}(\:)){7}(([0-9]|[a-f]){1,4})$")
+
+;; https://stackoverflow.com/questions/2814002/private-ip-address-identifier-in-regular-expression
+;; for now, this test should test for the following private IPv4 address pattaerns
+;; 0.x.x.x
+;; 10.x.x.x - Private network, local communications
+;; 100.64.x.x–100.127.x.x - Private network, communications between a service provider and its subscribers
+;; 127.x.x.x - host, loopback addresses to the local host
+;; 169.254.x.x - Subnet, Used for link-local addresses[7] between two hosts on a single link when no IP address is otherwise specified
+;; 172.16.x.x – 172.31.x.x - Private network, local communications within a private network
+;; 192.0.0.x - Private network, IETF Protocol Assignments
+;; 192.0.2.x - Documentation, Assigned as TEST-NET-1, documentation and examples.
+;; 192.168.x.x - Private network, local communications within a private network
+;; 198.18.x.x - 198.19.x.x - Private network, benchmark testing of inter-network communications between two separate subnets
+;; 198.51.100.x - Documentation, Assigned as TEST-NET-2, documentation and examples.
+;; 203.0.113.x - Documentation, Assigned as TEST-NET-3, documentation and examples.
+;; 224.x.x.x - In use for IP multicast.[11] (Former Class D network).
+;; 240.x.x.x–255.x.x.x - multicast
+(def +reserved-ip-address-range-regex+
+  (re-pattern
+   (str
+    "((0|10|127|224)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$)|"
+    "(100\\.(6[4-9]|[7-9][0-9]|1[0-1][0-7]|12[0-7])\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$)|"
+    "(169\\.254\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$)|"
+    "(172\\.(1[6-9]|2[0-9]|3[0-1])\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$)|"
+    "(192\\.0\\.(0|2?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$)|"
+    "(192\\.168\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$)|"
+    "(198\\.1[8-9]\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$)|"
+    "(198\\.51\\.100\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$)|"
+    "(203\\.0\\.113\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$)|"
+    "((2[4-5][0-9])\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$)")))
+
+;; https://simpledns.plus/private-ipv6
+;; https://serverfault.com/questions/546606/what-are-the-ipv6-public-and-private-and-reserved-ranges
+(def +reserved-ip-address-range-regex-version-six+
+  (re-pattern
+   (str "(fdce|fc00|fd00)\\:(([0-9]|[a-f]){1,4}(\\:)){6}(([0-9]|[a-f]){1,4})$")))
+
+(deftest test-ip-address-regex
+  (is (= "0.0.0.0"
+         (first (re-matches +valid-ip-address-regex+ "0.0.0.0"))))
+  (is (= "255.255.255.255"
+         (first (re-matches +valid-ip-address-regex+ "255.255.255.255"))))
+  (is (= "127.0.0.1"
+         (first (re-matches +valid-ip-address-regex+ "127.0.0.1"))))
+  (is (= "192.168.0.0"
+         (first (re-matches +valid-ip-address-regex+ "192.168.0.0"))))
+  (is (= "142.250.74.110"
+         (first (re-matches +valid-ip-address-regex+ "142.250.74.110"))))
+  (is (= "10.255.255.255"
+         (first (re-matches +valid-ip-address-regex+ "10.255.255.255"))))
+  (is (= nil
+         (first (re-matches +valid-ip-address-regex+ "256.255.255.255"))))
+  (is (= nil
+         (first (re-matches +valid-ip-address-regex+ "10.foo.bar.255"))))
+  ;; 10.x.x.x - Private network, local communications
+  (is (= "10.0.0.0"
+         (first (re-matches +reserved-ip-address-range-regex+ "10.0.0.0"))))
+  (is (= "10.26.167.0"
+         (first (re-matches +reserved-ip-address-range-regex+ "10.26.167.0"))))
+  (is (= "10.0.255.255"
+         (first (re-matches +reserved-ip-address-range-regex+ "10.0.255.255"))))
+  (is (= "192.0.0.255"
+         (first (re-matches +reserved-ip-address-range-regex+ "192.0.0.255"))))
+  (is (= "192.0.2.255"
+         (first (re-matches +reserved-ip-address-range-regex+ "192.0.2.255"))))
+  (is (= "192.168.10.255"
+         (first (re-matches +reserved-ip-address-range-regex+ "192.168.10.255"))))
+  (is (= "172.16.0.255"
+         (first (re-matches +reserved-ip-address-range-regex+ "172.16.0.255"))))
+
+  ;; 100.64.x.x–100.127.x.x - Private network, communications between a service provider and its subscribers
+  (is (= "100.71.0.255"
+         (first (re-matches +reserved-ip-address-range-regex+ "100.71.0.255"))))
+  (is (= "100.64.0.255"
+         (first (re-matches +reserved-ip-address-range-regex+ "100.64.0.255"))))
+  (is (= "100.100.0.255"
+         (first (re-matches +reserved-ip-address-range-regex+ "100.100.0.255"))))
+  (is (= "100.127.78.10"
+         (first (re-matches +reserved-ip-address-range-regex+ "100.127.78.10"))))
+  (is (= nil
+         (first (re-matches +reserved-ip-address-range-regex+ "100.255.78.10"))))
+  (is (= nil
+         (first (re-matches +reserved-ip-address-range-regex+ "100.198.78.10"))))
+
+  ;; 169.254.x.x - Subnet, Used for link-local addresses[7] between two hosts on a single link when no IP address is otherwise specified
+  (is (= "169.254.1.1"
+         (first (re-matches +reserved-ip-address-range-regex+ "169.254.1.1"))))
+  (is (= nil
+         (first (re-matches +reserved-ip-address-range-regex+ "16.254.1.1"))))
+  (is (= nil
+         (first (re-matches +reserved-ip-address-range-regex+ "169.25.1.1"))))
+  (is (= nil
+         (first (re-matches +reserved-ip-address-range-regex+ "169.25.1.1"))))
+
+  ;; 172.16.x.x – 172.31.x.x - Private network, local communications within a private network
+  (is (= "172.16.0.0"
+         (first (re-matches +reserved-ip-address-range-regex+ "172.16.0.0"))))
+  (is (= nil
+         (first (re-matches +reserved-ip-address-range-regex+ "172.1.0.0"))))
+  (is (= "172.30.0.0"
+         (first (re-matches +reserved-ip-address-range-regex+ "172.30.0.0"))))
+  (is (= "172.31.0.0"
+         (first (re-matches +reserved-ip-address-range-regex+ "172.31.0.0"))))
+  (is (= nil
+         (first (re-matches +reserved-ip-address-range-regex+ "172.11.0.0"))))
+
+  ;; 192.0.0.x - Private network, IETF Protocol Assignments
+  ;; 192.0.2.x - Documentation, Assigned as TEST-NET-1, documentation and examples.
+  ;; 192.168.x.x - Private network, local communications within a private network
+
+  (is (= "192.0.0.0"
+         (first (re-matches +reserved-ip-address-range-regex+ "192.0.0.0"))))
+  (is (= "192.0.0.189"
+         (first (re-matches +reserved-ip-address-range-regex+ "192.0.0.189"))))
+  (is (= "192.0.2.189"
+         (first (re-matches +reserved-ip-address-range-regex+ "192.0.2.189"))))
+  (is (= nil
+         (first (re-matches +reserved-ip-address-range-regex+ "192.0.25.189"))))
+  (is (= nil
+         (first (re-matches +reserved-ip-address-range-regex+ "192.0.1.10"))))
+  (is (= nil
+         (first (re-matches +reserved-ip-address-range-regex+ "19.0.1.10"))))
+  (is (= "192.168.1.10"
+         (first (re-matches +reserved-ip-address-range-regex+ "192.168.1.10"))))
+  (is (= nil
+         (first (re-matches +reserved-ip-address-range-regex+ "192.16.1.10"))))
+
+  ;; 198.18.x.x - 198.19.x.x - Private network, benchmark testing of inter-network communications between two separate subnets
+  ;; 198.51.100.x - Documentation, Assigned as TEST-NET-2, documentation and examples.
+
+  (is (= nil
+         (first (re-matches +reserved-ip-address-range-regex+ "198.1.100.78"))))
+  (is (= "198.18.100.78"
+         (first (re-matches +reserved-ip-address-range-regex+ "198.18.100.78"))))
+  (is (= "198.51.100.78"
+         (first (re-matches +reserved-ip-address-range-regex+ "198.51.100.78"))))
+  (is (= nil
+         (first (re-matches +reserved-ip-address-range-regex+ "198.50.100.78"))))
+
+  ;; 203.0.113.x - Documentation, Assigned as TEST-NET-3, documentation and examples.
+  (is (= "203.0.113.89"
+         (first (re-matches +reserved-ip-address-range-regex+ "203.0.113.89"))))
+  (is (= nil
+         (first (re-matches +reserved-ip-address-range-regex+ "203.0.111.89"))))
+
+  (is (= nil
+         (first (re-matches +reserved-ip-address-range-regex+ "142.250.74.110"))))
+
+  (is (= "2001:db8:1870:999:128:7648:3849:688"
+         (first (re-matches +valid-ip-address-regex-version-six+ "2001:db8:1870:999:128:7648:3849:688"))))
+  (is (= nil
+         (first (re-matches +valid-ip-address-regex-version-six+ "2001:db8:1g70:999:128:7648:3849:688"))))
+  (is (= nil
+         (first (re-matches +reserved-ip-address-range-regex-version-six+ "2001:db8:1g70:999:128:7648:3849:688"))))
+  (is (= "fdce:de09:d25d:b23e:de8:d0e8:de8:de8"
+         (first (re-matches +reserved-ip-address-range-regex-version-six+ "fdce:de09:d25d:b23e:de8:d0e8:de8:de8")))))
+
 ;; TODO remove separate clj and cljs implementations of getx and getx-in
 (defn getx
   "Like `get` but throws an exception if the key is not found."
diff --git a/src/cljs/rems/administration/create_form.cljs b/src/cljs/rems/administration/create_form.cljs
index f9a441810..1bc83eac7 100644
--- a/src/cljs/rems/administration/create_form.cljs
+++ b/src/cljs/rems/administration/create_form.cljs
@@ -523,6 +523,7 @@
                                          {:value :date :label (text :t.create-form/type-date)}
                                          {:value :email :label (text :t.create-form/type-email)}
                                          {:value :phone-number :label (text :t.create-form/type-phone-number)}
+                                         {:value :ip-address :label (text :t.create-form/type-ip-address)}
                                          {:value :attachment :label (text :t.create-form/type-attachment)}
                                          {:value :label :label (text :t.create-form/type-label)}
                                          {:value :header :label (text :t.create-form/type-header)}]}])
diff --git a/src/cljs/rems/fields.cljs b/src/cljs/rems/fields.cljs
index 3a8e1e64f..c489bc998 100644
--- a/src/cljs/rems/fields.cljs
+++ b/src/cljs/rems/fields.cljs
@@ -485,6 +485,7 @@
       :multiselect [multiselect-field f]
       :option [option-field f]
       :phone-number [text-field f]
+      :ip-address [text-field f]
       :table [table-field f]
       :text [text-field f]
       :texta [texta-field f]


Test patch:
diff --git a/src/clj/rems/db/test_data.clj b/src/clj/rems/db/test_data.clj
index 0e6d7c7ff..c64de3135 100644
--- a/src/clj/rems/db/test_data.clj
+++ b/src/clj/rems/db/test_data.clj
@@ -199,6 +199,12 @@
     :field/title {:en "Phone number"
                   :fi "Puhelinnumero"
                   :sv "Telefonnummer"}
+    :field/optional true}
+
+   {:field/type :ip-address
+    :field/title {:en "IP address"
+                  :fi "IP-osoite"
+                  :sv "IP-adress"}
     :field/optional true}])
 
 (deftest test-all-field-types-example
diff --git a/src/clj/rems/db/test_data_helpers.clj b/src/clj/rems/db/test_data_helpers.clj
index 3aaecfd44..c7c095482 100644
--- a/src/clj/rems/db/test_data_helpers.clj
+++ b/src/clj/rems/db/test_data_helpers.clj
@@ -212,6 +212,7 @@
                                                :date "2002-03-04"
                                                :email "user@example.com"
                                                :phone-number "+358451110000"
+                                               :ip-address "142.250.74.110"
                                                :table (repeat 2 (for [column (:field/columns field)]
                                                                   {:column (:key column) :value field-value}))
                                                :attachment (str attachment)
diff --git a/test/clj/rems/api/test_forms.clj b/test/clj/rems/api/test_forms.clj
index 041c44501..3d21c88a6 100644
--- a/test/clj/rems/api/test_forms.clj
+++ b/test/clj/rems/api/test_forms.clj
@@ -192,6 +192,9 @@
                                  {:field/type :phone-number
                                   :field/title localized
                                   :field/optional true}
+                                 {:field/type :ip-address
+                                  :field/title localized
+                                  :field/optional true}
                                  {:field/type :date
                                   :field/title localized
                                   :field/optional true}
diff --git a/test/clj/rems/test_browser.clj b/test/clj/rems/test_browser.clj
index 2a0ed4e74..56a6c72b0 100644
--- a/test/clj/rems/test_browser.clj
+++ b/test/clj/rems/test_browser.clj
@@ -334,6 +334,7 @@
         ;; leave "Text are with max length" empty
 
         (fill-form-field "Phone number" "+358450000100")
+        (fill-form-field "IP address" "142.250.74.110")
 
         (testing "save draft succesfully"
           (btu/scroll-and-click :save)
@@ -424,7 +425,8 @@
                       ["label" ""]
                       ["text" ""]
                       ["texta" ""]
-                      ["phone-number" "+358450000100"]]
+                      ["phone-number" "+358450000100"]
+                      ["ip-address" "142.250.74.110"]]
                      (for [field (select [:application/forms ALL :form/fields ALL] application)]
                        ;; TODO could test other fields here too, e.g. title
                        [(:field/type field)
diff --git a/test/clj/rems/test_form_validation.clj b/test/clj/rems/test_form_validation.clj
index e287fbffb..8edec4cc5 100644
--- a/test/clj/rems/test_form_validation.clj
+++ b/test/clj/rems/test_form_validation.clj
@@ -393,6 +393,30 @@
                                         :field/visible  true
                                         :field/value    "+358450000100"}]))))
 
+  (testing "error: field input invalid ip address"
+    (is (= [{:type :t.form.validation/invalid-ip-address :field-id "invalid ip address"}
+            {:type :t.form.validation/invalid-ip-address-private :field-id "private ip address"}]
+           (validate-fields-for-draft [{:field/id       "empty ip address"
+                                        :field/type     :ip-address
+                                        :field/optional false
+                                        :field/visible  true
+                                        :field/value    ""}
+                                       {:field/id       "invalid ip address"
+                                        :field/type     :ip-address
+                                        :field/optional false
+                                        :field/visible  true
+                                        :field/value    "+058450000100"}
+                                       {:field/id       "correct ip address"
+                                        :field/type     :ip-address
+                                        :field/optional false
+                                        :field/visible  true
+                                        :field/value    "142.250.74.110"}
+                                       {:field/id       "private ip address"
+                                        :field/type     :ip-address
+                                        :field/optional true
+                                        :field/visible  true
+                                        :field/value    "192.0.2.255"}]))))
+
   (testing "table validation"
     (let [fields [{:field/id "tbl"
                    :field/type :table
diff --git a/test/clj/rems/test_pdf.clj b/test/clj/rems/test_pdf.clj
index 4cc39d44b..d86b23bf2 100644
--- a/test/clj/rems/test_pdf.clj
+++ b/test/clj/rems/test_pdf.clj
@@ -164,7 +164,9 @@
                  [[:paragraph pdf/field-style "Text area with max length"]
                   [:paragraph "pdf test"]]
                  [[:paragraph pdf/field-style "Phone number"]
-                  [:paragraph "+358451110000"]]]]]
+                  [:paragraph "+358451110000"]]
+                 [[:paragraph pdf/field-style "IP address"]
+                  [:paragraph "142.250.74.110"]]]]]
               [[:heading pdf/heading-style "Events"]
                [:list
                 [[:phrase "2000-01-01 00:00" " " "Alice Applicant created application 2000/1." nil nil nil]