From 4d71a44fcd60ce65fac626e833cf0278f1a067cc Mon Sep 17 00:00:00 2001 From: Scott Lyons Date: Wed, 21 Aug 2024 08:38:23 -0700 Subject: [PATCH] test(python): adding python integration test --- tests/python-sync/config.yaml | 179 +++++++++++++ tests/python-sync/docker-compose.yml | 51 ++++ tests/python-sync/requirements.txt | 6 + tests/python-sync/test.py | 364 +++++++++++++++++++++++++++ tests/python-sync/test.sh | 9 + 5 files changed, 609 insertions(+) create mode 100644 tests/python-sync/config.yaml create mode 100644 tests/python-sync/docker-compose.yml create mode 100644 tests/python-sync/requirements.txt create mode 100644 tests/python-sync/test.py create mode 100755 tests/python-sync/test.sh diff --git a/tests/python-sync/config.yaml b/tests/python-sync/config.yaml new file mode 100644 index 0000000..26ef126 --- /dev/null +++ b/tests/python-sync/config.yaml @@ -0,0 +1,179 @@ +# rather than editing this file, it is strongly suggested to create config.yaml +# and override only what you need. + +# shown in the website title and on the front page +name: integrationland +# full url to the homepage of this szurubooru site, with no trailing slash +domain: https://localhost:9802 +# used to salt the users' password hashes and generate filenames for static content +secret: awdopk1mdqsapdoawd21!!!#asw + +# Delete thumbnails and source files on post delete +# Original functionality is no, to mitigate the impacts of admins going +# on unchecked post purges. +delete_source_files: no + +thumbnails: + avatar_width: 300 + avatar_height: 300 + post_width: 300 + post_height: 300 + +# settings used to download files from the web on behalf of the api users +user_agent: +max_dl_filesize: 25.0E+6 # maximum filesize limit in bytes + +# automatically convert animated GIF uploads to video formats +convert: + gif: + to_webm: false + to_mp4: false + +# allow posts to be uploaded even if some image processing errors occur +allow_broken_uploads: false + +# used to send password reset e-mails +smtp: + host: # example: localhost + port: # example: 25 + user: # example: bot + pass: # example: groovy123 + from: # example: noreply@example.com + # if host is left empty the password reset feature will be disabled, + # in which case it is recommended to fill contactEmail so that users + # know who to contact when they want to reset their password + +contact_email: # example: bob@example.com. Meant for manual password reset procedures + +enable_safety: yes + +tag_name_regex: ^\S+$ +tag_category_name_regex: ^[^\s%+#/]+$ + +pool_name_regex: ^\S+$ +pool_category_name_regex: ^[^\s%+#/]+$ + +# don't make these more restrictive unless you want to annoy people; if you do +# customize them, make sure to update the instructions in the registration form +# template as well. +password_regex: '^.{5,}$' +user_name_regex: '^[a-zA-Z0-9_-]{1,32}$' + +# webhooks to call when events occur (such as post/tag/user/etc. changes) +# the listed urls will be called with a HTTP POST request with a payload +# containing a snapshot resource as JSON. See doc/API.md for details +webhooks: +# - https://api.example.com/webhooks/ + +default_rank: regular + +privileges: + 'users:create:self': anonymous # Registration permission + 'users:create:any': administrator + 'users:list': regular + 'users:view': regular + 'users:edit:any:name': moderator + 'users:edit:any:pass': moderator + 'users:edit:any:email': moderator + 'users:edit:any:avatar': moderator + 'users:edit:any:rank': moderator + 'users:edit:self:name': regular + 'users:edit:self:pass': regular + 'users:edit:self:email': regular + 'users:edit:self:avatar': regular + 'users:edit:self:rank': moderator # one can't promote themselves or anyone to upper rank than their own. + 'users:delete:any': administrator + 'users:delete:self': regular + + 'user_tokens:list:any': administrator + 'user_tokens:list:self': regular + 'user_tokens:create:any': administrator + 'user_tokens:create:self': regular + 'user_tokens:edit:any': administrator + 'user_tokens:edit:self': regular + 'user_tokens:delete:any': administrator + 'user_tokens:delete:self': regular + + 'posts:create:anonymous': regular + 'posts:create:identified': regular + 'posts:list': anonymous + 'posts:reverse_search': regular + 'posts:view': anonymous + 'posts:view:featured': anonymous + 'posts:edit:content': power + 'posts:edit:flags': regular + 'posts:edit:notes': regular + 'posts:edit:relations': regular + 'posts:edit:safety': power + 'posts:edit:source': regular + 'posts:edit:tags': regular + 'posts:edit:thumbnail': power + 'posts:feature': moderator + 'posts:delete': moderator + 'posts:score': regular + 'posts:merge': moderator + 'posts:favorite': regular + 'posts:bulk-edit:tags': power + 'posts:bulk-edit:safety': power + 'posts:bulk-edit:delete': power + + 'tags:create': regular + 'tags:edit:names': power + 'tags:edit:category': power + 'tags:edit:description': power + 'tags:edit:implications': power + 'tags:edit:suggestions': power + 'tags:list': regular + 'tags:view': anonymous + 'tags:merge': moderator + 'tags:delete': moderator + + 'tag_categories:create': moderator + 'tag_categories:edit:name': moderator + 'tag_categories:edit:color': moderator + 'tag_categories:edit:order': moderator + 'tag_categories:list': anonymous + 'tag_categories:view': anonymous + 'tag_categories:delete': moderator + 'tag_categories:set_default': moderator + + 'pools:create': regular + 'pools:edit:names': power + 'pools:edit:category': power + 'pools:edit:description': power + 'pools:edit:posts': power + 'pools:list': regular + 'pools:view': anonymous + 'pools:merge': moderator + 'pools:delete': moderator + + 'pool_categories:create': moderator + 'pool_categories:edit:name': moderator + 'pool_categories:edit:color': moderator + 'pool_categories:list': anonymous + 'pool_categories:view': anonymous + 'pool_categories:delete': moderator + 'pool_categories:set_default': moderator + + 'comments:create': regular + 'comments:delete:any': moderator + 'comments:delete:own': regular + 'comments:edit:any': moderator + 'comments:edit:own': regular + 'comments:list': regular + 'comments:view': regular + 'comments:score': regular + + 'snapshots:list': power + + 'uploads:create': regular + 'uploads:use_downloader': power + +## ONLY SET THESE IF DEPLOYING OUTSIDE OF DOCKER +#debug: 0 # generate server logs? +#show_sql: 0 # show sql in server logs? +#data_url: /data/ +#data_dir: /var/www/data +## usage: schema://user:password@host:port/database_name +## example: postgres://szuru:dog@localhost:5432/szuru_test +#database: diff --git a/tests/python-sync/docker-compose.yml b/tests/python-sync/docker-compose.yml new file mode 100644 index 0000000..2dc35e7 --- /dev/null +++ b/tests/python-sync/docker-compose.yml @@ -0,0 +1,51 @@ +## Example Docker Compose configuration +## +## Use this as a template to set up docker-compose, or as guide to set up other +## orchestration services +version: '2' + +services: + + server: + image: szurubooru/server:latest + depends_on: + - sql + environment: + ## These should be the names of the dependent containers listed below, + ## or FQDNs/IP addresses if these services are running outside of Docker + POSTGRES_HOST: sql + ## Credentials for database: + POSTGRES_USER: pguser + POSTGRES_PASSWORD: pgpassword + ## Commented Values are Default: + #POSTGRES_DB: defaults to same as POSTGRES_USER + #POSTGRES_PORT: 5432 + #LOG_SQL: 0 (1 for verbose SQL logs) + THREADS: + volumes: + - "sz-data:/data" + - "./config.yaml:/opt/app/config.yaml" + + client: + image: szurubooru/client:latest + depends_on: + - server + environment: + BACKEND_HOST: server + BASE_URL: http://localhost:9802 + volumes: + - "sz-data:/data:ro" + ports: + - "9802:80" + + sql: + image: postgres:11-alpine + restart: unless-stopped + environment: + POSTGRES_USER: pguser + POSTGRES_PASSWORD: pgpassword + #volumes: + # - "${MOUNT_SQL}/sqldata:/var/lib/postgresql/data" + +volumes: + sz-data: diff --git a/tests/python-sync/requirements.txt b/tests/python-sync/requirements.txt new file mode 100644 index 0000000..aa5030d --- /dev/null +++ b/tests/python-sync/requirements.txt @@ -0,0 +1,6 @@ +Jinja2==3.1.4 +MarkupSafe==2.1.5 +maturin==1.7.0 +pdoc==14.6.0 +Pygments==2.18.0 +loguru==0.7.2 diff --git a/tests/python-sync/test.py b/tests/python-sync/test.py new file mode 100644 index 0000000..1511129 --- /dev/null +++ b/tests/python-sync/test.py @@ -0,0 +1,364 @@ +from szurubooru_client import * +import time, sys +from loguru import logger +import hashlib +import tempfile, pathlib + +def connect(): + logger.info("Connecting to Szurubooru instance") + anon_client = SzurubooruSyncClient("http://localhost:9802") + error = None + for i in range(5): + try: + anon_client.global_info() + except Exception as e: + error=e + time.sleep(5) + else: + logger.info("Connection successful!") + break + else: + logger.error("Could not connect to Szurubooru instance, error is {}", error) + sys.exit(1) + return anon_client + +def create_auth_client(client): + client.create_user("integration_user", "integration_password", rank=UserRank.Administrator) + auth_client = SzurubooruSyncClient("http://localhost:9802", username="integration_user", + password="integration_password", allow_insecure=True) + return auth_client + +def test_tag_categories(client): + logger.info("Testing tag categories") + logger.info("Listing tag categories") + tag_cats = client.list_tag_categories() + assert len(tag_cats) == 1 + + logger.info("Creating tag category") + result_tag_cat = client.create_tag_category("my_tag_cat", color="purple", order=1) + assert result_tag_cat.name == "my_tag_cat" + + tag_cats = client.list_tag_categories() + assert len(tag_cats) != 1 + assert tag_cats[1].name == "my_tag_cat" + + logger.info("Getting tag category") + get_tag_cat = client.get_tag_category("my_tag_cat") + assert get_tag_cat.color == result_tag_cat.color + + logger.info("Updating tag category") + update_tag_cat = client.update_tag_category("my_tag_cat", get_tag_cat.version, color="red") + assert update_tag_cat.color != get_tag_cat.color + + logger.info("Deleting tag category") + client.delete_tag_category("my_tag_cat", update_tag_cat.version) + tag_cats = client.list_tag_categories() + assert len(tag_cats) == 1 + +def test_tag(client): + logger.info("Testing tags") + logger.info("Listing tags") + + tags = client.list_tags() + assert len(tags.results) == 0 + + logger.info("Creating tag") + foo_tag = client.create_tag("foo", category="default", description="The foo tag") + assert foo_tag.names == ["foo"] + tags = client.list_tags() + assert len(tags.results) == 1 + + logger.info("Testing field selection") + tags = client.list_tags(fields=["version", "names", "category"]) + assert len(tags.results) == 1 + assert tags.results[0].description is None + + logger.info("Updating tag") + foo_tag = client.update_tag(foo_tag.names[0], version=foo_tag.version, description="The foo2 tag") + assert foo_tag.description == "The foo2 tag" + + logger.info("Getting tag") + foo_tag2 = client.get_tag("foo") + assert foo_tag.description == foo_tag2.description + + logger.info("Creating a second tag") + bar_tag = client.create_tag("bar", category="default", description="The bar tag") + assert bar_tag.names == ["bar"] + + logger.info("Merging tags") + foo_tag2 = client.merge_tags(bar_tag.names[0], bar_tag.version, + foo_tag2.names[0], foo_tag2.version) + tags = client.list_tags() + assert len(tags.results) == 1 + + logger.info("Deleting tag") + client.delete_tag(foo_tag2.names[0], foo_tag2.version) + +def test_creating_posts(client): + logger.info("Testing posts") + + logger.info("Listing posts") + posts = client.list_posts() + assert len(posts.results) == 0 + + logger.info("Creating post from URL") + wiki_post = client.create_post(url="https://upload.wikimedia.org/wikipedia/commons/thumb/5/5a/Maine_Coon_cat_by_Tomitheos.JPG/225px-Maine_Coon_cat_by_Tomitheos.JPG", + safety=PostSafety.Safe) + posts = client.list_posts() + assert len(posts.results) == 1 + + logger.info("Updating post") + wiki_post = client.update_post(wiki_post.id, wiki_post.version, source="Wikipedia") + assert wiki_post.source == "Wikipedia" + + logger.info("Deleting post") + client.delete_post(wiki_post.id, wiki_post.version) + posts = client.list_posts() + assert len(posts.results) == 0 + + logger.info("Testing upload by file path") + folly1 = client.create_post(file_path="../folly1.jpg", + tags=["maine_coon", "cat", "folly1"], + safety=PostSafety.Safe) + posts = client.list_posts() + assert len(posts.results) == 1 + + folly2 = client.create_post(file_path="../folly2.jpg", + tags=["maine_coon", "cat", "folly2"], + safety=PostSafety.Safe) + + logger.info("Testing upload with thumbnail") + folly3 = client.create_post(file_path="../folly3.jpg", + thumbnail_path="../folly3_thumb.jpg", + tags=["maine_coon", "cat", "folly3"], + safety=PostSafety.Safe) + + logger.info("Searching for a post with image") + folly3_search = client.post_for_image("../folly3.jpg") + assert folly3_search is not None + assert folly3_search.id == folly3.id + + logger.info("Reverse image searching") + reverse_search = client.reverse_image_search("../folly3.jpg") + assert reverse_search.exact_post.id == folly3.id + + logger.info("Testing temporary upload") + token = client.upload_temporary_file("../folly4.jpg") + folly4 = client.create_post(token=token, tags=["maine_coon", "cat", "folly4"], + safety=PostSafety.Safe) + + logger.info("Querying by anonymous tag") + cat_posts = client.list_posts(query=[anonymous_token("cat")]) + assert len(cat_posts.results) == 4 + + logger.info("Querying by named tag") + cat_posts = client.list_posts(query=[named_token(PostNamedToken.Tag, "maine_coon")]) + assert len(cat_posts.results) == 4 + + logger.info("Testing pagination") + cat_posts = client.list_posts(limit=1) + assert cat_posts.total == 4 + assert len(cat_posts.results) == 1 + + cat_posts2 = client.list_posts(limit=1, offset=1) + assert cat_posts.results != cat_posts2.results + + logger.info("Testing tag siblings") + tag_occurrences = client.get_tag_siblings("maine_coon") + cat_occurrences = list(filter(lambda x: "cat" in x.tag.names, tag_occurrences)) + assert len(cat_occurrences) == 1 + + logger.info("Rating post") + client.rate_post(folly3.id, 1) + + logger.info("Testing rating error validation") + try: + client.rate_post(folly3.id, -2) + except ValueError: + assert True + else: + assert False + + logger.info("Favoriting post") + client.favorite_post(folly3.id) + + logger.info("Unfavoriting post") + client.unfavorite_post(folly3.id) + + logger.info("Featuring post") + client.set_featured_post(folly3.id) + + logger.info("Getting featured post") + featured_post = client.get_featured_post() + assert folly3.id == featured_post.id + + logger.info("Merging posts") + merged_post = client.merge_post(folly4.id, folly4.version, folly3.id, folly3.version) + assert merged_post.id == folly3.id + +def test_pool_categories(client): + logger.info("Testing pool categories") + + logger.info("Listing pool categories") + pool_cats = client.list_pool_categories() + assert len(pool_cats) != 0 + + logger.info("Creating pool category") + pool_cat = client.create_pool_category("cat_pool_category", color="purple") + assert pool_cat.color == "purple" + pool_dog = client.create_pool_category("dog_category", color="orange") + + logger.info("Updating pool category") + pool_cat = client.update_pool_category(pool_cat.name, pool_cat.version, color="white") + assert pool_cat.color == "white" + + logger.info("Getting pool category") + pool_dog = client.get_pool_category(pool_dog.name) + assert pool_dog.color == "orange" + + logger.info("Deleting pool category") + pool_dog = client.delete_pool_category(pool_dog.name, pool_dog.version) + + logger.info("Setting default pool category") + client.set_default_pool_category(pool_cat.name) + +def test_pools(client): + logger.info("Testing post pools") + logger.info("Listing post pools") + pools = client.list_pools() + assert len(pools.results) == 0 + + logger.info("Creating pools") + cat_pool = client.create_pool("cats_pool", category="cat_pool_category") + catz_pool = client.create_pool("catz_pool", category="cat_pool_category") + dogs_pool = client.create_pool("dogs_pool", category="cat_pool_category") + + logger.info("Getting pool") + cat_pool = client.get_pool(cat_pool.id) + + logger.info("Deleting pool") + client.delete_pool(dogs_pool.id, dogs_pool.version) + + logger.info("Updating pool") + f4_results = client.list_posts([anonymous_token("cat")]) + post_ids= list(map(lambda p: p.id, f4_results.results)) + cat_pool = client.update_pool(cat_pool.id, cat_pool.version, + posts=post_ids, description="All cat pictures") + assert len(cat_pool.posts) != 0 + + logger.info("Merging pools") + merged_pool = client.merge_pools(catz_pool.id, catz_pool.version, cat_pool.id, cat_pool.version) + assert merged_pool.id == cat_pool.id + +def test_comments(client): + logger.info("Testing post comments") + + logger.info("Listing post comments") + comment_list = client.list_comments() + assert len(comment_list.results) == 0 + + cat_results = client.list_posts([anonymous_token("cat")]) + post_id = cat_results.results[0].id + + logger.info("Creating comment") + comment = client.create_comment("Excellent cat!", post_id) + + logger.info("Updating comment") + comment = client.update_comment(comment.id, comment.version, text="Beautiful cat!") + assert comment.text == "Beautiful cat!" + + logger.info("Getting comment") + comment = client.get_comment(comment.id) + assert comment.text == "Beautiful cat!" + + logger.info("Getting all comments for post") + comment_list = client.list_comments([named_token(CommentNamedToken.Post, post_id)]) + assert len(comment_list.results) != 0 + + logger.info("Testing rating comments") + comment = client.rate_comment(comment.id, -1) + assert comment.own_score == -1 + + try: + client.rate_comment(comment.id, -2) + except SzuruPyClientError: + assert True + else: + assert False + + logger.info("Deleting comment") + client.delete_comment(comment.id, comment.version) + +def test_users(client): + logger.info("Testing users") + + logger.info("Listing users") + user_list = client.list_users() + + logger.info("Creating user with avatar") + user = client.create_user("iu2", "ipass2", rank=UserRank.Regular, avatar_path="../avatar.jpg") + assert user.avatar_style == UserAvatarStyle.Manual + + logger.info("Updating user") + user = client.update_user(user.name, user.version, rank=UserRank.Restricted) + + logger.info("Getting user") + user = client.get_user(user.name) + + logger.info("Deleting user") + client.delete_user(user.name, user.version) + + logger.info("Listing user tokens") + tokens = client.list_user_tokens("integration_user") + assert len(tokens) == 0 + + logger.info("Creating user token") + token = client.create_user_token("integration_user", "My token") + + logger.info("Updating user token") + token2 = client.update_user_token("integration_user", token.token, token.version, enabled=False) + assert token2.enabled == False + + logger.info("Deleting user token") + client.delete_user_token("integration_user", token2.token, token2.version) + +def test_snapshots(client): + logger.info("Testing snapshots") + + logger.info("Listing snapshots") + snapshot_list = client.list_snapshots() + assert len(snapshot_list.results) != 0 + +def test_downloads(client): + logger.info("Testing downloads") + f3_hasher = hashlib.new("sha1") + + with open("../folly3.jpg", "rb") as f: + while (byte := f.read(1)): + f3_hasher.update(byte) + + f3_post = client.list_posts([anonymous_token("folly3")]).results[0] + with tempfile.TemporaryDirectory() as tmpdirname: + tmpdir = pathlib.Path(tmpdirname) + fname = tmpdir / "folly3_dl.jpg" + #fname = "../folly4_dl.jpg" + client.download_image_to_path(f3_post.id, fname) + dl_hasher = hashlib.new("sha1") + with open(fname, "rb") as f: + while (byte := f.read(1)): + dl_hasher.update(byte) + + assert f3_hasher.hexdigest() == dl_hasher.hexdigest() + +if __name__ == "__main__": + client = connect() + client = create_auth_client(client) + test_tag_categories(client) + test_tag(client) + test_creating_posts(client) + test_pool_categories(client) + test_pools(client) + test_comments(client) + test_users(client) + test_snapshots(client) + test_downloads(client) diff --git a/tests/python-sync/test.sh b/tests/python-sync/test.sh new file mode 100755 index 0000000..fe8aca1 --- /dev/null +++ b/tests/python-sync/test.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -e + +pip uninstall -y szurubooru_client || true +pip install -r requirements.txt +maturin develop -F python -m ../../szurubooru-client/Cargo.toml +docker compose down +docker compose up -d +python test.py && docker compose down