feat: define BDD tests per RFC6415
This commit is contained in:
parent
76cb235dce
commit
469b002fb4
3 changed files with 311 additions and 0 deletions
44
features/environment.py
Normal file
44
features/environment.py
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
# SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
|
||||||
|
#
|
||||||
|
# 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)
|
15
features/host_meta.feature
Normal file
15
features/host_meta.feature
Normal file
|
@ -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
|
252
features/steps/host_meta.py
Normal file
252
features/steps/host_meta.py
Normal file
|
@ -0,0 +1,252 @@
|
||||||
|
# SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
|
||||||
|
#
|
||||||
|
# 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)
|
Loading…
Reference in a new issue