diff --git a/.docker-base/Dockerfile b/.docker-base/Dockerfile index 592922471aa83d5175b1fbc1d355be8b547966a1..3476e1193312f57e86778cd51422e7fc954e6e18 100644 --- a/.docker-base/Dockerfile +++ b/.docker-base/Dockerfile @@ -18,8 +18,3 @@ COPY wait-for-it.sh /opt/caosdb/wait-for-it.sh WORKDIR /opt/caosdb RUN mkdir -p /opt/caosdb/build_docker/ CMD /bin/bash - -# python client -ADD https://gitlab.com/api/v4/projects/13656973/repository/branches/dev \ - pylib_version.json -RUN pip3 install git+https://gitlab.com/caosdb/caosdb-pylib diff --git a/.docker/docker-compose.yml b/.docker/docker-compose.yml index 912fa9869898f91fe476c977f177e5aa2615bc70..efaa0b27ddc0827cb5db9e01552de53eaaf81246 100644 --- a/.docker/docker-compose.yml +++ b/.docker/docker-compose.yml @@ -23,15 +23,20 @@ services: - type: volume source: scripting target: /opt/caosdb/git/caosdb-server/scripting + - type: volume + source: authtoken + target: /opt/caosdb/git/caosdb-server/authtoken ports: # - "from_outside:from_inside" - "10443:10443" - "10080:10080" environment: DEBUG: 1 + CAOSDB_CONFIG_AUTHTOKEN_CONFIG: "conf/core/authtoken.example.yaml" volumes: scripting: extroot: + authtoken: networks: caosnet: driver: bridge diff --git a/.docker/pycaosdb.ini b/.docker/pycaosdb.ini deleted file mode 100644 index 29846d6f976db29f8bc05d29240a2b4a18fa6ec5..0000000000000000000000000000000000000000 --- a/.docker/pycaosdb.ini +++ /dev/null @@ -1,23 +0,0 @@ -[IntegrationTests] -test_server_side_scripting.bin_dir=/scripting-bin/ - -# location of the files from the pyinttest perspective -test_files.test_insert_files_in_dir.local=/extroot/test_insert_files_in_dir/ -# location of the files from the caosdb_servers perspective -test_files.test_insert_files_in_dir.server=/opt/caosdb/mnt/extroot/test_insert_files_in_dir/ - - -[Connection] -url=https://caosdb-server:10443/ -username=admin -cacert=/cert/caosdb.cert.pem -#cacert=/etc/ssl/cert.pem -debug=0 - -passwordmethod=plain -password=caosdb - -ssl_insecure=True -timeout=500 -[Container] -debug=0 diff --git a/.docker/run.sh b/.docker/run.sh index 00c7618f87295fbe67c7c485a9204b46adb1a438..b0e1a716f28516b83043fb3fdb6594515a0bafd4 100755 --- a/.docker/run.sh +++ b/.docker/run.sh @@ -1,5 +1,5 @@ #!/bin/sh -docker-compose -f tester.yml run tester +docker-compose -f tester.yml run tester rv=$? echo $rv > result diff --git a/.docker/tester.yml b/.docker/tester.yml index 912446bf7e3c8a3b9fe5c64afd77af3b548abf00..83db879c6072bfdea7b3212c833116b96bb54d0c 100644 --- a/.docker/tester.yml +++ b/.docker/tester.yml @@ -14,9 +14,13 @@ services: - type: volume source: scripting target: /scripting + - type: volume + source: authtoken + target: /authtoken networks: docker_caosnet: external: true volumes: scripting: extroot: + authtoken: diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b3c51e8d8b235afe9ffa688cb1b4a6a8399e0e3c..f5adb2885b56d998e9b47fe48a4258290acbdb6f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -21,6 +21,7 @@ variables: CI_REGISTRY_IMAGE: $CI_REGISTRY/caosdb/caosdb-pyinttest/testenv:latest CI_REGISTRY_IMAGE_BASE: $CI_REGISTRY/caosdb/caosdb-pyinttest/base:latest + DEPLOY_REF: dev stages: - setup @@ -42,24 +43,27 @@ stages: # | +-(caosdb-server)-------------------+ | # | | | | # | | /opt/caosdb | | -# | .---->| + /git/caosdb-server/scripting/ | | -# | | .--->| + /mnt/extroot | | -# | | | .->| + /cert | | -# | | | | | | | -# | | | | +-----------------------------------+ | -# | | | | | -# | | | | filesystem: | -# | | | *- /cert -----------. | -# | | | | | -# | | | volumes: | | -# | . *-- extroot ------. | | -# | *---- scripting --. | | | -# | | | | | -# | +-(caosdb-pyinttest)---+ | | | | -# | | | | | | | -# | | /scripting |<---* | | | -# | | /extroot |<----* . | -# | | /cert |<------* | +# | .------->| + /git/caosdb-server/scripting/ | | +# | | .----->| + /git/caosdb-server/authtoken/ | | +# | | | .--->| + /mnt/extroot | | +# | | | | .->| + /cert | | +# | | | | | | | | +# | | | | | +-----------------------------------+ | +# | | | | | | +# | | | | | filesystem: | +# | | | | *--- /cert -----------. | +# | | | | | | +# | | | | volumes: | | +# | | | *----- extroot ------. | | +# | | *------- scripting --. | | | +# | *--------- authtoken -. | | | | +# | | | | | | +# | +-(caosdb-pyinttest)---+ | | | | | +# | | | | | | | | +# | | /authtoken |<---* | | | | +# | | /scripting |<----* | | | +# | | /extroot |<------* | | +# | | /cert |<--------* | # | | | | # | +----------------------+ | # +---------------------------------------------------+ @@ -69,9 +73,8 @@ stages: # pipeline and a certificate is created in there. The certificat is then # available in mounted directories in the server and pyinttest containers. # -# Two additional volumes in the root docker are shared by the caosdb-server and -# the caosdb-pyintest containers. -# These volumes are inteded to be used for testing server-side scripting and +# Additional volumes in the root docker are shared by the caosdb-server and the caosdb-pyintest +# containers. These volumes are intended to be used for testing server-side scripting and # file-system features. # services: @@ -81,7 +84,9 @@ test: tags: [docker] stage: test image: $CI_REGISTRY_IMAGE_BASE + needs: ["cert"] script: + - ls / - echo $F_BRANCH - echo $CAOSDB_TAG - echo $CI_COMMIT_REF_NAME @@ -92,23 +97,27 @@ test: - if ! echo "$F_BRANCH" | grep -c "^f-" ; then F_BRANCH=dev; fi + + - echo $F_BRANCH + - echo $CI_COMMIT_REF_NAME + - echo $CI_REGISTRY_IMAGE + + - docker login -u gitlab+deploy-token-ci-pull -p $TOKEN_CI_PULL $CI_REGISTRY_INDISCALE - if [[ "$CAOSDB_TAG" == "" ]]; then if echo "$F_BRANCH" | grep -c "^f-" ; then - CAOSDB_TAG=dev_F_${F_BRANCH}-latest; + CAOSDB_TAG=${DEPLOY_REF}_F_${F_BRANCH}-latest; + docker pull $CI_REGISTRY_INDISCALE/caosdb/src/caosdb-deploy:$CAOSDB_TAG || CAOSDB_TAG=${DEPLOY_REF}-latest ; else - CAOSDB_TAG=dev-latest; + CAOSDB_TAG=${DEPLOY_REF}-latest; fi; fi - # TODO default $CAOSDB_TAG to dev_F_$F_BRANCH-latest - - echo $F_BRANCH + - docker pull $CI_REGISTRY_INDISCALE/caosdb/src/caosdb-deploy:$CAOSDB_TAG || CAOSDB_TAG=dev-latest ; - echo $CAOSDB_TAG - - echo $CI_COMMIT_REF_NAME - time docker load < /image-cache/caosdb-pyint-testenv-${CI_COMMIT_REF_NAME}.tar || true - time docker load < /image-cache/mariadb-${F_BRANCH}.tar || true - time docker load < /image-cache/caosdb-${F_BRANCH}.tar || true - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY - - docker login -u gitlab+deploy-token-ci-pull -p $TOKEN_CI_PULL $CI_REGISTRY_INDISCALE - docker pull $CI_REGISTRY_IMAGE - cd .docker # here the server and the mysql backend docker are being started @@ -134,6 +143,7 @@ build-testenv: tags: [cached-dind] image: docker:19.03 stage: setup + needs: [] script: - df -h - command -v wget @@ -165,6 +175,7 @@ cert: tags: [docker] stage: cert image: $CI_REGISTRY_IMAGE + needs: ["build-testenv"] artifacts: paths: - .docker/cert/ @@ -175,6 +186,7 @@ cert: style: tags: [docker] stage: style + needs: ["build-testenv"] image: $CI_REGISTRY_IMAGE script: - autopep8 -r --diff --exit-code . diff --git a/CHANGELOG.md b/CHANGELOG.md index 12e03fb33fbbffd2b9c88296384ad1ee90b6c45d..5ae2f4f080273dc0d9e9a95dd5aaf97123386cf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added (for new features) +* Tests for deeply nested SELECT queries - Tests for [#62](https://gitlab.com/caosdb/caosdb-server/-/issues/62) +* Tests for One-time Authentication Tokens +* Test for [caosdb-pylib#31](https://gitlab.com/caosdb/caosdb-pylib/-/issues/31) +* Tests for [caosdb-server#62](https://gitlab.com/caosdb/caosdb-server/-/issues/62) in caosdb-server-project, i.e., renaming of a RecordType that should be reflected in properties with that RT as datatype. @@ -27,7 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed (for any bug fixes) -- Tests for NaN Double Values (see https://gitlab.com/caosdb/caosdb-server/issues/41) +* Tests for NaN Double Values (see https://gitlab.com/caosdb/caosdb-server/issues/41) * Tests for name queries. [caosdb-server#51](https://gitlab.com/caosdb/caosdb-server/-/issues/51) ### Security (in case of vulnerabilities) diff --git a/resources/ok_anonymous b/resources/ok_anonymous new file mode 100755 index 0000000000000000000000000000000000000000..fdc5d87618d9c6b229febd4e25ec7d7ec03347cb --- /dev/null +++ b/resources/ok_anonymous @@ -0,0 +1,3 @@ +#!/bin/bash + +echo "ok_anonymous" diff --git a/resources/simple_script.py b/resources/simple_script.py deleted file mode 100755 index 71bd9c05b4e86133cc356e1c15359701642a9486..0000000000000000000000000000000000000000 --- a/resources/simple_script.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# ** header v3.0 -# This file is a part of the CaosDB Project. -# -# Copyright (C) 2018 Research Group Biomedical Physics, -# Max-Planck-Institute for Dynamics and Self-Organization Göttingen -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <https://www.gnu.org/licenses/>. -# -# ** end header -# -"""server_side_script.py. - -An example which implements a minimal server-side script. - -1) This script expects to find a *.txt file in the .upload_files dir which is -printed to stdout. - -2) It executes a "Count stars" query and prints the result to stdout. - -3) It will return with code 0 if everything is ok, or with any code that is -specified with the commandline option --exit -""" - -import sys -from os import listdir -from caosdb import configure_connection, execute_query - - -# parse --auth-token option and configure connection -CODE = 0 -QUERY = "COUNT stars" -for arg in sys.argv: - if arg.startswith("--auth-token="): - auth_token = arg[13:] - configure_connection(auth_token=auth_token) - if arg.startswith("--exit="): - CODE = int(arg[7:]) - if arg.startswith("--query="): - QUERY = arg[8:] - - -############################################################ -# 1 # find and print *.txt file ############################ -############################################################ - -try: - for fname in listdir(".upload_files"): - if fname.endswith(".txt"): - with open(".upload_files/{}".format(fname)) as f: - print(f.read()) -except FileNotFoundError: - pass - - -############################################################ -# 2 # query "COUNT stars" ################################## -############################################################ - -RESULT = execute_query(QUERY) -print(RESULT) - -############################################################ -# 3 ######################################################## -############################################################ - -sys.exit(CODE) diff --git a/tests/test_administration.py b/tests/test_administration.py index 0d4f4901c084474c0d703f2100f8ca16a0b1ea45..c951307472bc5a5eb7f2cea3b6da12de921bc0cd 100644 --- a/tests/test_administration.py +++ b/tests/test_administration.py @@ -27,7 +27,7 @@ """ from caosdb import (administration as admin, get_config) -from nose.tools import assert_true, assert_equal, assert_is_not_none, with_setup, assert_raises +from nose.tools import assert_true, assert_equal, assert_is_not_none, assert_raises from caosdb.exceptions import (ClientErrorException, HTTPAuthorizationException, LoginFailedException, @@ -99,12 +99,10 @@ def test_set_server_property(): assert admin.get_server_property("AUTH_OPTIONAL") == "FALSE" -@with_setup(setup, teardown) def test_insert_role_success(): - assert_true(admin._insert_role(name=test_role, description=test_role_desc)) + assert admin._insert_role(name=test_role, description=test_role_desc) -@with_setup(setup, teardown) def test_insert_role_failure_permission(): switch_to_normal_user() with raises(HTTPAuthorizationException) as cm: @@ -112,7 +110,6 @@ def test_insert_role_failure_permission(): assert cm.value.msg == "You are not permitted to insert a new role." -@with_setup(setup, teardown) def test_insert_role_failure_name_duplicates(): test_insert_role_success() with assert_raises(ClientErrorException) as cm: @@ -122,7 +119,6 @@ def test_insert_role_failure_name_duplicates(): "Role name is already in use. Choose a different name.") -@with_setup(setup, teardown) def test_update_role_success(): test_insert_role_success() assert_is_not_none( @@ -132,7 +128,6 @@ def test_update_role_success(): "asdf")) -@with_setup(setup, teardown) def test_update_role_failure_permissions(): test_insert_role_success() switch_to_normal_user() @@ -141,20 +136,17 @@ def test_update_role_failure_permissions(): assert cm.value.msg == "You are not permitted to update this role." -@with_setup(setup, teardown) def test_update_role_failure_non_existing(): with raises(ResourceNotFoundException) as cm: admin._update_role(name=test_role, description=test_role_desc + "asdf") assert cm.value.msg == "Role does not exist." -@with_setup(setup, teardown) def test_delete_role_success(): test_insert_role_success() assert_true(admin._delete_role(name=test_role)) -@with_setup(setup, teardown) def test_delete_role_failure_permissions(): test_insert_role_success() switch_to_normal_user() @@ -163,21 +155,18 @@ def test_delete_role_failure_permissions(): assert cm.value.msg == "You are not permitted to delete this role." -@with_setup(setup, teardown) def test_delete_role_failure_non_existing(): with raises(ResourceNotFoundException) as cm: admin._delete_role(name=test_role) assert cm.value.msg == "Role does not exist." -@with_setup(setup, teardown) def test_retrieve_role_success(): test_insert_role_success() r = admin._retrieve_role(test_role) assert_is_not_none(r) -@with_setup(setup, teardown) def test_retrieve_role_failure_permission(): test_insert_role_success() switch_to_normal_user() @@ -186,14 +175,12 @@ def test_retrieve_role_failure_permission(): assert cm.value.msg == "You are not permitted to retrieve this role." -@with_setup(setup, teardown) def test_retrieve_role_failure_non_existing(): with raises(ResourceNotFoundException) as cm: admin._retrieve_role(name=test_role) assert cm.value.msg == "Role does not exist." -@with_setup(setup, teardown) def test_set_permissions_success(): test_insert_role_success() assert_true( @@ -205,7 +192,6 @@ def test_set_permissions_success(): "BLA:BLA:BLA")])) -@with_setup(setup, teardown) def test_set_permissions_failure_permissions(): test_insert_role_success() switch_to_normal_user() @@ -217,7 +203,6 @@ def test_set_permissions_failure_permissions(): assert cm.value.msg == "You are not permitted to set this role's permissions." -@with_setup(setup, teardown) def test_set_permissions_failure_non_existing(): with raises(ResourceNotFoundException) as cm: admin._set_permissions( @@ -227,7 +212,6 @@ def test_set_permissions_failure_non_existing(): assert cm.value.msg == "Role does not exist." -@with_setup(setup, teardown) def test_get_permissions_success(): test_set_permissions_success() r = admin._get_permissions(role=test_role) @@ -235,7 +219,6 @@ def test_get_permissions_success(): assert_is_not_none(r) -@with_setup(setup, teardown) def test_get_permissions_failure_permissions(): test_set_permissions_success() switch_to_normal_user() @@ -244,14 +227,12 @@ def test_get_permissions_failure_permissions(): assert cm.value.msg == "You are not permitted to retrieve this role's permissions." -@with_setup(setup, teardown) def test_get_permissions_failure_non_existing(): with raises(ResourceNotFoundException) as cm: admin._get_permissions(role="non-existing-role") assert cm.value.msg == "Role does not exist." -@with_setup(setup, teardown) def test_get_roles_success(): test_insert_role_success() r = admin._get_roles(username=test_user) @@ -259,7 +240,6 @@ def test_get_roles_success(): return r -@with_setup(setup, teardown) def test_get_roles_failure_permissions(): test_insert_role_success() switch_to_normal_user() @@ -268,14 +248,12 @@ def test_get_roles_failure_permissions(): assert cm.value.msg == "You are not permitted to retrieve this user's roles." -@with_setup(setup, teardown) def test_get_roles_failure_non_existing(): with raises(ResourceNotFoundException) as cm: admin._get_roles(username="non-existing-user") assert cm.value.msg == "User does not exist." -@with_setup(setup, teardown) def test_set_roles_success(): roles_old = test_get_roles_success() roles = {test_role} @@ -285,7 +263,6 @@ def test_set_roles_success(): assert_is_not_none(admin._set_roles(username=test_user, roles=roles_old)) -@with_setup(setup, teardown) def test_set_roles_success_with_warning(): test_insert_role_success() roles = {test_role} @@ -294,7 +271,6 @@ def test_set_roles_success_with_warning(): assert_is_not_none(admin._set_roles(username=test_user, roles=[])) -@with_setup(setup, teardown) def test_set_roles_failure_permissions(): roles_old = test_get_roles_success() roles = {test_role} @@ -320,7 +296,6 @@ def test_set_roles_failure_non_existing_user(): assert cm.value.msg == "User does not exist." -@with_setup(setup, teardown) def test_insert_user_success(): admin._insert_user( name=test_user + "2", @@ -330,7 +305,6 @@ def test_insert_user_success(): entity=None) -@with_setup(setup, teardown) def test_insert_user_failure_permissions(): switch_to_normal_user() with raises(HTTPAuthorizationException) as cm: @@ -343,7 +317,6 @@ def test_insert_user_failure_permissions(): assert cm.value.msg == "You are not permitted to insert a new user." -@with_setup(setup, teardown) def test_insert_user_failure_name_in_use(): test_insert_user_success() with assert_raises(ClientErrorException) as cm: @@ -351,13 +324,11 @@ def test_insert_user_failure_name_in_use(): assert_equal(cm.exception.msg, "User name is already in use.") -@with_setup(setup, teardown) def test_delete_user_success(): test_insert_user_success() assert_is_not_none(admin._delete_user(name=test_user + "2")) -@with_setup(setup, teardown) def test_delete_user_failure_permissions(): test_insert_user_success() switch_to_normal_user() @@ -366,14 +337,12 @@ def test_delete_user_failure_permissions(): assert cm.value.msg == "You are not permitted to delete this user." -@with_setup(setup, teardown) def test_delete_user_failure_non_existing(): with raises(ResourceNotFoundException) as cm: admin._delete_user(name="non_existing_user") assert cm.value.msg == "User does not exist." -@with_setup(setup, teardown) def test_update_user_success_status(): assert_is_not_none( admin._insert_user( @@ -391,7 +360,6 @@ def test_update_user_success_status(): entity=None) -@with_setup(setup, teardown) def test_update_user_success_email(): assert_is_not_none( admin._insert_user( @@ -409,7 +377,6 @@ def test_update_user_success_email(): entity=None) -@with_setup(setup, teardown) def test_update_user_success_entity(): assert_is_not_none( admin._insert_user( @@ -422,7 +389,6 @@ def test_update_user_success_entity(): status=None, email=None, entity="21") -@with_setup(setup, teardown) def test_update_user_success_password(): assert_is_not_none( admin._insert_user( @@ -440,7 +406,6 @@ def test_update_user_success_password(): entity=None) -@with_setup(setup, teardown) def test_update_user_failure_permissions_status(): assert admin._insert_user(name=test_user + "2", password="secret1P!", @@ -458,7 +423,6 @@ def test_update_user_failure_permissions_status(): assert cm.value.msg == "You are not permitted to update this user." -@with_setup(setup, teardown) def test_update_user_failure_permissions_email(): assert admin._insert_user(name=test_user + "2", password="secret1P!", status="ACTIVE", @@ -475,7 +439,6 @@ def test_update_user_failure_permissions_email(): assert cm.value.msg == "You are not permitted to update this user." -@with_setup(setup, teardown) def test_update_user_failure_permissions_entity(): assert admin._insert_user(name=test_user + "2", password="secret1P!", status="ACTIVE", @@ -492,7 +455,6 @@ def test_update_user_failure_permissions_entity(): assert cm.value.msg == "You are not permitted to update this user." -@with_setup(setup, teardown) def test_update_user_failure_permissions_password(): assert admin._insert_user(name=test_user + "2", password="secret1P!", status="ACTIVE", @@ -509,7 +471,6 @@ def test_update_user_failure_permissions_password(): assert cm.value.msg == "You are not permitted to update this user." -@with_setup(setup, teardown) def test_update_user_failure_non_existing_user(): with raises(ResourceNotFoundException) as cm: admin._update_user( @@ -522,7 +483,6 @@ def test_update_user_failure_non_existing_user(): assert cm.value.msg == "User does not exist." -@with_setup(setup, teardown) def test_update_user_failure_non_existing_entity(): assert admin._insert_user(name=test_user + "2", password="secret1P!", status="ACTIVE", @@ -538,13 +498,11 @@ def test_update_user_failure_non_existing_entity(): assert cm.value.msg == "Entity does not exist." -@with_setup(setup, teardown) def test_retrieve_user_success(): test_insert_user_success() assert_is_not_none(admin._retrieve_user(realm=None, name=test_user + "2")) -@with_setup(setup, teardown) def test_retrieve_user_failure_permissions(): test_insert_user_success() switch_to_normal_user() @@ -553,14 +511,12 @@ def test_retrieve_user_failure_permissions(): assert cm.value.msg == "You are not permitted to retrieve this user." -@with_setup(setup, teardown) def test_retrieve_user_failure_non_existing(): with raises(ResourceNotFoundException) as cm: admin._retrieve_user(realm=None, name="non_existing") assert cm.value.msg == "User does not exist." -@with_setup(setup, teardown) def test_login_with_inactive_user_failure(): assert_is_not_none( admin._insert_user( diff --git a/tests/test_affiliation.py b/tests/test_affiliation.py index f51305cfde4644387727e29c5eab1cfa5cf4407a..df5d8979d02b38b7c06791019babace954283850 100644 --- a/tests/test_affiliation.py +++ b/tests/test_affiliation.py @@ -49,10 +49,9 @@ file_path = "testfile.dat" def setup(): - try: - db.execute_query("FIND Test*").delete() - except Exception as e: - print(e) + d = db.execute_query("FIND ENTITY WITH ID > 99") + if len(d) > 0: + d.delete() db.RecordType(name=recty_name).insert() db.Record(name=rec_name).add_parent(name=recty_name).insert() db.File(name=file_name, file=file_path, path="testfile.dat").insert() diff --git a/tests/test_authentication.py b/tests/test_authentication.py index dc862c4e1c358376a039a8a50d0863fca2d5a112..ce2a095bab5f6b2d70afbea1a2a96af624c71389 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -5,6 +5,8 @@ # # Copyright (C) 2018 Research Group Biomedical Physics, # Max-Planck-Institute for Dynamics and Self-Organization Göttingen +# Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> +# Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -21,40 +23,45 @@ # # ** end header # -"""Created on 20.01.2015. - -@author: tf -""" import os +import time from sys import hexversion from urllib.parse import urlparse from http.client import HTTPSConnection import ssl -from subprocess import call, check_output +from subprocess import call from lxml import etree -from pytest import skip +from pytest import raises, mark from caosdb.exceptions import LoginFailedException -import caosdb as h -from nose.tools import (assert_false, assert_true, assert_is_none, - assert_raises, assert_equal, assert_is_not_none, - nottest, with_setup) -from caosdb.connection.connection import _Connection +import caosdb as db +from .test_server_side_scripting import request + + +_USED_OTA_TOKEN = set() def setup(): - try: - h.execute_query("FIND Test*").delete() - except Exception as e: - print(e) + db.configure_connection() + + # deactivate anonymous user + db.administration.set_server_property("AUTH_OPTIONAL", "FALSE") + d = db.execute_query("FIND Test*") + if len(d) > 0: + d.delete() + + +def teardown(): + setup() +@mark.skipif( + not db.get_config().has_option("Connection", "password_method") + or not db.get_config().get("Connection", "password_method") == "pass", + reason="password_method is not pass") def test_pass(): - if not h.get_config().has_option("Connection", "password_method") or not h.get_config( - ).get("Connection", "password_method") == "pass": - skip() - assert call(["pass", h.get_config().get("Connection", - "password_identifier")]) == 0 + assert call(["pass", db.get_config().get("Connection", + "password_identifier")]) == 0 def test_https_support(): @@ -65,9 +72,9 @@ def test_https_support(): context.verify_mode = ssl.CERT_REQUIRED if hasattr(context, "check_hostname"): context.check_hostname = True - context.load_verify_locations(h.get_config().get("Connection", "cacert")) + context.load_verify_locations(db.get_config().get("Connection", "cacert")) - url = h.get_config().get("Connection", "url") + url = db.get_config().get("Connection", "url") fullurl = urlparse(url) http_con = HTTPSConnection( @@ -79,27 +86,35 @@ def test_https_support(): def test_login_via_post_form_data_failure(): - with assert_raises(LoginFailedException) as cm: - h.get_connection().post_form_data( + with raises(LoginFailedException): + db.get_connection().post_form_data( "login", { - "username": h.get_config().get("Connection", "username"), + "username": db.get_config().get("Connection", "username"), "password": "wrongpassphrase" }) +def test_anonymous_not_returning_auth_token(): + # activate anonymous user + db.administration.set_server_property("AUTH_OPTIONAL", "TRUE") + + response = request(method="GET", headers={}, path="Entity") + assert response.getheader("Set-Cookie") is None # no auth token returned + + def test_anonymous_setter(): - """ this test verifies that the "test_login_while_anonymous_is_active" is + """This test verifies that the "test_login_while_anonymous_is_active" is effective.""" # activate anonymous user - h.administration.set_server_property("AUTH_OPTIONAL", "TRUE") + db.administration.set_server_property("AUTH_OPTIONAL", "TRUE") # connect without auth-token context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) context.verify_mode = ssl.CERT_REQUIRED - context.load_verify_locations(h.get_config().get("Connection", "cacert")) + context.load_verify_locations(db.get_config().get("Connection", "cacert")) - url = h.get_config().get("Connection", "url") + url = db.get_config().get("Connection", "url") fullurl = urlparse(url) http_con = HTTPSConnection( @@ -113,15 +128,14 @@ def test_anonymous_setter(): assert xml.xpath("/Response/UserInfo/Roles/Role")[0].text == "anonymous" -@with_setup(setup, setup) def test_login_while_anonymous_is_active(): # activate anonymous user - h.administration.set_server_property("AUTH_OPTIONAL", "TRUE") + db.administration.set_server_property("AUTH_OPTIONAL", "TRUE") # logout - h.get_connection()._logout() + db.get_connection()._logout() - body = h.get_connection().retrieve( + body = db.get_connection().retrieve( entity_uri_segments=["Entity"], reconnect=True).read() xml = etree.fromstring(body) @@ -129,3 +143,176 @@ def test_login_while_anonymous_is_active(): # pylib did the login even though the anonymous user is active assert xml.xpath( "/Response/UserInfo/Roles/Role")[0].text == "administration" + + +def test_authtoken_config(): + assert db.administration.get_server_property( + "AUTHTOKEN_CONFIG") == "conf/core/authtoken.example.yaml" + + +def get_one_time_token(testcase): + username = "anonymous" + realm = "OneTimeAuthenticationToken" + roles = ["administration"] + permissions = [] + filename = db.get_config().get("IntegrationTests", + "test_authentication.{}".format(testcase)) + assert os.path.isdir(os.path.split(filename)[0]) + assert os.path.isfile(filename) + with open(filename, "r") as f: + auth_token = f.read() + while auth_token in _USED_OTA_TOKEN: + # wait until the server has renewed the token + time.sleep(1) + with open(filename, "r") as f: + auth_token = f.read() + else: + _USED_OTA_TOKEN.add(auth_token) + + assert auth_token.startswith('["O","{re}","{us}",["{rol}"],{perm},'.format( + re=realm, us=username, rol=",".join(roles), perm=permissions)) + return auth_token + + +def test_one_time_token(): + assert db.Info().user_info.roles == ["administration"] + assert db.Info().user_info.name == db.get_config().get("Connection", "username") + + auth_token = get_one_time_token("admin_token_crud") + db.configure_connection(password_method="auth_token", + auth_token=auth_token) + + assert db.Info().user_info.roles == ["administration"] + assert db.Info().user_info.name == "anonymous" + assert db.Info().user_info.realm == "OneTimeAuthenticationToken" + + db.configure_connection() + + assert db.Info().user_info.roles == ["administration"] + assert db.Info().user_info.name == db.get_config().get("Connection", + "username") + + +def test_one_time_token_invalid(): + auth_token = get_one_time_token("admin_token_crud") + auth_token = auth_token.replace("[]", '["permission"]') + + db.configure_connection(password_method="auth_token", + auth_token=auth_token) + with raises(db.LoginFailedException) as lfe: + db.Info() + assert lfe.value.args[0] == ( + "The authentication token is expired or you have been logged out otherwise. The auth_token " + "authenticator cannot log in again. You must provide a new authentication token.") + + # also raises exception when anonymous is enabled + db.configure_connection() + db.administration.set_server_property("AUTH_OPTIONAL", "TRUE") + db.configure_connection(password_method="auth_token", + auth_token=auth_token) + with raises(db.LoginFailedException) as lfe: + db.Info() + assert lfe.value.args[0] == ( + "The authentication token is expired or you have been logged out otherwise. The auth_token " + "authenticator cannot log in again. You must provide a new authentication token.") + + +def test_one_time_token_expired(): + auth_token = get_one_time_token("admin_token_expired") + db.configure_connection(password_method="auth_token", + auth_token=auth_token) + with raises(db.LoginFailedException) as lfe: + db.Info() + assert lfe.value.args[0] == ( + "The authentication token is expired or you have been logged out otherwise. The auth_token " + "authenticator cannot log in again. You must provide a new authentication token.") + + # also raises exception when anonymous is enabled + db.configure_connection() + db.administration.set_server_property("AUTH_OPTIONAL", "TRUE") + db.configure_connection(password_method="auth_token", + auth_token=auth_token) + with raises(db.LoginFailedException) as lfe: + db.Info() + assert lfe.value.args[0] == ( + "The authentication token is expired or you have been logged out otherwise. The auth_token " + "authenticator cannot log in again. You must provide a new authentication token.") + + +def test_one_time_token_3_attempts(): + auth_token = get_one_time_token("admin_token_3_attempts") + + # 1st + db.configure_connection(password_method="auth_token", + auth_token=auth_token) + assert db.get_connection()._authenticator.auth_token == auth_token + assert db.Info().user_info.roles == ["administration"] + assert db.get_connection()._authenticator.auth_token != auth_token + assert db.Info().user_info.name == "anonymous" + assert db.Info().user_info.realm == "OneTimeAuthenticationToken" + + # 2nd + db.configure_connection(password_method="auth_token", + auth_token=auth_token) + assert db.get_connection()._authenticator.auth_token == auth_token + assert db.Info().user_info.roles == ["administration"] + assert db.get_connection()._authenticator.auth_token != auth_token + assert db.Info().user_info.name == "anonymous" + assert db.Info().user_info.realm == "OneTimeAuthenticationToken" + + # 3rd + db.configure_connection(password_method="auth_token", + auth_token=auth_token) + assert db.get_connection()._authenticator.auth_token == auth_token + assert db.Info().user_info.roles == ["administration"] + assert db.get_connection()._authenticator.auth_token != auth_token + assert db.Info().user_info.name == "anonymous" + assert db.Info().user_info.realm == "OneTimeAuthenticationToken" + + # 4th + db.configure_connection(password_method="auth_token", + auth_token=auth_token) + assert db.get_connection()._authenticator.auth_token == auth_token + with raises(db.LoginFailedException) as lfe: + db.Info() + assert lfe.value.args[0] == ( + "The authentication token is expired or you have been logged out otherwise. The auth_token " + "authenticator cannot log in again. You must provide a new authentication token.") + + db.configure_connection() + db.administration.set_server_property("AUTH_OPTIONAL", "TRUE") + + # 5th attempt, also raises error when anonymous user is enabled + db.configure_connection(password_method="auth_token", + auth_token=auth_token) + assert db.get_connection()._authenticator.auth_token == auth_token + with raises(db.LoginFailedException) as lfe: + db.Info() + assert lfe.value.args[0] == ( + "The authentication token is expired or you have been logged out otherwise. The auth_token " + "authenticator cannot log in again. You must provide a new authentication token.") + + +def test_crud_with_one_time_token(): + auth_token = get_one_time_token("admin_token_crud") + db.configure_connection(password_method="auth_token", + auth_token=auth_token) + + # CREATE + rt = db.RecordType(name="TestRT") + rt.insert() + assert rt.id == db.execute_query("FIND TestRT", unique=True).id + + # UPDATE + assert db.execute_query("FIND TestRT", unique=True).description is None + rt.description = "new desc" + rt.update() + assert rt.description == db.execute_query( + "FIND TestRT", unique=True).description + + # RETRIEVE + rt.retrieve() + assert rt.id == db.execute_query("FIND TestRT", unique=True).id + + rt.delete() + assert len(db.execute_query("FIND TestRT")) == 0 diff --git a/tests/test_datatype.py b/tests/test_datatype.py index 3a276b04137a4be3f25e1d7f423116331e091dd7..a996004167bba6bb5bb0f12387b53381036851e5 100644 --- a/tests/test_datatype.py +++ b/tests/test_datatype.py @@ -26,8 +26,6 @@ @author: tf """ import caosdb as db -# @UnresolvedImport -from nose.tools import nottest, with_setup, assert_true, assert_equal from pytest import raises @@ -45,7 +43,6 @@ def teardown(): pass -@with_setup(setup, teardown) def test_override_with_non_existing_ref(): rt1 = db.RecordType("TestRecordType1").insert() rt2 = db.RecordType("TestRecordType2").insert() @@ -62,7 +59,6 @@ def test_override_with_non_existing_ref(): "Referenced entity does not exist.") -@with_setup(setup, teardown) def test_override_with_existing_ref(): rt1 = db.RecordType("TestRecordType1").insert() rt2 = db.RecordType("TestRecordType2").insert() @@ -75,10 +71,9 @@ def test_override_with_existing_ref(): datatype=rt2, value="TestRecord").add_parent(rt1).insert() - assert_true(rec2.is_valid()) + assert rec2.is_valid() is True -@with_setup(setup, teardown) def test_reference_datatype_sequencial(): rt = db.RecordType(name="TestRT", description="TestRTDesc").insert() p = db.Property( @@ -86,22 +81,21 @@ def test_reference_datatype_sequencial(): description="RefOnTestRT", datatype="TestRT").insert() - assert_true(p.is_valid()) + assert p.is_valid() is True dt = db.execute_query("FIND TestProp", unique=True).datatype - assert_equal(dt, "TestRT") + assert dt == "TestRT" rt2 = db.RecordType( "TestRT2", description="TestRT2Desc").add_property( name="TestProp", value="TestRT").insert() - assert_true(rt2.is_valid()) - assert_equal(rt2.get_property("TestProp").value, rt.id) - assert_equal(rt2.get_property("TestProp").datatype, "TestRT") + assert rt2.is_valid() is True + assert rt2.get_property("TestProp").value == rt.id + assert rt2.get_property("TestProp").datatype == "TestRT" -@with_setup(setup, teardown) def test_reference_datatype_at_once(): rt = db.RecordType(name="TestRT") rt2 = db.RecordType(name="TestRT2").add_property(name="TestProp") @@ -113,10 +107,9 @@ def test_reference_datatype_at_once(): p = db.Property(name="TestProp", datatype="TestRT") c = db.Container().extend([rt, rt2, rec, rec2, p]).insert() - assert_true(c.is_valid()) + assert c.is_valid() is True -@with_setup(setup, teardown) def test_generic_reference_success(): rt1 = db.RecordType(name="TestRT1").insert() rt2 = db.RecordType(name="TestRT2").insert() @@ -128,14 +121,13 @@ def test_generic_reference_success(): name="TestP1", value=rec1.id).insert() - assert_true(rt1.is_valid()) - assert_true(rt2.is_valid()) - assert_true(p.is_valid()) - assert_true(rec1.is_valid()) - assert_true(rec2.is_valid()) + assert rt1.is_valid() is True + assert rt2.is_valid() is True + assert p.is_valid() is True + assert rec1.is_valid() is True + assert rec2.is_valid() is True -@with_setup(setup, teardown) def test_generic_reference_failure(): db.RecordType(name="TestRT2").insert() db.Property(name="TestP1", datatype=db.REFERENCE).insert() @@ -144,20 +136,16 @@ def test_generic_reference_failure(): name="TestRT2").add_property( name="TestP1", value="asdf") - with raises(db.TransactionError): - rec2.insert() + raises(db.TransactionError, rec2.insert) -@with_setup(setup, teardown) def test_unknown_datatype1(): p = db.Property(name="TestP", datatype="Non-Existing") with raises(db.TransactionError) as te: p.insert() - assert_true(False) assert te.value.get_errors()[0].msg == "Unknown datatype." -@with_setup(setup, teardown) def test_unknown_datatype2(): p = db.Property(name="TestP", datatype="12345687654334567") with raises(db.TransactionError) as te: @@ -165,7 +153,6 @@ def test_unknown_datatype2(): assert te.value.get_errors()[0].msg == "Unknown datatype." -@with_setup(setup, teardown) def test_unknown_datatype3(): p = db.Property(name="TestP", datatype="-134") with raises(db.TransactionError) as te: @@ -173,7 +160,6 @@ def test_unknown_datatype3(): assert te.value.get_errors()[0].msg == "Unknown datatype." -@with_setup(setup, teardown) def test_wrong_refid(): rt1 = db.RecordType(name="TestRT1").insert() rt2 = db.RecordType(name="TestRT2").insert() @@ -192,11 +178,31 @@ def test_wrong_refid(): value=rec2.id) with raises(db.TransactionError): rec3.insert() - assert (rec3.get_property("TestP1").get_errors()[0].description == - 'Reference not qualified. The value of this Reference Property is to be a child of its data type.') + except db.TransactionError: + desc = ('Reference not qualified. The value of this Reference ' + 'Property is to be a child of its data type.') + err = rec3.get_property("TestP1").get_errors()[0] + assert err.description == desc rec4 = db.Record().add_parent( name="TestRT3").add_property( name="TestP1", value=rec1.id).insert() assert rec4.is_valid() + + +def test_datatype_mismatch_in_response(): + p = db.Property("TestDoubleProperty", datatype=db.DOUBLE).insert() + + with raises(ValueError) as exc: + rt = db.RecordType().add_property(p, "not a double") + assert exc.value.args[0] == ("could not convert string to " + "float: 'not a double'") + + # add property by name, + rt = db.RecordType("TestEntity").add_property(p.name, "not a double") + + with raises(db.TransactionError) as exc: + # should not raise ValueError but transaction error. + rt.insert() + assert exc.value.get_errors()[0].msg == "Cannot parse value to double." diff --git a/tests/test_properties.py b/tests/test_properties.py new file mode 100644 index 0000000000000000000000000000000000000000..9219501f09a5ee5eff4b89a78f17f8e02b842151 --- /dev/null +++ b/tests/test_properties.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +# +# ** header v3.0 +# This file is a part of the CaosDB Project. +# +# Copyright (c) 2020 IndiScale GmbH <www.indiscale.com> +# Copyright (c) 2020 Timm Fitschen <t.fitschen@indiscale.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +# ** end header +import pytest + +import caosdb as db +from caosdb.common.utils import xml2str + + +def setup_module(): + d = db.execute_query("FIND Test*") + if len(d) > 0: + d.delete() + + +def setup(): + setup_module() + + +def teardown(): + setup_module() + + +@pytest.mark.xfail(reason="""see pylib issue #31""") +def test_add_properties_with_wrong_role(): + p = db.Property(name="TestProperty1", datatype=db.TEXT).insert() + rt = db.RecordType(name="TestRT1").add_property("TestProperty1").insert() + + wrong_role = db.Record(name="TestRT1").retrieve() + right_role = db.RecordType(name="TestRT1").retrieve() + rec = db.Record(name="fail").add_property(wrong_role) + rec2 = db.Record(name="ok").add_property(right_role) + + assert wrong_role.get_property("TestProperty1") is not None + assert str(wrong_role.get_property("TestProperty1")) == str( + right_role.get_property("TestProperty1")) + + xml = rec2.to_xml() + assert not xml.xpath("/Record/Property/Property") + + xml = rec.to_xml() + # TODO this fails because the Record is treated differently than the + # RecordType. + assert not xml.xpath("/Record/Property/Property") diff --git a/tests/test_query.py b/tests/test_query.py index 9b188d9c94f964508ef254506ea19783fd02b5b6..8432b37d92c87d772ff07c0b4772def48b6aaab1 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -1002,3 +1002,25 @@ def test_query_by_name(): assert len(h.execute_query("FIND TestRT WITH name LIKE 'TestRec*'")) == 2 assert len(h.execute_query("FIND TestRT WITH name LIKE '*Rec*'")) == 2 assert len(h.execute_query("FIND ENTITY WITH name LIKE 'TestRec*'")) == 2 + + +@mark.xfail(reason="Issue: https://gitlab.com/caosdb/caosdb-server/-/issues/96") +def test_referenced_as(): + rt_person = db.RecordType("TestPerson").insert() + db.RecordType("TestParty").insert() + + # three persons + g1 = db.Record().add_parent("TestPerson").insert() + g2 = db.Record().add_parent("TestPerson").insert() + g3 = db.Record().add_parent("TestPerson").insert() + guest_ids = set(g1.id, g2.id, g3.id) + + party = db.Record( + "Diamond Jubilee of Elizabeth II").add_parent("TestParty") + party.add_property(rt_person, datatype=db.LIST(rt_person), name="Guests", + value=[g1, g2, g3]) + party.insert() + guests = db.execute_query("FIND RECORD TestPerson WHICH IS REFERENCED BY " + "TestParty AS Guests") + assert len(guests) == 3 + assert set([e.id for e in guests]) == guest_ids diff --git a/tests/test_select.py b/tests/test_select.py index 748b85dd07d85a32e5e4643f562204433639da04..66c3992d1519f101b7b18941924b9bd939fa0066 100644 --- a/tests/test_select.py +++ b/tests/test_select.py @@ -25,123 +25,304 @@ @author: tf """ -import caosdb as h - -# @UnresolvedImport -from nose.tools import assert_true, assert_equal, assert_is_not_none, assert_is_none +import caosdb as db def setup_module(): - print("SETUP") teardown_module() - h.Property(name="TestPropertyOne", datatype=h.TEXT).insert() - h.Property( + db.Property(name="TestPropertyOne", datatype=db.TEXT).insert() + db.Property( name="TestPropertyTwo", description="Desc2", - datatype=h.TEXT).insert() - h.RecordType( + datatype=db.TEXT).insert() + db.RecordType( name="TestRecordType", description="DescRecTy").add_property( - name="TestPropertyOne").add_property( - name="TestPropertyTwo").insert() + name="TestPropertyOne", value="v1").add_property( + name="TestPropertyTwo", value="v2").insert() + rt_house = db.RecordType("TestHouse", description="TestHouseDesc").insert() + db.RecordType("TestWindow").insert() + rt_person = db.RecordType("TestPerson", + description="TestPersonDesc").insert() + db.RecordType("TestParty", description="TestPartyDesc").insert() + db.Property("TestHeight", description="TestHeightDesc", datatype=db.DOUBLE, + unit="ft").insert() + db.Property("TestDate", description="TestDateDesc", + datatype=db.DATETIME).insert() + + window = db.Record("Window1", + description="Window1Desc").add_parent("TestWindow") + window.add_property("TestHeight", 20.5, unit="ft") + window.insert() + + owner = db.Record("The Queen").add_parent("TestPerson").insert() + + house = db.Record("Buckingham Palace") + house.description = "A rather large house" + house.add_parent("TestHouse") + house.add_property(rt_person, name="TestOwner", value=owner) + house.add_property("TestWindow", window).insert() + + g1 = db.Record().add_parent("TestPerson").insert() + g2 = db.Record().add_parent("TestPerson").insert() + g3 = db.Record().add_parent("TestPerson").insert() + + party = db.Record( + "Diamond Jubilee of Elizabeth II").add_parent("TestParty") + party.add_property(rt_house, name="Location", value=house) + party.add_property("TestDate", "2012-02-06") + party.add_property(rt_person, datatype=db.LIST(rt_person), name="Guests", + value=[g1, g2, g3]) + party.insert() def teardown_module(): - print("TEARDOWN") - try: - h.execute_query("FIND Test*").delete() - except BaseException: - pass + d = db.execute_query("FIND Test*") + + if len(d) > 0: + d.delete() def test_id1(): - p1 = h.execute_query("FIND TestPropertyOne", unique=True) - assert_true(p1.is_valid()) - assert_is_not_none(p1.name) - assert_is_not_none(p1.datatype) - assert_is_none(p1.description) + p1 = db.execute_query("FIND TestPropertyOne", unique=True) + assert p1.is_valid() is True + assert p1.name is not None + assert p1.datatype is not None + assert p1.description is None - p1_c = h.execute_query("SELECT id FROM TestPropertyOne", unique=True) - assert_true(p1_c.is_valid()) - assert_equal(p1_c.id, p1.id) - assert_is_none(p1_c.name) - assert_is_none(p1_c.datatype) - assert_is_none(p1_c.description) + p1_c = db.execute_query("SELECT id FROM TestPropertyOne", unique=True) + assert p1_c.is_valid() is True + assert p1_c.id == p1.id + assert p1_c.name is not None, "Name is always included" + assert p1_c.datatype is None + assert p1_c.description is None def test_id2(): - p2 = h.execute_query("FIND TestPropertyTwo", unique=True) - assert_true(p2.is_valid()) - assert_is_not_none(p2.name) - assert_is_not_none(p2.datatype) - assert_is_not_none(p2.description) + p2 = db.execute_query("FIND TestPropertyTwo", unique=True) + assert p2.is_valid() is True + assert p2.name is not None + assert p2.datatype is not None + assert p2.description is not None - p2_c = h.execute_query("SELECT id FROM TestPropertyTwo", unique=True) - assert_true(p2_c.is_valid()) - assert_equal(p2_c.id, p2.id) - assert_is_none(p2_c.name) - assert_is_none(p2_c.datatype) - assert_is_none(p2_c.description) + p2_c = db.execute_query("SELECT id FROM TestPropertyTwo", unique=True) + assert p2_c.is_valid() is True + assert p2_c.id == p2.id + assert p2_c.name is not None, "Name is always included" + assert p2_c.datatype is None + assert p2_c.description is None def test_id3(): - p3s = h.execute_query("SELECT description FROM TestProperty*") - assert_equal(len(p3s), 2) + p3s = db.execute_query("SELECT description FROM TestProperty*") + assert len(p3s) == 2 + for e in p3s: - assert_is_not_none(e.id) + assert e.id is not None def test_name1(): - p3s = h.execute_query("SELECT description FROM TestProperty*") - assert_equal(len(p3s), 2) + p3s = db.execute_query("SELECT description FROM TestProperty*") + assert len(p3s) == 2 + for e in p3s: - assert_is_not_none(e.name) + assert e.name is not None def test_name2(): - p3s = h.execute_query("SELECT name FROM TestProperty*") - assert_equal(len(p3s), 2) + p3s = db.execute_query("SELECT name FROM TestProperty*") + assert len(p3s) == 2 + for e in p3s: - assert_is_not_none(e.name) - assert_is_none(e.description) + assert e.name is not None + assert e.description is None def test_multi1(): - p1 = h.execute_query( + p1 = db.execute_query( "SELECT id, name, description FROM TestPropertyOne", unique=True) - assert_is_not_none(p1.id) - assert_equal(p1.name, "TestPropertyOne") - assert_is_none(p1.description) + assert p1.id is not None + assert p1.name == "TestPropertyOne" + assert p1.description is None - p2 = h.execute_query( + p2 = db.execute_query( "SELECT id, name, description FROM TestPropertyTwo", unique=True) - assert_is_not_none(p2.id) - assert_equal(p2.name, "TestPropertyTwo") - assert_equal(p2.description, "Desc2") + assert p2.id is not None + assert p2.name == "TestPropertyTwo" + assert p2.description == "Desc2" def test_sub1(): - rt = h.execute_query("FIND TestRecordType", unique=True) - assert_is_not_none(rt.id) - assert_is_not_none(rt.name) - assert_is_not_none(rt.get_property("TestPropertyOne")) + rt = db.execute_query("FIND TestRecordType", unique=True) + assert rt.id is not None + assert rt.name is not None + assert rt.get_property("TestPropertyOne") is not None - rt = h.execute_query( + rt = db.execute_query( "SELECT TestPropertyOne FROM TestRecordType", unique=True) - assert_is_not_none(rt.id) - assert_is_not_none(rt.name) - assert_is_not_none(rt.get_property("TestPropertyOne")) + assert rt.id is not None + assert rt.name is not None + assert rt.get_property("TestPropertyOne") is not None + assert rt.get_property("TestPropertyOne").value == "v1" + assert rt.get_property("TestPropertyTwo") is None def test_sub2(): - rt = h.execute_query( + rt = db.execute_query( "SELECT TestPropertyTwo.description FROM TestRecordType", unique=True) - assert_is_not_none(rt.id) - assert_is_not_none(rt.name) - assert_is_not_none(rt.get_property("TestPropertyTwo")) - assert_equal(rt.get_property("TestPropertyTwo").description, "Desc2") - assert_is_none(rt.get_property("TestPropertyTwo").datatype) + assert rt.id is not None + assert rt.name is not None + assert rt.get_property("TestPropertyTwo") is not None + assert rt.get_property("TestPropertyTwo").description == "Desc2" + assert rt.get_property("TestPropertyTwo").datatype is None + assert rt.get_property("TestPropertyTwo").value is None + + +def test_subref(): + window = db.execute_query("FIND RECORD TestWindow", unique=True) + s = db.execute_query("SELECT name, TestWindow.TestHeight.value, " + "TestWindow.TestHeight.unit FROM RECORD TestHouse") + + assert len(s) == 1 + + row = s.get_property_values("name", "TestWindow")[0] + assert row[0] == "Buckingham Palace" + assert row[1] == window.id + + row = s.get_property_values("name", ("TestWindow", "TestHeight"))[0] + assert row[0] == "Buckingham Palace" + assert row[1] == 20.5 + + row = s.get_property_values( + "name", ("TestWindow", "TestHeight", "unit"))[0] + assert row[0] == "Buckingham Palace" + assert row[1] == "ft" + + +def test_subref_deep(): + p = db.execute_query( + "SELECT name, Testdate, location, location.TestWindow.Testheight FROM " + "RECORD TestParty", unique=True) + row = p.get_property_values("name", "Testdate", + ("location", "Testwindow", "Testheight")) + assert row == ("Diamond Jubilee of Elizabeth II", "2012-02-06", 20.5) + + +def test_select_list(): + guests = db.execute_query( + "FIND RECORD TestPerson WHICH IS REFERENCED BY TestParty") + s = db.execute_query("SELECT guests FROM RECORD TestParty", unique=True) + + column = s.get_property_values("guests")[0] + assert len(column) == len(guests) + + for eid in [e.id for e in guests]: + assert eid in column + + +def test_select_unit(): + s = db.execute_query("SELECT unit FROM RECORD TestHouse", unique=True) + column = s.get_property_values("unit") + assert column == (None,) + + s = db.execute_query("SELECT unit FROM PROPERTY TestHeight", unique=True) + column = s.get_property_values("unit") + assert column == ("ft",) + + s = db.execute_query("SELECT TestWindow.TestHeight.unit FROM " + "RECORD TestHouse", unique=True) + column = s.get_property_values(("TestWindow", "TestHeight", "unit")) + assert column == ("ft",) + + s = db.execute_query("SELECT TestHeight.unit.TestWindow FROM " + "RECORD TestWindow", unique=True) + column = s.get_property_values(("TestHeight", "unit", "TestWindow")) + assert column == (None,) + + +def test_select_description(): + s = db.execute_query("SELECT description FROM RECORD TestPerson") + column = s.get_property_values("description") + assert column == [(None,), (None,), (None,), (None,)] + + s = db.execute_query("SELECT description" + "FROM RECORD TestHouse", unique=True) + column = s.get_property_values(("description")) + assert column == ("A rather large house",) + + s = db.execute_query("SELECT location.description" + "FROM RECORD TestParty", unique=True) + column = s.get_property_values(("location", "description")) + assert column == ("A rather large house",) + + s = db.execute_query("SELECT TestHeight.description FROM " + "RECORD TestWindow", unique=True) + column = s.get_property_values(("TestHeight", "description")) + assert column == ('TestHeightDesc',) + + s = db.execute_query("SELECT TestWindow.TestHeight.description FROM " + "RECORD TestHouse", unique=True) + column = s.get_property_values(("TestWindow", "TestHeight", "description")) + assert column == ('TestHeightDesc',) + + s = db.execute_query("SELECT TestHeight.description.TestWindow FROM " + "RECORD TestWindow", unique=True) + column = s.get_property_values(("TestHeight", "description", "TestWindow")) + assert column == (None,) + + +def test_select_id(): + house_id = db.execute_query("FIND RECORD TestHouse", unique=True).id + s = db.execute_query("SELECT id FROM RECORD TestHouse", unique=True) + column = s.get_property_values("id") + assert column == (house_id,) + + s = db.execute_query( + "SELECT location.id FROM RECORD TestHouse", + unique=True) + column = s.get_property_values("id") + assert column == (house_id,) + + height_id = db.execute_query("FIND PROPERTY TestHeight", unique=True).id + s = db.execute_query("SELECT id FROM PROPERTY TestHeight", unique=True) + column = s.get_property_values("id") + assert column == (height_id,) + + s = db.execute_query("SELECT TestWindow.TestHeight.id FROM " + "RECORD TestHouse", unique=True) + column = s.get_property_values(("TestWindow", "TestHeight", "id")) + assert column == (height_id,) + + s = db.execute_query("SELECT TestHeight.id.TestWindow FROM " + "RECORD TestWindow", unique=True) + column = s.get_property_values(("TestHeight", "id", "TestWindow")) + assert column == (None,) + + +def test_select_name(): + s = db.execute_query("SELECT name FROM RECORD TestHouse", unique=True) + column = s.get_property_values("name") + assert column == ("Buckingham Palace",) + + s = db.execute_query("SELECT location.name FROM RECORD TestHouse", + unique=True) + column = s.get_property_values("name") + assert column == ("Buckingham Palace",) + + s = db.execute_query("SELECT name FROM PROPERTY TestHeight", unique=True) + column = s.get_property_values("name") + assert column == ("TestHeight",) + + s = db.execute_query("SELECT TestWindow.TestHeight.name FROM " + "RECORD TestHouse", unique=True) + column = s.get_property_values(("TestWindow", "TestHeight", "name")) + assert column == ("TestHeight",) + + s = db.execute_query("SELECT TestHeight.name.TestWindow FROM " + "RECORD TestWindow", unique=True) + column = s.get_property_values(("TestHeight", "name", "TestWindow")) + assert column == (None,) diff --git a/tests/test_server_side_scripting.py b/tests/test_server_side_scripting.py index 74c877bd5d5e3a056e659aa49ff6f3950a834429..cff2017861e7779da9ded0d5541a77ce3a866751 100644 --- a/tests/test_server_side_scripting.py +++ b/tests/test_server_side_scripting.py @@ -26,34 +26,67 @@ Integration tests for the implementation of the server-side-scripting api. """ from __future__ import print_function, unicode_literals -from pytest import raises, mark +import os +from pytest import raises +import json from lxml import etree -from caosdb import get_connection, get_config +from http.client import HTTPSConnection +import ssl +from caosdb import get_connection, get_config, Info, execute_query, RecordType from caosdb.exceptions import (ClientErrorException, ResourceNotFoundException) from caosdb.connection.encode import MultipartParam, multipart_encode +from caosdb.connection.utils import urlencode, urlparse from caosdb import administration as admin +from caosdb.utils.server_side_scripting import run_server_side_script -_TEST_SCRIPTS = ["not_executable", "ok", "err", "simple_script.py"] -_SERVER_SIDE_SCRIPTING_BIN_DIR = get_config().get( +_TEST_SCRIPTS = ["not_executable", "ok", "err", "ok_anonymous"] +_SERVER_SIDE_SCRIPTING_BIN_DIR_LOCAL = get_config().get( "IntegrationTests", - "test_server_side_scripting.bin_dir") + "test_server_side_scripting.bin_dir.local") +_SERVER_SIDE_SCRIPTING_BIN_DIR_SERVER = get_config().get( + "IntegrationTests", + "test_server_side_scripting.bin_dir.server") _TEST_SCRIPTS_DIR = "./resources/" _REMOVE_FILES_AFTERWARDS = [] +_ORIGINAL_SERVER_SCRIPTING_BIN_DIR = "" + + +def clean_database(): + admin._set_permissions("anonymous", []) + d = execute_query("FIND ENTITY WITH ID > 99") + if len(d) > 0: + d.delete() + + +def setup(): + clean_database() + + +def teardown(): + admin.set_server_property("SERVER_SIDE_SCRIPTING_BIN_DIR", + _ORIGINAL_SERVER_SCRIPTING_BIN_DIR) + clean_database() def setup_module(): + global _ORIGINAL_SERVER_SCRIPTING_BIN_DIR + _ORIGINAL_SERVER_SCRIPTING_BIN_DIR = admin.get_server_property( + "SERVER_SIDE_SCRIPTING_BIN_DIR") + clean_database() + from os import makedirs from os.path import join, isdir, exists from shutil import copyfile, copymode - print("bin: " + str(_SERVER_SIDE_SCRIPTING_BIN_DIR)) + print("bin dir (local): " + str(_SERVER_SIDE_SCRIPTING_BIN_DIR_LOCAL)) + print("bin dir (server): " + str(_SERVER_SIDE_SCRIPTING_BIN_DIR_SERVER)) print("tests scripts: " + str(_TEST_SCRIPTS)) - if not exists(_SERVER_SIDE_SCRIPTING_BIN_DIR): - makedirs(_SERVER_SIDE_SCRIPTING_BIN_DIR) - _REMOVE_FILES_AFTERWARDS.append(_SERVER_SIDE_SCRIPTING_BIN_DIR) - assert isdir(_SERVER_SIDE_SCRIPTING_BIN_DIR) + if not exists(_SERVER_SIDE_SCRIPTING_BIN_DIR_LOCAL): + makedirs(_SERVER_SIDE_SCRIPTING_BIN_DIR_LOCAL) + _REMOVE_FILES_AFTERWARDS.append(_SERVER_SIDE_SCRIPTING_BIN_DIR_LOCAL) + assert isdir(_SERVER_SIDE_SCRIPTING_BIN_DIR_LOCAL) for script_file in _TEST_SCRIPTS: - target = join(_SERVER_SIDE_SCRIPTING_BIN_DIR, script_file) + target = join(_SERVER_SIDE_SCRIPTING_BIN_DIR_LOCAL, script_file) src = join(_TEST_SCRIPTS_DIR, script_file) copyfile(src, target) copymode(src, target) @@ -70,6 +103,7 @@ def teardown_module(): rmtree(obsolete) else: remove(obsolete) + clean_database() def test_call_script_non_existing(): @@ -80,15 +114,8 @@ def test_call_script_non_existing(): def test_call_script_not_executable(): - props = admin.get_server_properties() - print(props) - - import os - dirpath = os.getcwd() - print("PWD={}".format(dirpath)) - print("ls ./\n{}".format(os.listdir("./"))) - print("ls ../\n{}".format(os.listdir("../"))) - + admin.set_server_property("SERVER_SIDE_SCRIPTING_BIN_DIR", + _SERVER_SIDE_SCRIPTING_BIN_DIR_SERVER) form = dict() form["call"] = "not_executable" with raises(ClientErrorException) as exc_info: @@ -97,6 +124,8 @@ def test_call_script_not_executable(): def test_call_ok(): + admin.set_server_property("SERVER_SIDE_SCRIPTING_BIN_DIR", + _SERVER_SIDE_SCRIPTING_BIN_DIR_SERVER) form = dict() form["call"] = "ok" r = get_connection().post_form_data("scripting", form) @@ -108,6 +137,8 @@ def test_call_ok(): def test_call_err(): + admin.set_server_property("SERVER_SIDE_SCRIPTING_BIN_DIR", + _SERVER_SIDE_SCRIPTING_BIN_DIR_SERVER) form = dict() form["call"] = "err" r = get_connection().post_form_data("scripting", form) @@ -118,29 +149,234 @@ def test_call_err(): assert xml.xpath("/Response/script/stderr")[0].text == "err" -@mark.skip(reason="need to setup .pycaosdb.ini in home dirs of sss within docker") -def test_simple_sss(): +def test_run_server_side_script_with_file_as_positional_param(): + RecordType("TestRT").insert() + _REMOVE_FILES_AFTERWARDS.append("test_file.txt") + with open("test_file.txt", "w") as f: + f.write("this is a test") + + response = run_server_side_script("administration/diagnostics.py", + "pos0", + "pos1", + exit="123", + query="COUNT TestRT", + files={"-p2": "test_file.txt"}) + assert response.stderr is None + assert response.code == 123 + assert response.call == ('administration/diagnostics.py ' + '--exit=123 --query=COUNT TestRT ' + 'pos0 pos1 .upload_files/test_file.txt') + + json_data = json.loads(response.stdout) + assert "caosdb" in json_data + assert "query" in json_data["caosdb"] + assert json_data["caosdb"]["query"] == ["COUNT TestRT", "1"] + assert "./.upload_files/test_file.txt" in json_data["files"] + + +def test_run_server_side_script_with_additional_file(): + RecordType("TestRT").insert() + _REMOVE_FILES_AFTERWARDS.append("test_file.txt") + with open("test_file.txt", "w") as f: + f.write("this is a test") + + response = run_server_side_script("administration/diagnostics.py", + "pos0", + "pos1", + exit="123", + query="COUNT TestRT", + files={"dummykey": "test_file.txt"}) + assert response.stderr is None + assert response.code == 123 + assert response.call == ('administration/diagnostics.py ' + '--exit=123 --query=COUNT TestRT ' + 'pos0 pos1') + + json_data = json.loads(response.stdout) + assert json_data["caosdb"]["query"] == ["COUNT TestRT", "1"] + assert "./.upload_files/test_file.txt" in json_data["files"] + + +def test_diagnostics_basic(): + RecordType("TestRT").insert() + form = dict() - form["call"] = "simple_script.py" + form["call"] = "administration/diagnostics.py" form["-Oexit"] = "123" - r = get_connection().post_form_data("scripting", form) - xml = etree.parse(r) + form["-Oquery"] = "COUNT TestRT" + + response = get_connection().post_form_data("scripting", form) + xml = etree.parse(response) print(etree.tostring(xml)) - assert xml.xpath("/Response/script/@code")[0] == "123" + assert response.status == 200 # ok + assert response.getheader("Content-Type") == 'text/xml; charset=UTF-8' + + diagnostics = xml.xpath("/Response/script/stdout")[0].text + assert diagnostics is not None + diagnostics = json.loads(diagnostics) + assert diagnostics["python_version"] is not None + assert diagnostics["import"]["caosdb"][0] is True, ("caosdb not installed" + " in server's python path") + assert diagnostics["auth_token"] is not None + EXC_ERR = ("There shouldn't be any exception during the diagnostics of " + "the interaction of the server-side script and the server.") + assert "exception" not in diagnostics["caosdb"], EXC_ERR + + assert "query" in diagnostics["caosdb"] + assert diagnostics["caosdb"]["query"][0] == "COUNT TestRT" + assert diagnostics["caosdb"]["query"][1] == "1", ("The RecordType should " + "have been found.") + assert xml.xpath("/Response/script/@code")[0] == "123", ("The script " + "should exit " + "with code 123.") + assert xml.xpath("/Response/script/call")[0].text.startswith( + "administration/diagnostics.py") + assert xml.xpath("/Response/script/stderr")[0].text is None + + +def test_diagnostics_with_file_upload(): + RecordType("TestRT").insert() _REMOVE_FILES_AFTERWARDS.append("test_file.txt") with open("test_file.txt", "w") as f: f.write("this is a test") + parts = [] parts.append(MultipartParam.from_file(paramname="txt_file", filename="test_file.txt")) - parts.append(MultipartParam("call", "simple_script.py")) + parts.append(MultipartParam("call", "administration/diagnostics.py")) body, headers = multipart_encode(parts) - r = get_connection().insert(["scripting"], body=body, headers=headers) - xml = etree.parse(r) - print(etree.tostring(xml).decode("utf-8")) + response = get_connection().insert( + ["scripting"], body=body, headers=headers) + assert response.status == 200 # ok + assert response.getheader("Content-Type") == 'text/xml; charset=UTF-8' + + xml = etree.parse(response) + print(etree.tostring(xml)) assert xml.xpath("/Response/script/@code")[0] == "0" - assert xml.xpath( - "/Response/script/call")[0].text.startswith("simple_script.py") - assert "this is a test" in xml.xpath("/Response/script/stdout")[0].text + + diagnostics = xml.xpath("/Response/script/stdout")[0].text + assert diagnostics is not None + diagnostics = json.loads(diagnostics) + assert diagnostics["python_version"] is not None + assert diagnostics["import"]["caosdb"][0] is True, ("caosdb not installed" + " in server's python path") + assert diagnostics["auth_token"] is not None + assert "exception" not in diagnostics["caosdb"], ("There shouldn't be any " + "exception during the " + "diagnostics of the " + "interaction of the " + "server-side " + "script and the server.") + + assert xml.xpath("/Response/script/@code")[0] == "0", ("The script " + "should exit " + "with code 0.") + assert xml.xpath("/Response/script/call")[0].text.startswith( + "administration/diagnostics.py") + assert xml.xpath("/Response/script/stderr")[0].text is None + + +def test_call_as_anonymous_with_administration_role(): + assert Info().user_info.roles == ["administration"] + + # activate anonymous user + admin.set_server_property("AUTH_OPTIONAL", "TRUE") + # give permission to call diagnostics.py + admin._set_permissions( + "anonymous", [ + admin.PermissionRule( + "Grant", "SCRIPTING:EXECUTE:administration:diagnostics.py")]) + + form = dict() + form["call"] = "administration/diagnostics.py" + + headers = {"Content-Type": "application/x-www-form-urlencoded"} + response = request(method="POST", headers=headers, path="scripting", + body=urlencode(form)) + + xml = etree.parse(response) + + assert response.getheader("Set-Cookie") is None # no auth token returned + assert response.getheader("Content-Type") == 'text/xml; charset=UTF-8' + + assert response.status == 200 # ok + assert xml.xpath("/Response/script/@code")[0] == "0" + diagnostics = xml.xpath("/Response/script/stdout")[0].text + assert diagnostics is not None + diagnostics = json.loads(diagnostics) + assert diagnostics["python_version"] is not None + assert diagnostics["import"]["caosdb"][0] is True, ("caosdb not installed" + " in server's python path") + assert diagnostics["auth_token"] is not None + assert "exception" not in diagnostics["caosdb"] + + +def request(method, headers, path, body=None): + """ Connect without auth-token. This is clumsy, bc the pylib is not + intended to be used as anonymous user. + """ + context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) + context.verify_mode = ssl.CERT_REQUIRED + if hasattr(context, "check_hostname"): + context.check_hostname = True + context.load_verify_locations(get_config().get("Connection", "cacert")) + + url = get_config().get("Connection", "url") + fullurl = urlparse(url) + + http_con = HTTPSConnection( + str(fullurl.netloc), timeout=200, context=context) + http_con.request(method=method, headers=headers, url=str(fullurl.path) + + path, body=body) + return http_con.getresponse() + + +def test_anonymous_script_calling_not_permitted(): + form = dict() + form["call"] = "ok" + + # activate anonymous user + admin.set_server_property("AUTH_OPTIONAL", "TRUE") + + # remove all anonymous permissions + admin._set_permissions("anonymous", [ + admin.PermissionRule("Grant", "SCRIPTING:EXECUTE:ok"), + admin.PermissionRule("Deny", "*") + ]) + + headers = {"Content-Type": "application/x-www-form-urlencoded"} + response = request(method="POST", headers=headers, path="scripting", + body=urlencode(form)) + assert response.status == 403 # forbidden + assert response.getheader("Set-Cookie") is None # no auth token returned + + +def test_anonymous_script_calling_success(): + admin.set_server_property("SERVER_SIDE_SCRIPTING_BIN_DIR", + _SERVER_SIDE_SCRIPTING_BIN_DIR_SERVER) + form = dict() + form["call"] = "ok_anonymous" + + # activate anonymous user + admin.set_server_property("AUTH_OPTIONAL", "TRUE") + # give permission to call ok_anonymous + admin._set_permissions("anonymous", + [admin.PermissionRule("Grant", "SCRIPTING:EXECUTE:ok_anonymous")]) + + headers = {"Content-Type": "application/x-www-form-urlencoded"} + response = request(method="POST", headers=headers, path="scripting", + body=urlencode(form)) + assert response.status == 200 # ok + assert response.getheader("Set-Cookie") is None # no auth token returned + assert response.getheader("Content-Type") == 'text/xml; charset=UTF-8' + + body = response.read() + xml = etree.fromstring(body) + + # verify unauthenticated call to script ok_anonymous + assert xml.xpath("/Response/UserInfo/Roles/Role")[0].text == "anonymous" + assert xml.xpath("/Response/script/call")[0].text == "ok_anonymous" + assert xml.xpath("/Response/script/stdout")[0].text == "ok_anonymous" assert xml.xpath("/Response/script/stderr")[0].text is None + assert xml.xpath("/Response/script/@code")[0] == "0"