From b8decb7660c8c1d867826608d62af77b8106fce7 Mon Sep 17 00:00:00 2001 From: Ivan Habunek Date: Thu, 24 Nov 2022 08:41:05 +0100 Subject: [PATCH] Set up integration tests against a local instance --- requirements-dev.txt | 4 +- tests/assets/test1.png | Bin 0 -> 596 bytes tests/assets/test2.png | Bin 0 -> 602 bytes tests/assets/test3.png | Bin 0 -> 608 bytes tests/assets/test4.png | Bin 0 -> 614 bytes tests/test_integration.py | 274 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 276 insertions(+), 2 deletions(-) create mode 100644 tests/assets/test1.png create mode 100644 tests/assets/test2.png create mode 100644 tests/assets/test3.png create mode 100644 tests/assets/test4.png create mode 100644 tests/test_integration.py diff --git a/requirements-dev.txt b/requirements-dev.txt index 8e1d32f..e559c3b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,10 +1,10 @@ keyring +psycopg2 pytest pytest-cov pyxdg +pyyaml sphinx sphinx-autobuild -stdeb twine wheel -pyyaml diff --git a/tests/assets/test1.png b/tests/assets/test1.png new file mode 100644 index 0000000000000000000000000000000000000000..3ecb770deff6975e92a2e079e20ffb7d1e231c37 GIT binary patch literal 596 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1SBWM%0FXZU~J8Fb`J1#c2+1T%1_J8No8Qr zm{>c}*5j~)%+dJZt&2n*twjn?1kBax=(5TZ?Fv|Ug=?+VjF?}FCYc^VF*2;zo0{dj zL)pdCV`XDl9X~K1xp>m^qN~@R_@E5I#|I1T@7=AwvzNIgs&i%9ZH)z4(qAsyPurN^P%&Iv5&dVqtZtweZ8OW z@s~+*^;EP|mot3PbfiCKdTia}n1YB0GZmHR&Qj(R>~0mC7Cl3hcWOnG#iA8bPq`(h zi=K;~b2X^d^xesLi8Gm}ySzsI2$&bzd55CVk`{&1u*&;oRZ}qqRHDqBaYplAuZGV66?mDI)^Bc9-zSO_o z@Y4U%YG2(VmM?kC;@5qB?=kC@H+-0^Cpk&^XC~KAn-6tv_ouM_%5Pj3x_$dGJ3sjb zlhUa>82R`bA2Ow`uiUeH^VaS4=S_b7X6*i5v{A~z)EXEsoCO|{#S9F3${@^GvDCf{ zD9B#o>Fdh=j9Y?DMC6`t@)4krWQl7;iF1B#Zfaf$gL6@8Vo7R>LV0FMhJw4NZ$Nk> zpEyvFt*47)NW|f{=L{Jc3^ OF?hQAxvXWn z(3n^|(bnUzgUr$R;H`^99j!$QPXx@>>FBb`6733Dc!g`N)r^>5iYA#JK`}C{*PEK< zyF=N<(_>|0SRFqwAGvta^P;QQpZK5*!N&&+?eE>KzO$FPC8~2}+HH*mS<@zV9Y5kz z=E}2Yk#u6o0{&njyUT*U68~{|~;;JNxIyjM*YRjBoX~{WWA^DQm2{x@~`d?(RCKAM+cv*S^%h z-tf}@(rRDbB9QZuh6K{>pD$7rK4>F*`r` z29wgMI~e)+8Xq#HuCLs)d-K-q_2*4~{bua`U9?fk!PFWUFPsG)k;M!QddeWoSh3W; z3@FH6;_2(k{)}6KO~i=D;h7~+NV3E=qQp5rH#aq}gu%HeHL)Z$MWH;iBtya7(>EZz zkxv|`$l24yF(l*O+jES(3<^BV8yZ`Ff3M2XSr~d@^35+hS1wsf0DMh;&8Wl4*!j3v RmknqfgQu&X%Q~loCICw``#t~w literal 0 HcmV?d00001 diff --git a/tests/assets/test3.png b/tests/assets/test3.png new file mode 100644 index 0000000000000000000000000000000000000000..f0ffc7851101c2c2b6ab2e10b1f1725545e214ce GIT binary patch literal 608 zcmeAS@N?(olHy`uVBq!ia0vp^MnLSw!2~23-vt-~$<|C~=KxPxlIal?Bg1;Vsad`| zlwCYMRyKy!@dNXbizhuVx_bSI56TdHe6Y~|-redudzo9JI#;IM)>x1=ZDQB)BR*xW zJbM;NCzdSW4;C{1dNaj{@%+b9e);(Ki)>E+zB7NZ`^lL+A3Bd1`m+qWOH^OJ8d zDV@55k&mzOAyex5%00U`Z{1#h-sIPB#_r!m8>Jjft%32vS>O>_%)p?h48n{ROYO^m zg6t)pzOL-gxFy&`te9r!aRY@UOI#yLobz*YQ}ap~oQqNuOHxx5$}>wc6x=<11Hv2m z#DR)DJzX3_GVZ-S$H>c|z{9+uvE}#osvMmqp%*6K{IYZ9lBGm}ui>v5t2-FyT8jRv Q2O7%Y>FVdQ&MBb@0QhD9Pyhe` literal 0 HcmV?d00001 diff --git a/tests/assets/test4.png b/tests/assets/test4.png new file mode 100644 index 0000000000000000000000000000000000000000..af99441abe51824494ddf4b5f3d104b2b28b526b GIT binary patch literal 614 zcmeAS@N?(olHy`uVBq!ia0vp^MnD|E!2~3)ByQZ!z`)p=>FgZf>FlgfP?VpRnUl)E zpfRy_qOHea2brVs!CM!JI$Dbqo(Pz$)6r#>CE69R@Cw&js~IuB6iqTcf?{M?uQxTz zcZafzr^m|1usVKVK63G-=S5eqKk-2sf{zat+TXideP=IoOH}8|wA&gBvZhV!I)22b z%#~-)BI(4E1^mH6#$Ruy7%`szSjsOS|9+9p>ECzeFLpmUljlR{5n~^7okyjQPWpO3 z-{UWn_5p{ z?K+!;<+H#xu9F{|{~vsxclOVZ8M8%t7~kq|`)kO;Qr1{?b=&^_+}(9dKjt@TuYIY1 zz2T+*rPaQ=MJ!+Pn8mOA`rc#KD{uHPSx<73^3P1JpEe)r-0n|d{gvOiE_D0$V|IS> z4JM^icQEqtH9llYU0=Cp_vWqJ>(86~`pww=yJ(}7gQ+zzUN{RpB8wRq^pruEv0|xx z8BmbD#M9T6{Ta6en<&TSaK2MOA;}Wgh!W@g+}zZ>5(ej@)Wnk16ovB4k_-iRPv3y> zMm}+%B7aX8$B>MBZ_hFEGAQscZ)j}!{kuYeq*t#*H5m S;|~IjX7F_Nb6Mw<&;$T=xdoE| literal 0 HcmV?d00001 diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 0000000..8c5c5cc --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,274 @@ +""" +This module contains integration tests meant to run against a test Mastodon instance. + +You can set up a test instance locally by following this guide: +https://docs.joinmastodon.org/dev/setup/ + +To enable integration tests, export the following environment variables to match +your test server and database: + +``` +export TOOT_TEST_HOSTNAME="localhost:3000" +export TOOT_TEST_DATABASE_DSN="mastodon_development" +``` +""" + +import os +import psycopg2 +import pytest +import re +import uuid + +from os import path +from toot import CLIENT_NAME, CLIENT_WEBSITE, api, App, User +from toot.console import run_command +from toot.exceptions import NotFoundError +from toot.utils import get_text + +# Host name of a test instance to run integration tests against +# DO NOT USE PUBLIC INSTANCES!!! +HOSTNAME = os.getenv("TOOT_TEST_HOSTNAME") + +# Mastodon database name, used to confirm user registration without having to click the link +DATABASE_DSN = os.getenv("TOOT_TEST_DATABASE_DSN") + + +if not HOSTNAME or not DATABASE_DSN: + pytest.skip("Skipping integration tests", allow_module_level=True) + +# ------------------------------------------------------------------------------ +# Fixtures +# ------------------------------------------------------------------------------ + + +def create_app(): + response = api.create_app(HOSTNAME, scheme="http") + return App(HOSTNAME, f"http://{HOSTNAME}", response["client_id"], response["client_secret"]) + + +def register_account(app: App): + username = str(uuid.uuid4())[-10:] + email = f"{username}@example.com" + + response = api.register_account(app, username, email, "password", "en") + confirm_user(email) + return User(app.instance, username, response["access_token"]) + + +def confirm_user(email): + conn = psycopg2.connect(DATABASE_DSN) + cursor = conn.cursor() + cursor.execute("UPDATE users SET confirmed_at = now() WHERE email = %s;", (email,)) + conn.commit() + + +@pytest.fixture(scope="session") +def app(): + return create_app() + + +@pytest.fixture(scope="session") +def user(app): + return register_account(app) + + +# ------------------------------------------------------------------------------ +# Tests +# ------------------------------------------------------------------------------ + +def test_get_instance(app): + response = api.get_instance(HOSTNAME, scheme="http") + assert response["title"] == "Mastodon" + assert response["uri"] == app.instance + + +def test_post(app, user, capsys): + text = "i wish i was a #lumberjack" + run_command(app, user, "post", [text]) + status_id = _posted_status_id(capsys) + + status = api.fetch_status(app, user, status_id) + assert text == get_text(status["content"]) + assert status["account"]["acct"] == user.username + assert status["application"]["name"] == CLIENT_NAME + assert status["application"]["website"] == CLIENT_WEBSITE + assert status["visibility"] == "public" + assert status["sensitive"] is False + assert status["spoiler_text"] == "" + + +def test_post_visibility(app, user, capsys): + for visibility in ["public", "unlisted", "private", "direct"]: + run_command(app, user, "post", ["foo", "--visibility", visibility]) + status_id = _posted_status_id(capsys) + status = api.fetch_status(app, user, status_id) + assert status["visibility"] == visibility + + +def test_media_attachments(app, user, capsys): + assets_dir = path.realpath(path.join(path.dirname(__file__), "assets")) + + path1 = path.join(assets_dir, "test1.png") + path2 = path.join(assets_dir, "test2.png") + path3 = path.join(assets_dir, "test3.png") + path4 = path.join(assets_dir, "test4.png") + + run_command(app, user, "post", [ + "--media", path1, + "--media", path2, + "--media", path3, + "--media", path4, + "--description", "Test 1", + "--description", "Test 2", + "--description", "Test 3", + "--description", "Test 4", + "some text" + ]) + + status_id = _posted_status_id(capsys) + status = api.fetch_status(app, user, status_id) + + [a1, a2, a3, a4] = status["media_attachments"] + + assert a1["meta"]["original"]["size"] == "50x50" + assert a2["meta"]["original"]["size"] == "50x60" + assert a3["meta"]["original"]["size"] == "50x70" + assert a4["meta"]["original"]["size"] == "50x80" + + assert a1["description"] == "Test 1" + assert a2["description"] == "Test 2" + assert a3["description"] == "Test 3" + assert a4["description"] == "Test 4" + + +def test_delete_status(app, user): + status = api.post_status(app, user, "foo") + + response = api.delete_status(app, user, status["id"]).json() + assert response["id"] == status["id"] + + with pytest.raises(NotFoundError): + api.fetch_status(app, user, response["id"]) + + +def test_favourite(app, user, capsys): + status = api.post_status(app, user, "foo") + assert not status["favourited"] + + run_command(app, user, "favourite", [status["id"]]) + + out, err = capsys.readouterr() + assert strip_ansi(out) == "✓ Status favourited" + assert err == "" + + status = api.fetch_status(app, user, status["id"]) + assert status["favourited"] + + run_command(app, user, "unfavourite", [status["id"]]) + + out, err = capsys.readouterr() + assert strip_ansi(out) == "✓ Status unfavourited" + assert err == "" + + status = api.fetch_status(app, user, status["id"]) + assert not status["favourited"] + + +def test_reblog(app, user, capsys): + status = api.post_status(app, user, "foo") + assert not status["reblogged"] + + run_command(app, user, "reblog", [status["id"]]) + + out, err = capsys.readouterr() + assert strip_ansi(out) == "✓ Status reblogged" + assert err == "" + + status = api.fetch_status(app, user, status["id"]) + assert status["reblogged"] + + run_command(app, user, "reblogged_by", [status["id"]]) + + out, err = capsys.readouterr() + assert strip_ansi(out) == f"@{user.username}" + + run_command(app, user, "unreblog", [status["id"]]) + + out, err = capsys.readouterr() + assert strip_ansi(out) == "✓ Status unreblogged" + assert err == "" + + status = api.fetch_status(app, user, status["id"]) + assert not status["reblogged"] + + +def test_pin(app, user, capsys): + status = api.post_status(app, user, "foo") + assert not status["pinned"] + + run_command(app, user, "pin", [status["id"]]) + + out, err = capsys.readouterr() + assert strip_ansi(out) == "✓ Status pinned" + assert err == "" + + status = api.fetch_status(app, user, status["id"]) + assert status["pinned"] + + run_command(app, user, "unpin", [status["id"]]) + + out, err = capsys.readouterr() + assert strip_ansi(out) == "✓ Status unpinned" + assert err == "" + + status = api.fetch_status(app, user, status["id"]) + assert not status["pinned"] + + +def test_bookmark(app, user, capsys): + status = api.post_status(app, user, "foo") + assert not status["bookmarked"] + + run_command(app, user, "bookmark", [status["id"]]) + + out, err = capsys.readouterr() + assert strip_ansi(out) == "✓ Status bookmarked" + assert err == "" + + status = api.fetch_status(app, user, status["id"]) + assert status["bookmarked"] + + run_command(app, user, "unbookmark", [status["id"]]) + + out, err = capsys.readouterr() + assert strip_ansi(out) == "✓ Status unbookmarked" + assert err == "" + + status = api.fetch_status(app, user, status["id"]) + assert not status["bookmarked"] + + +# ------------------------------------------------------------------------------ +# Utils +# ------------------------------------------------------------------------------ + +strip_ansi_pattern = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") + + +def strip_ansi(string): + return strip_ansi_pattern.sub("", string).strip() + + +def _posted_status_id(capsys): + out, err = capsys.readouterr() + out = strip_ansi(out) + assert err == "" + + pattern = re.compile(r"Toot posted: http://([^/]+)/@([^/]+)/(.+)") + match = re.search(pattern, out) + assert match + + host, _, status_id = match.groups() + assert host == HOSTNAME + + return status_id