From 469b002fb4fea451f0d6f55d42160611cc20268e Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Mon, 2 Oct 2023 17:44:39 +0530 Subject: [PATCH] feat: define BDD tests per RFC6415 --- features/environment.py | 44 +++++++ features/host_meta.feature | 15 +++ features/steps/host_meta.py | 252 ++++++++++++++++++++++++++++++++++++ 3 files changed, 311 insertions(+) create mode 100644 features/environment.py create mode 100644 features/host_meta.feature create mode 100644 features/steps/host_meta.py diff --git a/features/environment.py b/features/environment.py new file mode 100644 index 0000000..b241afc --- /dev/null +++ b/features/environment.py @@ -0,0 +1,44 @@ +# SPDX-FileCopyrightText: 2023 Aravinth Manivannan +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +from behave import * + +from ftest_common.logger import upload_logs_to_ftest + + +def before_all(context): + context.success = [] + context.failure = {} + + +def after_all(context): + max_score = 9 + score = 0 + + score = len(context.success) + print("\n\n===============") + if score == max_score: + print("All tests passed") + elif score > 0: + print(f"Partial success. {score} out of {max_score} tests passed") + + print("Summary:\n") + + logs = "" + + if context.success: + print(f"Successful tests:\n") + for s in context.success: + log = f"[OK] {s}\n" + print(log) + logs += log + + if context.failure: + print(f"\n\nFailed tests:\n") + for _, (test, error) in enumerate(context.failure.items()): + log = f"[FAIL] {test} failed with error:\n{error}\n-----\n" + print(log) + logs += log + + upload_logs_to_ftest(score == max_score, logs) diff --git a/features/host_meta.feature b/features/host_meta.feature new file mode 100644 index 0000000..591bfc0 --- /dev/null +++ b/features/host_meta.feature @@ -0,0 +1,15 @@ +Feature: Host Meta + As defined in https://www.rfc-editor.org/rfc/rfc6415.html + + Scenario: + Given A Fediverse server + When Querying /.well-known/host-meta + Then The page must resolve at port 80 or 443 + And If both port 80 _and_ 443 work, then they SHOULD return same document + And The document SHOULD be served with "application/xrd+xml" media type + And The host-meta document root MUST be an "XRD" element + And The document SHOULD NOT include a "Subject" element + And The document SHOULD include "Link" element + And The document's "Link" element should include either "template" or "href" attributes + And The document should have at least on 'lrdd' document + And Fediverse specific: The lrdd document must include template containing WebFinger well-known URI diff --git a/features/steps/host_meta.py b/features/steps/host_meta.py new file mode 100644 index 0000000..23d9d4d --- /dev/null +++ b/features/steps/host_meta.py @@ -0,0 +1,252 @@ +# SPDX-FileCopyrightText: 2023 Aravinth Manivannan +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +import os +import collections.abc +from urllib.parse import urlparse, urlunparse + +import requests +import xmltodict +from behave import * + +from ftest_common.env import FTEST_USER +from ftest_common.logger import logger +from ftest_common.webfinger import get_webfinger +from ftest_common.obj import get_ap_obj + + +def query_host_meta(https: bool): + from ftest_common.env import TARGET_HOST + + if https: + scheme = "https" + else: + scheme = "http" + parsed_target_host = urlparse(TARGET_HOST) + host_meta = urlunparse( + ( + scheme, + parsed_target_host.netloc, + "/.well-known/host-meta", + "", + "", + "", + ) + ) + logger.debug(f"fetching {host_meta}") + resp = requests.get(host_meta) + logger.debug( + f"host-meta response:\n\nSTATUS: {resp.status_code}\n\nHEADERS:\n {resp.headers}\n\nRESPONSE PAYLOAD:\n{resp.content}" + ) + return resp + + +@given("A Fediverse server") +def step_impl(context): + pass + + +@when("Querying /.well-known/host-meta") +def step_impl(context): + try: + http_res = query_host_meta(False) + context.http_res = http_res + if http_res.status_code == 200: + context.http_host_meta = xmltodict.parse(http_res.content) + logger.info("[SUCCESS] host-meta available on 80") + context.host_meta = context.http_host_meta + context.resp = context.http_res + except: + logger.debug("[ERROR] host-meta not available on 80") + pass + + try: + https_res = query_host_meta(True) + context.https_res = https_res + if https_res.status_code == 200: + context.https_host_meta = xmltodict.parse(https_res.content) + logger.info("[SUCCESS] host-meta available on 443") + context.host_meta = context.https_host_meta + context.resp = context.https_res + except: + logger.debug("[ERROR] host-meta not available on 443") + pass + + +@then("The page must resolve at port 80 or 443") +def step_impl(context): + name = "Page must resolve at port 80 or 443" + + try: + assert ( + any( + [ + "https_host_meta" in context, + "http_host_meta" in context, + ] + ) + is True + ), name + logger.info("[SUCCESS] {name}") + except Exception as e: + logger.error(e) + context.failure[name] = e + raise e + context.success.append(name) + + +@then("If both port 80 _and_ 443 work, then they SHOULD return same document") +def step_impl(context): + if all(["https_host_meta" in context, "http_host_meta" in context]): + assert ( + context.host_meta == context.https_host_meta + ), "Both 80 and 443 returned the same response" + + +@then('The document SHOULD be served with "application/xrd+xml" media type') +def step_impl(context): + name = 'The document SHOULD be served with "application/xrd+xml" media type' + try: + resps = [] + if all(["https_host_meta" in context, "http_host_meta" in context]): + resps = [context.http_res, context.https_res] + else: + resps = [context.resp] + + for resp in resps: + assert ( + "application/xrd+xml" in resp.headers["Content-Type"] + ), "Document served with 'application/xrd+xml' media type" + except Exception as e: + logger.error(e) + context.failure[name] = e + raise e + context.success.append(name) + + +@then('The host-meta document root MUST be an "XRD" element') +def step_impl(context): + name = 'The host-meta document root MUST be an "XRD" element' + try: + assert len(context.host_meta) == 1, "Only one root element" + assert "XRD" in context.host_meta, "'XRX' is the root element" + except Exception as e: + logger.error(e) + context.failure[name] = e + raise e + context.success.append(name) + + +@then('The document SHOULD NOT include a "Subject" element') +def step_impl(context): + name = 'The document SHOULD NOT include a "Subject" element' + try: + assert ( + "Subject" not in context.host_meta["XRD"] + ), "Document SHOULD NOT include a 'Subject' element" + except Exception as e: + logger.error(e) + context.failure[name] = e + raise e + context.success.append(name) + + +@then('The document SHOULD include "Link" element') +def step_impl(context): + name = 'The document SHOULD include "Link" element' + try: + assert ( + "Link" in context.host_meta["XRD"] + ), "Document SHOULD include a 'Link' element" + except Exception as e: + logger.error(e) + context.failure[name] = e + raise e + context.success.append(name) + + +@then( + 'The document\'s "Link" element should include either "template" or "href" attributes' +) +def step_impl(context): + def _check(obj): + return any(["@template" in obj, "@href" in obj]) + + name = 'The document\'s "Link" element should include either "template" or "href" attributes' + try: + if isinstance(context.host_meta["XRD"]["Link"], collections.abc.Sequence): + for link in context.host_meta["XRD"]["Link"]: + assert _check( + link + ), f'The document\'s "Link" element should include either "template" or "href" attributes. Got {link}' + else: + link = context.host_meta["XRD"]["Link"] + assert _check( + link + ), f'The document\'s "Link" element should include either "template" or "href" attributes. Got {link}' + except Exception as e: + logger.error(e) + context.failure[name] = e + raise e + context.success.append(name) + + +@then("The document should have at least on 'lrdd' document") +def step_impl(context): + def _check(obj): + return obj["@rel"] == "lrdd" + + lrdd_found = False + name = "The document should have at least on 'lrdd' document" + + if isinstance(context.host_meta["XRD"]["Link"], collections.abc.Sequence): + for link in context.host_meta["XRD"]["Link"]: + if _check(link): + lrdd_found = True + break + else: + link = context.host_meta["XRD"]["Link"] + if _check(link): + lrdd_found = True + + try: + assert lrdd_found is True + except Exception as e: + logger.error(e) + context.failure[name] = e + raise e + context.success.append(name) + + +@then( + "Fediverse specific: The lrdd document must include template containing WebFinger well-known URI" +) +def step_impl(context): + def _check(obj): + if obj["@rel"] == "lrdd": + if "@template" in obj: + if "/.well-known/webfinger?resource={uri}" in obj["@template"]: + return True + return False + + webfinger_found = False + name = "Fediverse specific: The lrdd document must include template containing WebFinger well-known URI" + + if isinstance(context.host_meta["XRD"]["Link"], collections.abc.Sequence): + for link in context.host_meta["XRD"]["Link"]: + if _check(link): + webfinger_found = True + break + else: + link = context.host_meta["XRD"]["Link"] + if _check(link): + webfinger_found = True + + try: + assert webfinger_found is True + except Exception as e: + logger.error(e) + context.failure[name] = e + raise e + context.success.append(name)