Compare commits

...

5 Commits

14 changed files with 561 additions and 0 deletions

4
.env_sample Normal file
View File

@ -0,0 +1,4 @@
export FTEST_AUTH="foobar"
export FTEST_HOST="http://ftest.example.org"
export FTEST_TARGET_HOST="http://localhost:3000"
export FTEST_USER="john@example.org"

20
.woodpecker.yml Normal file
View File

@ -0,0 +1,20 @@
pipeline:
build_docker_img:
image: plugins/docker
when:
event: [pull_request]
settings:
dry_run: true
repo: forgeflux/ap-test
tags: latest
build_and_publish_docker_img:
image: plugins/docker
when:
event: [push, tag, deployment]
settings:
username: forgeflux
password:
from_secret: DOCKER_TOKEN
repo: forgeflux/ap-test
tags: latest

16
Dockerfile Normal file
View File

@ -0,0 +1,16 @@
FROM python
LABEL org.opencontainers.image.source https://git.batsense.net/ForgeFlux/ap-test
RUN useradd -ms /bin/bash -u 1001 ap
RUN apt-get update && apt-get install -y ca-certificates git
WORKDIR /src/
COPY requirements.txt .
RUN pip install -r requirements.txt
USER ap
WORKDIR /home/ap
RUN mkdir app/
WORKDIR /home/ap/app/
COPY . .
CMD [ "behave"]

13
Makefile Normal file
View File

@ -0,0 +1,13 @@
lint:
black ./common/ ./features/
docker.build:
docker buildx build -t forgeflux/ap-test --load .
docker.run:
docker run \
--network=ftest \
-e FTEST_AUTH=${FTEST_AUTH} \
-e FTEST_HOST=${FTEST_HOST} \
-e FTEST_TARGET_HOST=${FTEST_TARGET_HOST} \
-e FTEST_USER=${FTEST_USER} forgeflux/ap-test

219
README.md Normal file
View File

@ -0,0 +1,219 @@
[![status-badge](https://ci.batsense.net/api/badges/86/status.svg)](https://ci.batsense.net/repos/86)
---
# ActivityPub tests for ForgeFlux/ftest
## TODO
- [ ] Write code to upload to ftest server
- [ ] Package Docker container
- [ ] Upload to Docker Hub
## Environment Variables
Please see [`.env_sample`](./.env_sample)
| NAME | Purpose | Example |
| ------------------- | ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `FTEST_AUTH` | Authorization token for uploading test results to ftest server | a random-generated string that will be provided by ftest server |
| `FTEST_HOST` | The URL of the ftest's instance to which tests results should be uploaded | `http://localhost:5000`, if an ftest implementation is running on localhost at port 5000 |
| `FTEST_TARGET_HOST` | The URL of the implementation's instance against which tests will be run | `http://localhost:3000`, if Forgejo or any other ActivityPub implementation is running on localhost at port 3000 |
| `FTEST_USER` | address of the actor against which ActivityPub tests will be run | `john@localhost:3000`. User `john` should exist on the instance |
## Usage
```bash
cp .env_sample .env # edit and populate .env with the right values
source .env
python run.py
```
### Docker
```bash
cp .env_sample .env # edit and populate .env with the right values
source .env
docker run \
-e FTEST_AUTH=$FTEST_AUTH \
-e FTEST_TARGET_HOST=$FTEST_TARGET_HOST \
-e FTEST_USER=$FTEST_USER forgeflux/webfinger-test
```
## Demo
### Successful run:
<details>
<summary>Please open to see logs</summary>
```bash
19:29 (venv) atm@lab activitypub ±|master ✗|→ make docker.run
docker run \
--network=ftest \
-e FTEST_AUTH=aACGJUaWKbUAynhz54762jYC0lcTtIei \
-e FTEST_HOST=http://localhost:9000 \
-e FTEST_TARGET_HOST=http://forgejo:7000 \
-e FTEST_USER=alice@forgejo:7000 forgeflux/ap-test
2023-10-01 13:59:50,866 - ap - INFO - Environment: FTEST_AUTH: aACGJUaWKbUAynhz54762jYC0lcTtIei
2023-10-01 13:59:50,866 - ap - INFO - Environment: FTEST_HOST: http://localhost:9000
2023-10-01 13:59:50,866 - ap - INFO - Environment: FTEST_USER: alice@forgejo:7000
2023-10-01 13:59:50,866 - ap - INFO - Environment: FTEST_TARGET_HOST: http://forgejo:7000
Feature: ActivityPub Object # features/object.feature:8
ActivityPub Object as defined in https://www.w3.org/TR/activitypub/#obj-id
Scenario: # features/object.feature:11
Given A Fediverse server # features/steps/obejct.py:12
2023-10-01 13:59:50,869 - ap - INFO - Query webfinger: http://forgejo:7000/.well-known/webfinger?resource=acct:alice@forgejo:7000
2023-10-01 13:59:50,872 - ap - DEBUG - webfinger response:
STATUS: 200
HEADERS:
{'Access-Control-Allow-Origin': '*', 'Cache-Control': 'max-age=0, private, must-revalidate, no-transform', 'Content-Type': 'application/json;charset=utf-8', 'Set-Cookie': 'i_like_gitea=6b18e16aba159c57; Path=/; HttpOnly; SameSite=Lax, _csrf=jvm3uaKAgUmAJqMuqqBeqdARdxY6MTY5NjE2ODc5MDg3MTIxNzc2Nw; Path=/; Max-Age=86400; HttpOnly; SameSite=Lax', 'X-Frame-Options': 'SAMEORIGIN', 'Date': 'Sun, 01 Oct 2023 13:59:50 GMT', 'Content-Length': '507'}
RESPONSE PAYLOAD:
{'subject': 'acct:alice@forgejo:7000', 'aliases': ['http://forgejo:7000/alice', 'http://forgejo:7000/api/v1/activitypub/user-id/3', 'mailto:alice@example.org'], 'links': [{'rel': 'http://webfinger.net/rel/profile-page', 'type': 'text/html', 'href': 'http://forgejo:7000/alice'}, {'rel': 'http://webfinger.net/rel/avatar', 'href': 'https://secure.gravatar.com/avatar/fbf7c6aec1d4280b7c2704c1c0478bd6?d=identicon'}, {'rel': 'self', 'type': 'application/activity+json', 'href': 'http://forgejo:7000/api/v1/activitypub/user-id/3'}]}
2023-10-01 13:59:50,872 - ap - INFO - [SUCCESS] webfinger query response is HTTP 200
2023-10-01 13:59:50,872 - ap - INFO - Actor URL http://forgejo:7000/api/v1/activitypub/user-id/3
When Receiving or querying an object # features/steps/obejct.py:28
2023-10-01 13:59:50,872 - ap - INFO - Getting object: http://forgejo:7000/api/v1/activitypub/user-id/3
2023-10-01 13:59:50,874 - ap - DEBUG - AP server response:
STATUS: 200
HEADERS:
{'Cache-Control': 'max-age=0, private, must-revalidate, no-transform', 'Content-Type': 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', 'X-Content-Type-Options': 'nosniff', 'X-Frame-Options': 'SAMEORIGIN', 'Date': 'Sun, 01 Oct 2023 13:59:50 GMT', 'Content-Length': '1103'}
RESPONSE PAYLOAD:
{'@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'], 'id': 'http://forgejo:7000/api/v1/activitypub/user-id/3', 'type': 'Person', 'icon': {'type': 'Image', 'mediaType': 'image/png', 'url': 'https://secure.gravatar.com/avatar/fbf7c6aec1d4280b7c2704c1c0478bd6?d=identicon'}, 'url': 'http://forgejo:7000/alice', 'inbox': 'http://forgejo:7000/api/v1/activitypub/user-id/3/inbox', 'outbox': 'http://forgejo:7000/api/v1/activitypub/user-id/3/outbox', 'preferredUsername': 'alice', 'publicKey': {'id': 'http://forgejo:7000/api/v1/activitypub/user-id/3#main-key', 'owner': 'http://forgejo:7000/api/v1/activitypub/user-id/3', 'publicKeyPem': '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArMXwd2H5QH1NQJ8FOhaO\nAn9xfchx6wcifO6yBkzt8YCemDXqc9VKpq6Uyz5tQT5S5r7MKF56Htibc1Fx2K6A\n7ra9S+PguY/V6ibWuv1i8J/78wcE1bpVxedl59NH8qbPD+025+7Wfsc2TYNJJsKI\n0AOoeZjc9ug2JGZ5WFk0aNSgZuYwMMb+wiyM+tGM5f2nGm2vhaNy9cxZLuwJW2sn\nC7tdvk5dnWShMzbuhmYoKAsJDlM7C+NNShi7WAX21cx92JULxT8mXYJmGa2Z1TQl\nhX4PSIwSkWOrDO8Y2D+LCkmFep8iZezPdDQAnioQanGY5jX7Y2rMfH7/xwm6RinZ\njQIDAQAB\n-----END PUBLIC KEY-----\n'}}
Then the response must contain an 'id' and a 'type' parameter # features/steps/obejct.py:35
And The 'id' must resolve to the same object # features/steps/obejct.py:42
2023-10-01 13:59:50,874 - ap - INFO - Getting object: http://forgejo:7000/api/v1/activitypub/user-id/3
2023-10-01 13:59:50,876 - ap - DEBUG - AP server response:
STATUS: 200
HEADERS:
{'Cache-Control': 'max-age=0, private, must-revalidate, no-transform', 'Content-Type': 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', 'X-Content-Type-Options': 'nosniff', 'X-Frame-Options': 'SAMEORIGIN', 'Date': 'Sun, 01 Oct 2023 13:59:50 GMT', 'Content-Length': '1103'}
RESPONSE PAYLOAD:
{'@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'], 'id': 'http://forgejo:7000/api/v1/activitypub/user-id/3', 'type': 'Person', 'icon': {'type': 'Image', 'mediaType': 'image/png', 'url': 'https://secure.gravatar.com/avatar/fbf7c6aec1d4280b7c2704c1c0478bd6?d=identicon'}, 'url': 'http://forgejo:7000/alice', 'inbox': 'http://forgejo:7000/api/v1/activitypub/user-id/3/inbox', 'outbox': 'http://forgejo:7000/api/v1/activitypub/user-id/3/outbox', 'preferredUsername': 'alice', 'publicKey': {'id': 'http://forgejo:7000/api/v1/activitypub/user-id/3#main-key', 'owner': 'http://forgejo:7000/api/v1/activitypub/user-id/3', 'publicKeyPem': '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArMXwd2H5QH1NQJ8FOhaO\nAn9xfchx6wcifO6yBkzt8YCemDXqc9VKpq6Uyz5tQT5S5r7MKF56Htibc1Fx2K6A\n7ra9S+PguY/V6ibWuv1i8J/78wcE1bpVxedl59NH8qbPD+025+7Wfsc2TYNJJsKI\n0AOoeZjc9ug2JGZ5WFk0aNSgZuYwMMb+wiyM+tGM5f2nGm2vhaNy9cxZLuwJW2sn\nC7tdvk5dnWShMzbuhmYoKAsJDlM7C+NNShi7WAX21cx92JULxT8mXYJmGa2Z1TQl\nhX4PSIwSkWOrDO8Y2D+LCkmFep8iZezPdDQAnioQanGY5jX7Y2rMfH7/xwm6RinZ\njQIDAQAB\n-----END PUBLIC KEY-----\n'}}
And context must be ActivityPub # features/steps/obejct.py:48
1 feature passed, 0 failed, 0 skipped
1 scenario passed, 0 failed, 0 skipped
5 steps passed, 0 failed, 0 skipped, 0 undefined
Took 0m0.007s
```
</details>
### Failed run:
<details>
<summary>Please open to see logs</summary>
```bash
19:48 (venv) atm@lab activitypub ±|master ✗|→ behave
2023-10-01 19:49:39,187 - ap - INFO - Environment: FTEST_AUTH: foobar
2023-10-01 19:49:39,187 - ap - INFO - Environment: FTEST_HOST: http://localhost:9000
2023-10-01 19:49:39,187 - ap - INFO - Environment: FTEST_USER: realaravinth@batsense.net
2023-10-01 19:49:39,187 - ap - INFO - Environment: FTEST_TARGET_HOST: https://gts.batsense.net
Feature: ActivityPub Object # features/object.feature:8
ActivityPub Object as defined in https://www.w3.org/TR/activitypub/#obj-id
Scenario: # features/object.feature:11
Given A Fediverse server # features/steps/object.py:12
2023-10-01 19:49:39,190 - ap - INFO - Query webfinger: https://gts.batsense.net/.well-known/webfinger?resource=acct:realaravinth@batsense.net
2023-10-01 19:49:39,265 - ap - DEBUG - webfinger response:
STATUS: 200
HEADERS:
{'Server': 'nginx/1.18.0', 'Date': 'Sun, 01 Oct 2023 14:19:39 GMT', 'Content-Type': 'application/jrd+json', 'Content-Length': '197', 'Connection': 'keep-alive', 'Cache-Control': 'public, max-age=120', 'Content-Encoding': 'gzip', 'Content-Security-Policy': "default-src 'self'; object-src 'none'; img-src 'self' blob:; media-src 'self'", 'Permissions-Policy': 'browsing-topics=(), interest-cohort=()', 'Vary': 'Accept-Encoding', 'X-Ratelimit-Limit': '300', 'X-Ratelimit-Remaining': '289', 'X-Ratelimit-Reset': '1696170087', 'X-Request-Id': 'd3psqtwa04001t6ca510'}
RESPONSE PAYLOAD:
{'subject': 'acct:realaravinth@batsense.net', 'aliases': ['https://gts.batsense.net/users/realaravinth', 'https://gts.batsense.net/@realaravinth'], 'links': [{'rel': 'http://webfinger.net/rel/profile-page', 'type': 'text/html', 'href': 'https://gts.batsense.net/@realaravinth'}, {'rel': 'self', 'type': 'application/activity+json', 'href': 'https://gts.batsense.net/users/realaravinth'}]}
2023-10-01 19:49:39,266 - ap - INFO - [SUCCESS] webfinger query response is HTTP 200
Given A Fediverse server # features/steps/object.py:12 0.077s
When Receiving or querying an object # features/steps/object.py:27
2023-10-01 19:49:39,267 - ap - INFO - Getting object: https://gts.batsense.net/users/realaravinth
2023-10-01 19:49:39,505 - ap - DEBUG - AP server response:
STATUS: 401
HEADERS:
{'Server': 'nginx/1.18.0', 'Date': 'Sun, 01 Oct 2023 14:19:39 GMT', 'Content-Type': 'application/json; charset=utf-8', 'Content-Length': '104', 'Connection': 'keep-alive', 'Cache-Control': 'no-store', 'Content-Encoding': 'gzip', 'Content-Security-Policy': "default-src 'self'; object-src 'none'; img-src 'self' blob:; media-src 'self'", 'Permissions-Policy': 'browsing-topics=()', 'Vary': 'Accept-Encoding', 'X-Ratelimit-Limit': '300', 'X-Ratelimit-Remaining': '288', 'X-Ratelimit-Reset': '1696170087', 'X-Request-Id': 'bbq9qtwa040015ce6dk0'}
RESPONSE PAYLOAD:
{"error":"Unauthorized: http request wasn't signed or http signature was invalid: (verifier)"}
2023-10-01 19:49:39,505 - ap - ERROR - Request FAILURE. AP server response:
STATUS: 401
HEADERS:
{'Server': 'nginx/1.18.0', 'Date': 'Sun, 01 Oct 2023 14:19:39 GMT', 'Content-Type': 'application/json; charset=utf-8', 'Content-Length': '104', 'Connection': 'keep-alive', 'Cache-Control': 'no-store', 'Content-Encoding': 'gzip', 'Content-Security-Policy': "default-src 'self'; object-src 'none'; img-src 'self' blob:; media-src 'self'", 'Permissions-Policy': 'browsing-topics=()', 'Vary': 'Accept-Encoding', 'X-Ratelimit-Limit': '300', 'X-Ratelimit-Remaining': '288', 'X-Ratelimit-Reset': '1696170087', 'X-Request-Id': 'bbq9qtwa040015ce6dk0'}
RESPONSE PAYLOAD:
When Receiving or querying an object # features/steps/object.py:27 0.238s
Assertion Failed: Request FAILURE. AP server response:
STATUS: 401
HEADERS:
{'Server': 'nginx/1.18.0', 'Date': 'Sun, 01 Oct 2023 14:19:39 GMT', 'Content-Type': 'application/json; charset=utf-8', 'Content-Length': '104', 'Connection': 'keep-alive', 'Cache-Control': 'no-store', 'Content-Encoding': 'gzip', 'Content-Security-Policy': "default-src 'self'; object-src 'none'; img-src 'self' blob:; media-src 'self'", 'Permissions-Policy': 'browsing-topics=()', 'Vary': 'Accept-Encoding', 'X-Ratelimit-Limit': '300', 'X-Ratelimit-Remaining': '288', 'X-Ratelimit-Reset': '1696170087', 'X-Request-Id': 'bbq9qtwa040015ce6dk0'}
RESPONSE PAYLOAD:
{"error":"Unauthorized: http request wasn't signed or http signature was invalid: (verifier)"}
Captured logging:
INFO:ap:Query webfinger: https://gts.batsense.net/.well-known/webfinger?resource=acct:realaravinth@batsense.net
INFO:ap:[SUCCESS] webfinger query response is HTTP 200
INFO:ap:Actor URL https://gts.batsense.net/users/realaravinth
INFO:ap:Getting object: https://gts.batsense.net/users/realaravinth
ERROR:ap:Request FAILURE. AP server response:
STATUS: 401
HEADERS:
{'Server': 'nginx/1.18.0', 'Date': 'Sun, 01 Oct 2023 14:19:39 GMT', 'Content-Type': 'application/json; charset=utf-8', 'Content-Length': '104', 'Connection': 'keep-alive', 'Cache-Control': 'no-store', 'Content-Encoding': 'gzip', 'Content-Security-Policy': "default-src 'self'; object-src 'none'; img-src 'self' blob:; media-src 'self'", 'Permissions-Policy': 'browsing-topics=()', 'Vary': 'Accept-Encoding', 'X-Ratelimit-Limit': '300', 'X-Ratelimit-Remaining': '288', 'X-Ratelimit-Reset': '1696170087', 'X-Request-Id': 'bbq9qtwa040015ce6dk0'}
RESPONSE PAYLOAD:
{"error":"Unauthorized: http request wasn't signed or http signature was invalid: (verifier)"}
Then the response must contain an 'id' and a 'type' parameter # None
And The 'id' must resolve to the same object # None
And context must be ActivityPub # None
===============
Summary:
Failed tests:
[FAIL] check_actor_exists failed with error:
Request FAILURE. AP server response:
STATUS: 401
HEADERS:
{'Server': 'nginx/1.18.0', 'Date': 'Sun, 01 Oct 2023 14:19:39 GMT', 'Content-Type': 'application/json; charset=utf-8', 'Content-Length': '104', 'Connection': 'keep-alive', 'Cache-Control': 'no-store', 'Content-Encoding': 'gzip', 'Content-Security-Policy': "default-src 'self'; object-src 'none'; img-src 'self' blob:; media-src 'self'", 'Permissions-Policy': 'browsing-topics=()', 'Vary': 'Accept-Encoding', 'X-Ratelimit-Limit': '300', 'X-Ratelimit-Remaining': '288', 'X-Ratelimit-Reset': '1696170087', 'X-Request-Id': 'bbq9qtwa040015ce6dk0'}
RESPONSE PAYLOAD:
{"error":"Unauthorized: http request wasn't signed or http signature was invalid: (verifier)"}
-----
Failing scenarios:
features/object.feature:11
0 features passed, 1 failed, 0 skipped
0 scenarios passed, 1 failed, 0 skipped
1 step passed, 1 failed, 3 skipped, 0 undefined
Took 0m0.315s
```
</details>

5
common/__init__.py Normal file
View File

@ -0,0 +1,5 @@
# SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
from .logger import logger

23
common/env.py Normal file
View File

@ -0,0 +1,23 @@
# SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
import os
from .logger import logger
def get_env(name) -> str:
env = FTEST_AUTH = os.environ.get(name)
logger.info(f"Environment: {name}: {env}")
if env is None:
raise Exception(
f"Please set environment variable {name}. See https://git.batsense.net/ForgeFlux/nodeinfo-test#environment-variables"
)
return env
FTEST_AUTH = get_env("FTEST_AUTH")
FTEST_HOST = get_env("FTEST_HOST")
FTEST_USER = get_env("FTEST_USER")
TARGET_HOST = get_env("FTEST_TARGET_HOST")

50
common/logger.py Normal file
View File

@ -0,0 +1,50 @@
# SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
import logging
LOG_FILE = "ap.log"
def configure_logger():
logger = logging.getLogger("ap")
logger.setLevel(logging.DEBUG)
fh = logging.FileHandler(LOG_FILE)
fh.setLevel(logging.DEBUG)
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
fh.setFormatter(formatter)
ch.setFormatter(formatter)
logger.addHandler(fh)
logger.addHandler(ch)
return logger
logger = configure_logger()
def upload_logs_to_ftest(success: bool, logs: str):
parsed_ftest_host = urlparse(FTEST_HOST)
ftest = urlunparse(
(
parsed_ftest_host.scheme,
parsed_ftest_host.netloc,
f"/api/v1/{FTEST_AUTH}/results",
"",
"",
"",
)
)
logger.info(f"Uploading logs to ftest server {ftest}")
payload = {"success": success, "logs": logs}
res = requests.post(ftest, json=payload, headers={"Origin": "http://example.org"})
if res.status_code == 200:
logger.info("Upload successful")
else:
print(res)

18
common/obj.py Normal file
View File

@ -0,0 +1,18 @@
# SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
import os
import requests
from .logger import logger
def get_ap_obj(url):
logger.info(f"Getting object: {url}")
res = requests.get(url, headers={"Accept": "application/activity+json"})
msg = f"AP server response:\n\nSTATUS: {res.status_code}\n\nHEADERS:\n {res.headers}\n\nRESPONSE PAYLOAD:\n{res.text}"
logger.debug(msg)
assert res.status_code == 200, f"Request FAILURE. {msg}"
return res.json()

33
common/webfinger.py Normal file
View File

@ -0,0 +1,33 @@
# SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
from urllib.parse import urlparse, urlunparse
import requests
from .logger import logger
def get_webfinger():
from .env import TARGET_HOST, FTEST_USER
parsed_target_host = urlparse(TARGET_HOST)
webfinger = urlunparse(
(
parsed_target_host.scheme,
parsed_target_host.netloc,
"/.well-known/webfinger",
"",
f"resource=acct:{FTEST_USER}",
"",
)
)
logger.info(f"Query webfinger: {webfinger}")
res = requests.get(webfinger)
logger.debug(
f"webfinger response:\n\nSTATUS: {res.status_code}\n\nHEADERS:\n {res.headers}\n\nRESPONSE PAYLOAD:\n{res.json()}"
)
assert res.status_code == 200
logger.info("[SUCCESS] webfinger query response is HTTP 200")
return res.json()

40
features/environment.py Normal file
View File

@ -0,0 +1,40 @@
from behave import *
from common.logger import upload_logs_to_ftest
def before_all(context):
context.success = []
context.failure = {}
def after_all(context):
max_score = 3
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 "failure" in context:
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)

16
features/object.feature Normal file
View File

@ -0,0 +1,16 @@
#Feature: showing off behave
#
# Scenario: run a simple test
# Given we have behave installed
# When we implement a test
# Then behave will test it for us!
Feature: ActivityPub Object
ActivityPub Object as defined in https://www.w3.org/TR/activitypub/#obj-id
Scenario:
Given A Fediverse server
When Receiving or querying an object
Then the response must contain an 'id' and a 'type' parameter
And The 'id' must resolve to the same object
And context must be ActivityPub

90
features/steps/object.py Normal file
View File

@ -0,0 +1,90 @@
# SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
import os
from urllib.parse import urlparse, urlunparse
import requests
from behave import *
from common.env import FTEST_USER
from common import logger
from common.webfinger import get_webfinger
from common.obj import get_ap_obj
@given("A Fediverse server")
def get_fediverse_actor(context):
webfinger = get_webfinger()
actor_url = None
for link in webfinger["links"]:
if "type" in link:
if all(
[link["rel"] == "self", link["type"] == "application/activity+json"]
):
actor_url = link["href"]
assert actor_url is not None
logger.info(f"Actor URL {actor_url}")
context.actor_url = actor_url
@when("Receiving or querying an object")
def check_actor_exists(context):
name = "check_actor_exists"
try:
actor = get_ap_obj(context.actor_url)
assert actor is not None
except Exception as e:
logger.error(e)
context.failure[name] = e
raise e
context.success.append(name)
context.actor = actor
@then("the response must contain an 'id' and a 'type' parameter")
def verify_obj_attrs(context):
name = "verify_obj_attrs"
actor = context.actor
try:
assert "id" in actor, "'id' attribute present in object'"
assert "type" in actor, "'type' attribute present in object'"
except Exception as e:
logger.error(e)
context.failure[name] = e
raise e
context.success.append(name)
@then("The 'id' must resolve to the same object")
def verify_id_resolves(context):
name = "verify_id_resolves"
actor = context.actor
try:
assert get_ap_obj(actor["id"]) == actor, "'id' URL resolves to same object"
except Exception as e:
logger.error(e)
context.failure[name] = e
raise e
@then("context must be ActivityPub")
def ctx_is_ap(context):
name = "ctx_is_ap"
obj_ctx = context.actor["@context"]
activitypub_present = False
for ctx in obj_ctx:
if ctx == "https://www.w3.org/ns/activitystreams":
activitypub_present = True
break
try:
assert activitypub_present is True, "ActivityPub context is included"
except Exception as e:
logger.error(e)
context.failure[name] = e
raise e

14
requirements.txt Normal file
View File

@ -0,0 +1,14 @@
attrs==23.1.0
behave==1.2.6
certifi==2023.7.22
charset-normalizer==3.3.0
idna==3.4
jsonschema==4.19.1
jsonschema-specifications==2023.7.1
parse==1.19.1
parse-type==0.6.2
referencing==0.30.2
requests==2.31.0
rpds-py==0.10.3
six==1.16.0
urllib3==2.0.5