From dd7d050ec096683365d2ce273d3b62bd998de0c6 Mon Sep 17 00:00:00 2001
From: Alexander Schlemmer <alexander@mail-schlemmer.de>
Date: Fri, 15 Jan 2021 16:38:33 +0100
Subject: [PATCH] ENH: validation of configuration schema including unit tests

---
 src/caosdb/configuration.py                   |  18 +-
 src/caosdb/schema-pycaosdb-ini.yml            | 169 +++++++++---------
 .../pycaosdb-indiscale-demo.ini               |  12 ++
 unittests/broken_configs/pycaosdb1.ini        |   5 +
 unittests/broken_configs/pycaosdb2.ini        |   9 +
 unittests/broken_configs/pycaosdb3.ini        |  12 ++
 unittests/broken_configs/pycaosdb4.ini        |  10 ++
 .../test_configs/pycaosdb-indiscale-demo.ini  |  12 ++
 unittests/test_configs/pycaosdb1.ini          |   6 +
 unittests/test_configs/pycaosdb2.ini          |   9 +
 unittests/test_configs/pycaosdb3.ini          |  12 ++
 unittests/test_schema.py                      |  22 ++-
 12 files changed, 205 insertions(+), 91 deletions(-)
 create mode 100644 unittests/broken_configs/pycaosdb-indiscale-demo.ini
 create mode 100644 unittests/broken_configs/pycaosdb1.ini
 create mode 100644 unittests/broken_configs/pycaosdb2.ini
 create mode 100644 unittests/broken_configs/pycaosdb3.ini
 create mode 100644 unittests/broken_configs/pycaosdb4.ini
 create mode 100644 unittests/test_configs/pycaosdb-indiscale-demo.ini
 create mode 100644 unittests/test_configs/pycaosdb1.ini
 create mode 100644 unittests/test_configs/pycaosdb2.ini
 create mode 100644 unittests/test_configs/pycaosdb3.ini

diff --git a/src/caosdb/configuration.py b/src/caosdb/configuration.py
index 27d44617..67427d10 100644
--- a/src/caosdb/configuration.py
+++ b/src/caosdb/configuration.py
@@ -24,6 +24,7 @@
 
 import os
 import yaml
+from jsonschema import validate
 
 try:
     # python2
@@ -48,7 +49,9 @@ def configure(inifile):
         _pycaosdbconf = None
     if _pycaosdbconf is None:
         _reset_config()
-    return _pycaosdbconf.read(inifile)
+    read_config = _pycaosdbconf.read(inifile)
+    validate_yaml_schema(config_to_yaml(_pycaosdbconf))
+    return read_config
 
 def get_config():
     return _pycaosdbconf
@@ -59,13 +62,18 @@ def config_to_yaml(config):
     for s in config.sections():
         valobj[s] = {}
         for key, value in config[s].items():
-            valobj[s][key] = value
+            # TODO: Can the type be inferred from the config object?
+            if key in ["timeout", "debug"]:
+                valobj[s][key] = int(value)
+            elif key in ["ssl_insecure"]:
+                valobj[s][key] = bool(value)
+            else:
+                valobj[s][key] = value
 
     return valobj
 
-def validate(valobj):
-    print(__file__)
+def validate_yaml_schema(valobj):
     with open(os.path.join(os.path.dirname(__file__), "schema-pycaosdb-ini.yml")) as f:
         schema = yaml.load(f, Loader=yaml.SafeLoader)
-    validate(instance=valobj, schema=schema)
+    validate(instance=valobj, schema=schema["schema-pycaosdb-ini"])
     
diff --git a/src/caosdb/schema-pycaosdb-ini.yml b/src/caosdb/schema-pycaosdb-ini.yml
index db8ded7f..977fcdfa 100644
--- a/src/caosdb/schema-pycaosdb-ini.yml
+++ b/src/caosdb/schema-pycaosdb-ini.yml
@@ -1,86 +1,87 @@
-Container:
+schema-pycaosdb-ini:
+  type: object
   additionalProperties: false
   properties:
-    debug:
-      type: integer
-Connection:
-  description: Settings for the connection to the CaosDB server
-  additionalProperties: false
-  properties:
-    url:
-      description: URL of the CaosDB server
-      type: string
-      pattern: https://[-a-zA-Z0-9\.]+(:[0-9]+)?/
-      examples: [https://demo.indiscale.com/, https://localhost:10443/]
-    debug:
-      type: integer
-    username:
-      type: string
-      description: User name used for authentication with the server
-      examples: [admin]
-    password_method:
-      description: The password input method defines how the password is supplied that is used for authentication with the server.
-      type: string
-      default: input
-      enum: [input, plain, pass, keyring]
-    password:
-      type: string
-      examples: [caosdb]
-    auth_token:
-      type: string
-      description: Using an authentication token to connect with the server. This setting is not recommended for users.
-    cacert:
-      type: string
-      description: If the server's SSL certificate cannot be validated by your installed certificates (default or installed by your admins), you may also need to supply the matching key (pem file)
-      examples: [/path/to/caosdb.ca.pem]
-    ssl_insecure:
-      description: If this option is set, the SSL certificate of the server will not be validated. This has the potential of a man-in-the-middle attack. Use with care!
-      type: boolean
-      default: false
-    ssl_version:
-        description: You may define the ssl version to be used. It has to be the name of the corresponding attribute in the Python ssl module.
-        examples: [PROTOCOL_TLS]
-    debug:
-      default: 0
-      type: integer
-      description: The debug key allows control the verbosity. Set it to 1 or 2 in case you  want to see debugging output or if you want to learn more about the internals of the protocol.  0 disables debugging output.
-    socket_proxy:
-      examples: [localhost:12345]
-      type: string
-      description: You can define a socket proxy to be used. This is for the case that the server sits behind a firewall which is being tunnelled with a socket proxy (SOCKS4 or SOCKS5) (e.g. via ssh's -D option or a dedicated proxy server).
-    implementation:
-      description: This option is used internally and for testing. Do not override.
-      examples: [_DefaultCaosDBServerConnection]
-    timeout:
-      type: integer
-  allOf:
-    - if:
-        properties:
-          password_method:
-            const: input
-      then:
-        required: [url]
-    - if:
-        properties:
-          password_method:
-            const: plain
-      then:
-        required: [url, username, password]
-    - if:
-        properties:
-          password_method:
-            const: pass
-      then:
-        required: [url, username, password_identifier]
-    - if:
-        properties:
-          password_method:
-            const: keyring
-      then:
-        required: [url, username]
-
-      
-
-        
-
-
+    Container:
+      additionalProperties: false
+      properties:
+        debug:
+          default: 0
+          type: integer
+          enum: [0, 1, 2]
+    Connection:
+      description: Settings for the connection to the CaosDB server
+      additionalProperties: false
+      properties:
+        url:
+          description: URL of the CaosDB server
+          type: string
+          pattern: https://[-a-zA-Z0-9\.]+(:[0-9]+)?/
+          examples: [https://demo.indiscale.com/, https://localhost:10443/]
+        username:
+          type: string
+          description: User name used for authentication with the server
+          examples: [admin]
+        password_method:
+          description: The password input method defines how the password is supplied that is used for authentication with the server.
+          type: string
+          default: input
+          enum: [input, plain, pass, keyring]
+        password_identifier:
+          type: string
+        password:
+          type: string
+          examples: [caosdb]
+        auth_token:
+          type: string
+          description: Using an authentication token to connect with the server. This setting is not recommended for users.
+        cacert:
+          type: string
+          description: If the server's SSL certificate cannot be validated by your installed certificates (default or installed by your admins), you may also need to supply the matching key (pem file)
+          examples: [/path/to/caosdb.ca.pem]
+        ssl_insecure:
+          description: If this option is set, the SSL certificate of the server will not be validated. This has the potential of a man-in-the-middle attack. Use with care!
+          type: boolean
+          default: false
+        ssl_version:
+            description: You may define the ssl version to be used. It has to be the name of the corresponding attribute in the Python ssl module.
+            examples: [PROTOCOL_TLS]
+        debug:
+          default: 0
+          type: integer
+          enum: [0, 1, 2]
+          description: The debug key allows control the verbosity. Set it to 1 or 2 in case you  want to see debugging output or if you want to learn more about the internals of the protocol.  0 disables debugging output.
+        socket_proxy:
+          examples: [localhost:12345]
+          type: string
+          description: You can define a socket proxy to be used. This is for the case that the server sits behind a firewall which is being tunnelled with a socket proxy (SOCKS4 or SOCKS5) (e.g. via ssh's -D option or a dedicated proxy server).
+        implementation:
+          description: This option is used internally and for testing. Do not override.
+          examples: [_DefaultCaosDBServerConnection]
+        timeout:
+          type: integer
+      allOf:
+        - if:
+            properties:
+              password_method:
+                const: input
+          then:
+            required: [url]
+        - if:
+            properties:
+              password_method:
+                const: plain
+          then:
+            required: [url, username, password]
+        - if:
+            properties:
+              password_method:
+                const: pass
+          then:
+            required: [url, username, password_identifier]
+        - if:
+            properties:
+              password_method:
+                const: keyring
+          then:
+            required: [url, username]
diff --git a/unittests/broken_configs/pycaosdb-indiscale-demo.ini b/unittests/broken_configs/pycaosdb-indiscale-demo.ini
new file mode 100644
index 00000000..d8994eae
--- /dev/null
+++ b/unittests/broken_configs/pycaosdb-indiscale-demo.ini
@@ -0,0 +1,12 @@
+[Connection]
+url=https://demo.indiscale.com
+username=admin
+password=caosdb
+password_method=plain
+cacert=/etc/ssl/cert.pem
+
+timeout=10000
+debug=0
+
+[Container]
+debug=0
\ No newline at end of file
diff --git a/unittests/broken_configs/pycaosdb1.ini b/unittests/broken_configs/pycaosdb1.ini
new file mode 100644
index 00000000..71180286
--- /dev/null
+++ b/unittests/broken_configs/pycaosdb1.ini
@@ -0,0 +1,5 @@
+[Connection]
+cacert=/very/long/path/to/self/signed/pem/file/caosdb.ca.pem
+url=https://hostname:8833/playground
+username=username
+password_method=pass
diff --git a/unittests/broken_configs/pycaosdb2.ini b/unittests/broken_configs/pycaosdb2.ini
new file mode 100644
index 00000000..6bdf58ea
--- /dev/null
+++ b/unittests/broken_configs/pycaosdb2.ini
@@ -0,0 +1,9 @@
+[Connection]
+url=https://0.0.0.0/
+username=username
+password_identifier=SECTION/SUBSECTION/identifier
+password_method=pass
+cacert=/etc/ssl/cert.pem
+ssl_insecure=true
+timeout=10000
+debug=9
diff --git a/unittests/broken_configs/pycaosdb3.ini b/unittests/broken_configs/pycaosdb3.ini
new file mode 100644
index 00000000..62d1fed9
--- /dev/null
+++ b/unittests/broken_configs/pycaosdb3.ini
@@ -0,0 +1,12 @@
+[connection]
+ssl_insecure=true
+url=https://localhost:10443/
+password=caosdb
+username=admin
+password_method=plain
+
+timeout=10000
+debug=0
+
+[Container]
+debug=0
diff --git a/unittests/broken_configs/pycaosdb4.ini b/unittests/broken_configs/pycaosdb4.ini
new file mode 100644
index 00000000..e96604ac
--- /dev/null
+++ b/unittests/broken_configs/pycaosdb4.ini
@@ -0,0 +1,10 @@
+[Connection]
+ssl_insecure=true
+url=https://localhost:10443/
+password=caosdb
+username=admin
+password_method=plain
+
+timeout=10000
+debug=0
+key=bla
\ No newline at end of file
diff --git a/unittests/test_configs/pycaosdb-indiscale-demo.ini b/unittests/test_configs/pycaosdb-indiscale-demo.ini
new file mode 100644
index 00000000..90103434
--- /dev/null
+++ b/unittests/test_configs/pycaosdb-indiscale-demo.ini
@@ -0,0 +1,12 @@
+[Connection]
+url=https://demo.indiscale.com/
+username=admin
+password=caosdb
+password_method=plain
+cacert=/etc/ssl/cert.pem
+
+timeout=10000
+debug=0
+
+[Container]
+debug=0
\ No newline at end of file
diff --git a/unittests/test_configs/pycaosdb1.ini b/unittests/test_configs/pycaosdb1.ini
new file mode 100644
index 00000000..dcfa7c21
--- /dev/null
+++ b/unittests/test_configs/pycaosdb1.ini
@@ -0,0 +1,6 @@
+[Connection]
+cacert=/very/long/path/to/self/signed/pem/file/caosdb.ca.pem
+url=https://hostname:8833/playground
+password_identifier=SECTION/caosdb
+username=username
+password_method=pass
diff --git a/unittests/test_configs/pycaosdb2.ini b/unittests/test_configs/pycaosdb2.ini
new file mode 100644
index 00000000..e5493bde
--- /dev/null
+++ b/unittests/test_configs/pycaosdb2.ini
@@ -0,0 +1,9 @@
+[Connection]
+url=https://0.0.0.0/
+username=username
+password_identifier=SECTION/SUBSECTION/identifier
+password_method=pass
+cacert=/etc/ssl/cert.pem
+ssl_insecure=true
+timeout=10000
+debug=0
diff --git a/unittests/test_configs/pycaosdb3.ini b/unittests/test_configs/pycaosdb3.ini
new file mode 100644
index 00000000..6c493403
--- /dev/null
+++ b/unittests/test_configs/pycaosdb3.ini
@@ -0,0 +1,12 @@
+[Connection]
+ssl_insecure=true
+url=https://localhost:10443/
+password=caosdb
+username=admin
+password_method=plain
+
+timeout=10000
+debug=0
+
+[Container]
+debug=0
diff --git a/unittests/test_schema.py b/unittests/test_schema.py
index b17ee7b1..c078b766 100644
--- a/unittests/test_schema.py
+++ b/unittests/test_schema.py
@@ -2,8 +2,26 @@
 # Test configuration schema
 # A. Schlemmer, 01/2021
 
+import yaml
+from jsonschema import validate
+from jsonschema.exceptions import ValidationError
+from pytest import raises
+from glob import glob
+import os
+from caosdb.configuration import config_to_yaml, validate_yaml_schema
+from configparser import ConfigParser
+
+
 def test_config_files():
-    pass
+    for fn in glob(os.path.join(os.path.dirname(__file__), "test_configs", "*.ini")):
+        c = ConfigParser()
+        c.read(fn)
+        validate_yaml_schema(config_to_yaml(c))
 
 def test_broken_config_files():
-    pass
+    for fn in glob(os.path.join(os.path.dirname(__file__), "broken_configs", "*.ini")):
+        print(fn)
+        with raises(ValidationError):
+            c = ConfigParser()
+            c.read(fn)
+            validate_yaml_schema(config_to_yaml(c))
-- 
GitLab