Reword the Molecule scenario comments

They were hard-wrapped at 80 characters, broke mid-parenthesis, and spent lines
restating what the code below them does.

Rewrapped at natural boundaries instead, with the narration dropped and only the
reasons, gotchas and surprises kept. Section dividers stay - they delineate long
plays rather than narrate them.

Comments only; no scenario behaviour changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SEH3vxYSQ5SV4N5z61eyGT
This commit is contained in:
Slavi Pantaleev
2026-08-27 18:02:53 +03:00
co-authored by Claude Opus 5
parent e2d3be504e
commit c447e1528b
32 changed files with 458 additions and 641 deletions
+25 -31
View File
@@ -4,21 +4,18 @@
"""A stand-in homeserver for Molecule scenarios. """A stand-in homeserver for Molecule scenarios.
Most components in this playbook talk to a homeserver while starting up and Most components talk to a homeserver while starting up and exit if it is unreachable,
exit if it is unreachable, so a scenario cannot get them running without one. so a scenario cannot get them running without one. A real Synapse for every role would
Standing up a real Synapse for every role would dominate the run time and drag dominate the run time and drag in Postgres, and the scenarios are not testing Synapse.
in Postgres, and the scenarios are not testing Synapse - they are testing that
the role's configuration reaches the component and that it starts.
So this answers the handful of endpoints components touch during startup, with This answers the handful of endpoints components touch during startup, with the blandest
the blandest plausible response in each case. It is deliberately permissive: an plausible response in each case. Deliberately permissive: an unknown path returns `{}` with
unknown path returns `{}` with a 200 rather than a 404, because the goal is to a 200 rather than a 404, because the goal is to get the component past its startup checks.
get the component past its startup checks, not to model the Matrix spec.
What it is NOT: an authentication check, a room state machine, or anything a What it is NOT: an authentication check, a room state machine, or anything a scenario should
scenario should assert *about*. Assert on what the role rendered and on what the assert *about*. Assert on what the role rendered and what the component reports about itself.
component reports about itself. If a scenario starts needing this stub to behave A scenario that needs this stub to behave like a real homeserver has outgrown what these
like a real homeserver, that scenario has outgrown what these tests are for. tests are for.
""" """
import json import json
@@ -32,18 +29,17 @@ from urllib.parse import parse_qs, urlparse
SERVER_NAME = os.environ.get("STUB_SERVER_NAME", "molecule.local") SERVER_NAME = os.environ.get("STUB_SERVER_NAME", "molecule.local")
PORT = int(os.environ.get("STUB_PORT", "8008")) PORT = int(os.environ.get("STUB_PORT", "8008"))
# Rooms reported as already joined. Components that resolve a room mapping at # Rooms reported as already joined. Components that resolve a room mapping at startup
# startup (matrix-alertmanager-receiver, for one) fail if the rooms they were # (matrix-alertmanager-receiver, for one) fail if the rooms they were configured with
# configured with are missing, so a scenario passes its own room IDs in. # are missing, so a scenario passes its own room IDs in.
JOINED_ROOMS = [r for r in os.environ.get("STUB_JOINED_ROOMS", "").split(",") if r] JOINED_ROOMS = [r for r in os.environ.get("STUB_JOINED_ROOMS", "").split(",") if r]
USER_ID = os.environ.get("STUB_USER_ID", f"@stub:{SERVER_NAME}") USER_ID = os.environ.get("STUB_USER_ID", f"@stub:{SERVER_NAME}")
# Longest a /sync call is held open. Long-polling clients (anything on # Longest a /sync call is held open. Long-polling clients ask for a 30s timeout and
# matrix-sdk: baibot and the other bots) ask for a 30s timeout and immediately # immediately ask again when the call returns, so answering instantly spins them into a hot
# ask again when the call returns, so answering instantly would spin them into a # loop that eats the test machine. Honouring the requested timeout, capped here, keeps an
# hot loop that eats the test machine. Honouring the requested timeout, capped # idle bot idle.
# here, keeps an idle bot idle.
SYNC_MAX_HOLD_SECONDS = 30 SYNC_MAX_HOLD_SECONDS = 30
@@ -109,12 +105,10 @@ class Handler(BaseHTTPRequestHandler):
if path.endswith("/capabilities"): if path.endswith("/capabilities"):
return {"capabilities": {}} return {"capabilities": {}}
# Bots that authenticate with a username and password rather than as an # Where bots authenticating with a username and password log in, rather than as an
# appservice with a token log in here. Matched loosely on purpose: # appservice with a token. Matched loosely on purpose, because clients differ on the
# clients differ on the API version prefix (matrix-nio has shipped both # API version prefix, and a login falling through to the catch-all `{}` below looks
# /_matrix/client/r0/login and /_matrix/client/v3/login over time), and a # to the client like bad credentials.
# login that falls through to the catch-all `{}` below looks to the
# client like bad credentials.
if path.endswith("/login"): if path.endswith("/login"):
return { return {
"user_id": USER_ID, "user_id": USER_ID,
@@ -155,8 +149,8 @@ class Handler(BaseHTTPRequestHandler):
if path.endswith("/health") or path.endswith("/_matrix/federation/v1/version"): if path.endswith("/health") or path.endswith("/_matrix/federation/v1/version"):
return {"server": {"name": "molecule-stub", "version": "0"}} return {"server": {"name": "molecule-stub", "version": "0"}}
# Anything unrecognised: an empty object, so a component doing a startup # Anything unrecognised: an empty object, so a component probing an endpoint
# probe of an endpoint not listed here still gets past it. # not listed here still gets past it.
return {} return {}
def do_GET(self): def do_GET(self):
@@ -175,8 +169,8 @@ class Handler(BaseHTTPRequestHandler):
self._send({}) self._send({})
def log_message(self, fmt, *args): def log_message(self, fmt, *args):
# Quiet by default; STUB_VERBOSE=1 when a scenario will not start and you # Quiet by default. STUB_VERBOSE=1 when a scenario will not start and you need
# need to see what the component is actually asking for. # to see what the component is actually asking for.
if os.environ.get("STUB_VERBOSE"): if os.environ.get("STUB_VERBOSE"):
sys.stderr.write("stub: " + (fmt % args) + "\n") sys.stderr.write("stub: " + (fmt % args) + "\n")
+18 -28
View File
@@ -3,21 +3,18 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
--- ---
# The variables a role here reads from its surroundings rather than from its own # The variables a role reads from its surroundings rather than from its own defaults.
# defaults. In a real run `matrix-base` and `group_vars/matrix_servers` provide # In a real run `matrix-base` and `group_vars/matrix_servers` provide them.
# them; in a scenario they have to come from somewhere, and including
# `matrix-base` itself does far more than a role scenario needs.
# #
# Include from a scenario's prepare.yml, converge.yml and verify.yml: # Include from a scenario's prepare.yml, converge.yml and verify.yml:
# #
# vars_files: # vars_files:
# - "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/playbook-context.yml" # - "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/playbook-context.yml"
# #
# A scenario can override any of these in its own group_vars - that is the point # Gotcha: `vars_files` outranks inventory `group_vars`, so a scenario cannot override these there.
# of testing a role with values it would not have chosen for itself.
# #
# Keep this to variables that come from OUTSIDE the role under test. Anything the # Keep this to variables that come from OUTSIDE the role under test.
# role defines belongs in the scenario, not here. # Anything the role defines itself belongs in the scenario.
# --- Identity and paths (matrix-base) -------------------------------------- # --- Identity and paths (matrix-base) --------------------------------------
@@ -27,32 +24,26 @@ matrix_domain: molecule.local
matrix_user_name: matrix matrix_user_name: matrix
matrix_group_name: matrix matrix_group_name: matrix
# Deliberately not 1000: the base images already have a user there, so a distinct # Deliberately not 1000: the base images already have a user there, so a distinct id
# id is what proves a role used the one it was given rather than coinciding with # is what proves a role used the one it was given rather than coinciding with the image's own.
# the image's own.
matrix_user_uid: 1234 matrix_user_uid: 1234
matrix_user_gid: 1234 matrix_user_gid: 1234
# Empty in the playbook's own defaults too. Components that would invite an # Empty in the playbook's own defaults too. Components that would invite an administrator
# administrator into a room skip doing so when it is empty, which is what a # into a room skip doing so when it is empty.
# scenario wants.
matrix_admin: '' matrix_admin: ''
# --- Host commands (matrix-base) ------------------------------------------- # --- Host commands (matrix-base) -------------------------------------------
#
# Some roles shell out to a host binary through this indirection instead of
# naming it directly (matrix-bridge-hookshot and matrix-bridge-appservice-irc
# both generate a key with it). matrix-base's defaults are what supplies the
# value in a real run; those roles install the binary themselves, by including
# matrix-base's `ensure_openssl_installed` tasks.
# Some roles shell out through this indirection instead of naming the binary directly
# (matrix-bridge-hookshot and matrix-bridge-appservice-irc both generate a key with it).
# They install it themselves by including matrix-base's `ensure_openssl_installed` tasks.
matrix_host_command_openssl: "/usr/bin/env openssl" matrix_host_command_openssl: "/usr/bin/env openssl"
# --- Bridge-wide switches (matrix-base) ------------------------------------ # --- Bridge-wide switches (matrix-base) ------------------------------------
#
# Every bridge role reads these, so they live here rather than in each bridge's # Every bridge role reads these, so they live here rather than in each bridge's scenario.
# scenario. The values match the playbook's own defaults: encryption off, no # The values match the playbook's own defaults. A scenario proving one of these reaches
# relay, no MSC4190. A bridge scenario that wants to prove one of these reaches
# the rendered configuration should override it in its own group_vars. # the rendered configuration should override it in its own group_vars.
matrix_bridges_encryption_enabled: false matrix_bridges_encryption_enabled: false
@@ -66,10 +57,9 @@ matrix_bridges_exposure_hostname: molecule.local
matrix_bridges_exposure_path_prefix: /bridges matrix_bridges_exposure_path_prefix: /bridges
# --- Public hostnames (matrix-base) ---------------------------------------- # --- Public hostnames (matrix-base) ----------------------------------------
#
# 18 of the roles here read one of these. Rendered against the scenario's # 18 of the roles here read one of these. Rendered against the scenario's matrix_domain
# matrix_domain rather than left as Jinja, so a scenario can read them in # rather than left as Jinja, so verify.yml can read them without the role's defaults in scope.
# verify.yml without the role's defaults being in scope.
matrix_server_fqn_matrix: matrix.molecule.local matrix_server_fqn_matrix: matrix.molecule.local
matrix_server_fqn_matrix_federation: matrix.molecule.local matrix_server_fqn_matrix_federation: matrix.molecule.local
+1 -3
View File
@@ -1,7 +1,5 @@
--- ---
# Shared by every role scenario under roles/custom/*/molecule/, referenced from # Shared by every role scenario, so the pins cannot drift apart across roles.
# each scenario's molecule.yml. Kept in one place so the pins cannot drift
# apart across roles.
roles: roles:
- name: ansible-role-docker - name: ansible-role-docker
src: https://github.com/geerlingguy/ansible-role-docker src: https://github.com/geerlingguy/ansible-role-docker
+11 -14
View File
@@ -3,8 +3,8 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
--- ---
# Stands up a stand-in homeserver on a container network, for scenarios whose # Stands up a stand-in homeserver on a container network, for scenarios whose component
# component contacts a homeserver while starting up. # contacts a homeserver while starting up.
# #
# Include from a scenario's prepare.yml: # Include from a scenario's prepare.yml:
# #
@@ -17,8 +17,7 @@
# #
# The component should then be pointed at http://matrix.molecule.local:8008. # The component should then be pointed at http://matrix.molecule.local:8008.
# #
# See molecule-shared/homeserver-stub.py for what it answers and, more # See molecule-shared/homeserver-stub.py for what it answers and, more importantly, what it is not.
# importantly, for what it is not.
- name: Ensure the homeserver stub script is present - name: Ensure the homeserver stub script is present
ansible.builtin.copy: ansible.builtin.copy:
@@ -37,8 +36,8 @@
changed_when: molecule_shared_stub_removal.rc == 0 changed_when: molecule_shared_stub_removal.rc == 0
failed_when: false failed_when: false
# The alias is what the component resolves, so its configuration can name a # The alias is what the component resolves, so its configuration can name a hostname
# hostname rather than a container name. # rather than a container name.
- name: Ensure the homeserver stub is running - name: Ensure the homeserver stub is running
ansible.builtin.command: ansible.builtin.command:
argv: argv:
@@ -50,15 +49,13 @@
- --network-alias={{ molecule_shared_stub_hostname | default('matrix.molecule.local') }} - --network-alias={{ molecule_shared_stub_hostname | default('matrix.molecule.local') }}
- --env=STUB_SERVER_NAME={{ molecule_shared_stub_server_name | default('molecule.local') }} - --env=STUB_SERVER_NAME={{ molecule_shared_stub_server_name | default('molecule.local') }}
- --env=STUB_JOINED_ROOMS={{ (molecule_shared_stub_joined_rooms | default([])) | join(',') }} - --env=STUB_JOINED_ROOMS={{ (molecule_shared_stub_joined_rooms | default([])) | join(',') }}
# Appservices call /whoami on startup and refuse to run if the id # Appservices call /whoami on startup and refuse to run if the id returned is not
# returned is not the bot user they were configured as, so a scenario # the bot user they were configured as, so a scenario bridging anything has to tell
# bridging anything has to tell the stub who it should claim to be. # the stub who it should claim to be.
- --env=STUB_USER_ID={{ molecule_shared_stub_user_id | default('@stub:' + (molecule_shared_stub_server_name | default('molecule.local'))) }} - --env=STUB_USER_ID={{ molecule_shared_stub_user_id | default('@stub:' + (molecule_shared_stub_server_name | default('molecule.local'))) }}
# Off by default. Set molecule_shared_stub_verbose to "1" to have the stub # Set molecule_shared_stub_verbose to "1" to log every request the stub is asked for.
# log every request it is asked for, which is both how you find out why a # How you find out why a component will not start, and for a component with no port of
# component will not start and - for a component that exposes no port of # its own, the only place to observe it acting on what the role configured.
# its own - the only place a scenario can observe it acting on what the
# role configured.
- --env=STUB_VERBOSE={{ molecule_shared_stub_verbose | default('') }} - --env=STUB_VERBOSE={{ molecule_shared_stub_verbose | default('') }}
- --volume=/root/molecule-homeserver-stub.py:/stub.py:ro - --volume=/root/molecule-homeserver-stub.py:/stub.py:ro
- "{{ molecule_shared_image_python }}" - "{{ molecule_shared_image_python }}"
+6 -8
View File
@@ -1,13 +1,11 @@
--- ---
# Helper container images the scenarios use for probing. They live here rather # Helper container images the scenarios use for probing. Here rather than inline in each
# than inline in each verify.yml so that there is one pin per image instead of # verify.yml, so there is one pin per image instead of one per role, and so Renovate can see
# one per role, and so Renovate can see them (see the customManager in # them. See the customManager in .github/renovate.json.
# .github/renovate.json).
# Used to reach a role's container over its own container network. A helper is # Used to reach a role's container over its own container network, because the role publishes
# needed because the role publishes no host port - exactly as in a real # no host port - exactly as in a real deployment. Publishing one for the test would collide
# deployment - and publishing one for the test would collide between scenarios # between scenarios running in parallel.
# running in parallel.
# renovate: datasource=docker depName=docker.io/curlimages/curl # renovate: datasource=docker depName=docker.io/curlimages/curl
molecule_shared_image_curl: "docker.io/curlimages/curl:8.11.1" molecule_shared_image_curl: "docker.io/curlimages/curl:8.11.1"
@@ -3,11 +3,9 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
--- ---
# The devture base roles carry the variables this role reads # The devture base roles carry the variables this role reads, the same way they do when
# (`devture_systemd_docker_base_*`, `devture_playbook_help_*`), the same way # the playbook runs. `matrix-base` is deliberately NOT included: it does far more than this
# they do when the playbook runs. `matrix-base` is deliberately NOT included: # role needs, and the two variables it would supply are set directly in molecule.yml.
# it does far more than this role needs, and the two variables it would supply
# are set directly in molecule.yml instead.
- name: Include roles for matrix-alertmanager-receiver Molecule tests - name: Include roles for matrix-alertmanager-receiver Molecule tests
hosts: all hosts: all
become: true become: true
@@ -26,8 +24,8 @@
loop_control: loop_control:
loop_var: role_name loop_var: role_name
# The role installs the unit but does not start it - in the playbook that is # The role installs the unit but does not start it; in the playbook that is
# `systemd_service_manager`'s job - so the scenario starts it here. # `systemd_service_manager`'s job.
- name: Ensure matrix-alertmanager-receiver is started - name: Ensure matrix-alertmanager-receiver is started
hosts: all hosts: all
become: true become: true
@@ -31,27 +31,24 @@ provisioner:
matrix_alertmanager_receiver_path_prefix: / matrix_alertmanager_receiver_path_prefix: /
matrix_alertmanager_receiver_container_network: matrix-alertmanager-receiver-molecule matrix_alertmanager_receiver_container_network: matrix-alertmanager-receiver-molecule
# verify.yml runs as its own play, where the role's defaults are out # verify.yml runs as its own play, where the role's defaults are out of scope,
# of scope, so the paths it reads are pinned here as literals. They # so the paths it reads are pinned here to match what the role derives.
# match what the role derives from matrix_base_data_path above.
matrix_alertmanager_receiver_base_path: /matrix/alertmanager-receiver matrix_alertmanager_receiver_base_path: /matrix/alertmanager-receiver
matrix_alertmanager_receiver_config_path: /matrix/alertmanager-receiver/config matrix_alertmanager_receiver_config_path: /matrix/alertmanager-receiver/config
# Traefik is not deployed in this scenario, so the labels the role would # Traefik is not deployed here, so the labels the role would render for it are
# render for it are switched off and their absence is asserted instead. # switched off and their absence is asserted instead.
matrix_alertmanager_receiver_container_labels_traefik_enabled: false matrix_alertmanager_receiver_container_labels_traefik_enabled: false
# Deliberately different from the role's own defaults (port 12345, # Different from the role's own defaults, so verify.yml can tell what the role
# metrics disabled, alerts under /alerts), so that `verify.yml` can tell # rendered apart from what the application would have done on its own.
# what the role rendered apart from what the application would have done
# on its own.
matrix_alertmanager_receiver_config_http_port: 12399 matrix_alertmanager_receiver_config_http_port: 12399
matrix_alertmanager_receiver_config_http_metrics_enabled: true matrix_alertmanager_receiver_config_http_metrics_enabled: true
matrix_alertmanager_receiver_config_http_metrics_path: /molecule-metrics matrix_alertmanager_receiver_config_http_metrics_path: /molecule-metrics
matrix_alertmanager_receiver_config_http_alerts_path_prefix: /molecule-alerts matrix_alertmanager_receiver_config_http_alerts_path_prefix: /molecule-alerts
# The homeserver IS reached at startup - the service fetches its joined # The homeserver IS reached at startup: the service fetches its joined rooms and
# rooms and exits 1 if that fails - so prepare.yml stands up a stub for it # exits 1 if that fails, so prepare.yml stands up a stub for it.
matrix_alertmanager_receiver_config_matrix_homeserver_url: http://matrix.molecule.local:8008 matrix_alertmanager_receiver_config_matrix_homeserver_url: http://matrix.molecule.local:8008
matrix_alertmanager_receiver_config_matrix_user_id: "@alertmanager:molecule.local" matrix_alertmanager_receiver_config_matrix_user_id: "@alertmanager:molecule.local"
matrix_alertmanager_receiver_config_matrix_access_token: molecule_access_token_4f2a91 matrix_alertmanager_receiver_config_matrix_access_token: molecule_access_token_4f2a91
@@ -31,9 +31,8 @@
docker_daemon_options: docker_daemon_options:
storage-driver: fuse-overlayfs storage-driver: fuse-overlayfs
# The role's file tasks set owner/group by name, and Ansible resolves those # The role's file tasks set owner/group by name, which Ansible resolves through the
# through the passwd database - so they have to exist before it runs. In a # passwd database, so they have to exist first. `matrix-base` creates them for real.
# real deployment `matrix-base` creates them.
- name: Ensure the matrix group exists - name: Ensure the matrix group exists
ansible.builtin.group: ansible.builtin.group:
name: "{{ matrix_group_name }}" name: "{{ matrix_group_name }}"
@@ -70,10 +69,9 @@
- matrix_alertmanager_receiver_molecule_network.rc != 0 - matrix_alertmanager_receiver_molecule_network.rc != 0
- "'already exists' not in matrix_alertmanager_receiver_molecule_network.stderr" - "'already exists' not in matrix_alertmanager_receiver_molecule_network.stderr"
# matrix-alertmanager-receiver contacts the homeserver while starting up - # The service fetches /_matrix/client/v3/joined_rooms to resolve its room mapping and
# it fetches /_matrix/client/v3/joined_rooms to resolve its room mapping and # exits 1 if that fails, so a homeserver has to exist for it to come up at all.
# exits 1 if that fails - so a homeserver has to exist for it to come up at # The shared stub is enough. See molecule-shared/homeserver-stub.py.
# all. The shared stub is enough; see molecule-shared/homeserver-stub.py.
- name: Ensure the homeserver stub is running - name: Ensure the homeserver stub is running
ansible.builtin.include_tasks: ansible.builtin.include_tasks:
file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/tasks/homeserver-stub.yml" file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/tasks/homeserver-stub.yml"
@@ -12,10 +12,9 @@
gather_facts: false gather_facts: false
tasks: tasks:
# The version is read out of the role's own defaults rather than pinned in # Read from the role's own defaults rather than pinned in molecule.yml, so the version
# molecule.yml, so that the assertion further down compares the running # assertion compares the running image against what defaults/main.yml ships.
# image against what defaults/main.yml actually ships. Pinning it here # Pinning it here would make that assertion compare the scenario with itself.
# would make that assertion compare the scenario with itself.
- name: Load the role's defaults under a separate name - name: Load the role's defaults under a separate name
ansible.builtin.include_vars: ansible.builtin.include_vars:
file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults/main.yml" file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults/main.yml"
@@ -30,10 +29,9 @@
delay: 5 delay: 5
failed_when: false failed_when: false
# `Restart=always` means a crash-looping container still reports `active`, # `Restart=always` means a crash-looping container still reports `active`, so the restart
# so the restart counter is checked alongside it. Asserted as `is defined` # counter is checked too. Asserted `is defined` because `| int` turns a missing property
# too, because `| int` turns a missing property into 0 and would pass # into 0 and would pass vacuously.
# vacuously on a systemd that does not expose it.
- name: Assert the service is active and has not been restarting - name: Assert the service is active and has not been restarting
ansible.builtin.assert: ansible.builtin.assert:
that: that:
@@ -47,9 +45,9 @@
automatic restart(s) automatic restart(s)
success_msg: "matrix-alertmanager-receiver.service is active and has not restarted" success_msg: "matrix-alertmanager-receiver.service is active and has not restarted"
# Probed from inside the container network rather than from the host: the # Probed from inside the container network rather than the host, because the role
# role publishes no host port, exactly as it does in a real deployment, # publishes no host port - exactly as in a real deployment, where Traefik reaches it
# where Traefik reaches it over the network instead. # over the network.
- name: Wait for matrix-alertmanager-receiver to answer on the port the role configured - name: Wait for matrix-alertmanager-receiver to answer on the port the role configured
ansible.builtin.command: ansible.builtin.command:
argv: argv:
@@ -70,9 +68,8 @@
delay: 5 delay: 5
failed_when: false failed_when: false
# The port and the metrics path are both non-default in this scenario, so a # Port and metrics path are both non-default here, so a 200 is only reachable if what
# 200 here is only reachable if the configuration the role rendered is what # the role rendered is what the process is running on.
# the process is actually running on.
- name: Assert the configured port and metrics path reached the process - name: Assert the configured port and metrics path reached the process
ansible.builtin.assert: ansible.builtin.assert:
that: that:
@@ -93,9 +90,8 @@
The metrics endpoint answered, but did not return Prometheus metrics The metrics endpoint answered, but did not return Prometheus metrics
success_msg: "The metrics endpoint returns Prometheus metrics" success_msg: "The metrics endpoint returns Prometheus metrics"
# A negative control for the assertion above: the role's own default metrics # Negative control for the assertion above: the role's own default metrics path must NOT
# path must NOT answer, or a 200 on the configured path would prove nothing # answer, or a 200 on the configured path would prove nothing.
# about the configuration having been applied.
- name: Ask for the role's default metrics path, which this scenario moved away from - name: Ask for the role's default metrics path, which this scenario moved away from
ansible.builtin.command: ansible.builtin.command:
argv: argv:
@@ -3,11 +3,9 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
--- ---
# The devture base roles carry the variables this role reads # The devture base roles carry the variables this role reads, the same way they do when
# (`devture_systemd_docker_base_*`, `devture_playbook_help_*`), the same way # the playbook runs. `matrix-base` is deliberately NOT included: it does far more than this
# they do when the playbook runs. `matrix-base` is deliberately NOT included: # role needs, and what it would supply comes from molecule-shared/playbook-context.yml.
# it does far more than this role needs, and what it would supply comes from
# molecule-shared/playbook-context.yml instead.
- name: Include roles for matrix-bot-baibot Molecule tests - name: Include roles for matrix-bot-baibot Molecule tests
hosts: all hosts: all
become: true become: true
@@ -26,8 +24,8 @@
loop_control: loop_control:
loop_var: role_name loop_var: role_name
# The role installs the unit but does not start it - in the playbook that is # The role installs the unit but does not start it; in the playbook that is
# `systemd_service_manager`'s job - so the scenario starts it here. # `systemd_service_manager`'s job.
- name: Ensure matrix-bot-baibot is started - name: Ensure matrix-bot-baibot is started
hosts: all hosts: all
become: true become: true
@@ -29,22 +29,18 @@ provisioner:
all: all:
matrix_bot_baibot_container_network: matrix-bot-baibot-molecule matrix_bot_baibot_container_network: matrix-bot-baibot-molecule
# verify.yml runs as its own play, where the role's defaults are out # verify.yml runs as its own play, where the role's defaults are out of scope,
# of scope, so the paths it reads are pinned here as literals. They # so the paths it reads are pinned here to match what the role derives.
# match what the role derives from matrix_base_data_path.
matrix_bot_baibot_base_path: /matrix/baibot matrix_bot_baibot_base_path: /matrix/baibot
matrix_bot_baibot_config_path: /matrix/baibot/config matrix_bot_baibot_config_path: /matrix/baibot/config
matrix_bot_baibot_data_path: /matrix/baibot/data matrix_bot_baibot_data_path: /matrix/baibot/data
# baibot is a plain Matrix client, not an appservice: it logs in with a # baibot is a plain Matrix client, not an appservice: it logs in with a password
# password and then syncs, so a homeserver has to answer for it to get # and then syncs, so a homeserver has to answer for it to get anywhere.
# anywhere. prepare.yml stands up the shared stub for that.
matrix_bot_baibot_config_homeserver_url: http://matrix.molecule.local:8008 matrix_bot_baibot_config_homeserver_url: http://matrix.molecule.local:8008
# Deliberately different from the role's defaults (localpart `baibot`, # Deliberately different from the role's defaults AND from baibot's own, so a passing
# name `baibot`, prefix `!bai`, self-introduction on) AND from baibot's # assertion cannot be explained by "it would have happened anyway".
# own built-in defaults, so that a passing assertion cannot be explained
# by "it would have happened anyway".
matrix_bot_baibot_config_user_mxid_localpart: molecule-baibot matrix_bot_baibot_config_user_mxid_localpart: molecule-baibot
matrix_bot_baibot_config_user_name: Molecule baibot matrix_bot_baibot_config_user_name: Molecule baibot
matrix_bot_baibot_config_user_password: molecule_baibot_password_5b7c14 matrix_bot_baibot_config_user_password: molecule_baibot_password_5b7c14
@@ -53,18 +49,15 @@ provisioner:
matrix_bot_baibot_config_access_admin_patterns: matrix_bot_baibot_config_access_admin_patterns:
- "@molecule-admin:molecule.local" - "@molecule-admin:molecule.local"
# `debug` rather than the role's `info`, so the journal carries what the # `debug` rather than the role's `info`, so the journal carries what the bot loaded.
# bot loaded. verify.yml reads it.
matrix_bot_baibot_config_logging_level_baibot: debug matrix_bot_baibot_config_logging_level_baibot: debug
# baibot talks to AI providers, and a scenario must not need a provider # A scenario must not need an AI provider account, and does not have to: providers are
# account. It does not have to: providers are contacted only when a # contacted only when a message asks an agent to do something, never at startup.
# message asks an agent to do something, never at startup. So a static # So the agent below carries a placeholder key and a base URL that resolves nowhere.
# agent is defined with a placeholder key and a base URL that resolves # Nothing is ever called, yet the definition still has to survive baibot's startup
# nowhere. Nothing is ever called, and the agent still has to survive # parsing - which is what proves the role's provider templating produced something
# baibot's startup parsing of `agents.static_definitions` - which is # the bot accepts.
# what proves the role's provider templating produced something the bot
# accepts.
matrix_bot_baibot_config_agents_static_definitions_anthropic_enabled: true matrix_bot_baibot_config_agents_static_definitions_anthropic_enabled: true
matrix_bot_baibot_config_agents_static_definitions_anthropic_id: molecule-anthropic matrix_bot_baibot_config_agents_static_definitions_anthropic_id: molecule-anthropic
matrix_bot_baibot_config_agents_static_definitions_anthropic_config_base_url: http://molecule-no-such-provider.invalid/v1 matrix_bot_baibot_config_agents_static_definitions_anthropic_config_base_url: http://molecule-no-such-provider.invalid/v1
@@ -31,9 +31,8 @@
docker_daemon_options: docker_daemon_options:
storage-driver: fuse-overlayfs storage-driver: fuse-overlayfs
# The role's file tasks set owner/group by name, and Ansible resolves those # The role's file tasks set owner/group by name, which Ansible resolves through the
# through the passwd database - so they have to exist before it runs. In a # passwd database, so they have to exist first. `matrix-base` creates them for real.
# real deployment `matrix-base` creates them.
- name: Ensure the matrix group exists - name: Ensure the matrix group exists
ansible.builtin.group: ansible.builtin.group:
name: "{{ matrix_group_name }}" name: "{{ matrix_group_name }}"
@@ -70,13 +69,11 @@
- matrix_bot_baibot_molecule_network.rc != 0 - matrix_bot_baibot_molecule_network.rc != 0
- "'already exists' not in matrix_bot_baibot_molecule_network.stderr" - "'already exists' not in matrix_bot_baibot_molecule_network.stderr"
# baibot logs in and then syncs forever; with no homeserver answering it # baibot logs in and then syncs forever; with no homeserver answering it never gets past
# never gets past login and the unit crash-loops. The shared stub is enough # login. The shared stub is enough - see molecule-shared/homeserver-stub.py for what it is not.
# - see molecule-shared/homeserver-stub.py for what it is not.
# #
# It is told to claim the bot's own MXID, because the bot resolves who it is # Told to claim the bot's own MXID, because the bot resolves who it is from what the
# from what the homeserver hands back at login, and everything it does # homeserver hands back at login, and its profile and own-message filtering hang off that.
# afterwards (its profile, its own-message filtering) hangs off that.
- name: Ensure the homeserver stub is running - name: Ensure the homeserver stub is running
ansible.builtin.include_tasks: ansible.builtin.include_tasks:
file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/tasks/homeserver-stub.yml" file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/tasks/homeserver-stub.yml"
@@ -12,10 +12,9 @@
gather_facts: false gather_facts: false
tasks: tasks:
# The version is read out of the role's own defaults rather than pinned in # Read from the role's own defaults rather than pinned in molecule.yml, so the version
# molecule.yml, so that the assertion further down compares the running # assertion compares the running image against what defaults/main.yml ships.
# image against what defaults/main.yml actually ships. Pinning it here # Pinning it here would make that assertion compare the scenario with itself.
# would make that assertion compare the scenario with itself.
- name: Load the role's defaults under a separate name - name: Load the role's defaults under a separate name
ansible.builtin.include_vars: ansible.builtin.include_vars:
file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults/main.yml" file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults/main.yml"
@@ -30,10 +29,9 @@
delay: 5 delay: 5
failed_when: false failed_when: false
# `Restart=always` means a crash-looping container still reports `active`, # `Restart=always` means a crash-looping container still reports `active`, so the restart
# so the restart counter is checked alongside it. Asserted as `is defined` # counter is checked too. Asserted `is defined` because `| int` turns a missing property
# too, because `| int` turns a missing property into 0 and would pass # into 0 and would pass vacuously.
# vacuously on a systemd that does not expose it.
- name: Assert the service is active and has not been restarting - name: Assert the service is active and has not been restarting
ansible.builtin.assert: ansible.builtin.assert:
that: that:
@@ -47,17 +45,15 @@
automatic restart(s) automatic restart(s)
success_msg: "matrix-bot-baibot.service is active and has not restarted" success_msg: "matrix-bot-baibot.service is active and has not restarted"
# baibot publishes no port of its own - it is a Matrix client, not a server - # baibot is a Matrix client, not a server, so what it says about itself has to come from
# so what it says about itself has to come from its output. The unit runs # its output. The unit runs `docker start --attach`, so `--log-driver=none` does not stop
# `docker start --attach`, so `--log-driver=none` on the container does not # the journal from carrying it.
# stop the journal from carrying it.
# #
# `Syncing..` is the line that matters, and it is what carries this scenario # `Syncing..` is what carries this scenario, not the unit check above. baibot does not exit
# rather than the unit check above. baibot does not exit when its startup # when startup goes wrong: a profile step it cannot complete is retried forever with a
# goes wrong: a profile step it cannot complete is retried forever with a # growing delay, so the unit stays `active` with `NRestarts` at 0 while the bot never
# growing delay, so the unit stays `active` with `NRestarts` at 0 while the # reaches its message loop. Point the avatar at a missing file and the assertion above
# bot never reaches its message loop. Point the avatar at a file that is not # still passes; this one does not.
# there and the assertion above still passes; this one does not.
- name: Wait for baibot to reach its sync loop - name: Wait for baibot to reach its sync loop
ansible.builtin.shell: ansible.builtin.shell:
cmd: >- cmd: >-
@@ -81,10 +77,8 @@
retrying profile setup retrying profile setup
success_msg: "baibot got past startup and is syncing" success_msg: "baibot got past startup and is syncing"
# `user.name` is the bot's display name. The scenario's value is neither the # The scenario's display name is neither the role's default nor what the stub reports the
# role's default (`baibot`) nor what the stub reports the account already has # account already has, so the bot wanting it can only have come from what the role rendered.
# (`stub`), so the bot naming this as what it wants can only have come from
# the configuration the role rendered.
- name: Assert the display name the role configured reached the process - name: Assert the display name the role configured reached the process
ansible.builtin.assert: ansible.builtin.assert:
that: that:
@@ -96,22 +90,19 @@
display name it wants, so `user.name` did not reach the process display name it wants, so `user.name` did not reach the process
success_msg: "baibot acts on the display name the role configured" success_msg: "baibot acts on the display name the role configured"
# The `logging` setting is one string carrying per-target levels # `logging` is one string carrying per-target levels, so proving it arrived means proving
# (`warn,mxlink=info,baibot=debug`), so proving it arrived means proving that # different targets ended up at different levels. A single global level satisfies neither half.
# different targets ended up at different levels - a single global level
# would satisfy neither half of this.
# #
# First clause: baibot's own records appear at DEBUG, which the role's # First clause: baibot's own records appear at DEBUG, which the role's default of `info`
# default of `info` would not produce. # would not produce.
# #
# Second clause is the control, and it is not vacuous: at DEBUG the crates # Second clause is the control, and it is not vacuous. At DEBUG the crates underneath are
# underneath (matrix-sdk and its spans, hyper, eyeball) are extremely # extremely talkative, so raising the catch-all turns these two records into roughly a
# talkative - raising the catch-all level turns these two records into # hundred. Their silence is the `warn` catch-all being enforced.
# roughly a hundred. Their silence is the `warn` catch-all being enforced.
# #
# A control on mxlink was tried first and is the trap here: mxlink happens to # The trap here: a control on mxlink was tried first, and mxlink emits no DEBUG records at
# emit no DEBUG records at all on a first run, so asserting their absence # all on a first run - so asserting their absence passed just as happily with mxlink set
# passed just as happily with mxlink set to `debug`. # to `debug`.
- name: Assert the per-target logging levels reached the process - name: Assert the per-target logging levels reached the process
ansible.builtin.assert: ansible.builtin.assert:
that: that:
@@ -150,10 +141,8 @@
vars: vars:
matrix_bot_baibot_config: "{{ matrix_bot_baibot_config_file.content | b64decode | from_yaml }}" matrix_bot_baibot_config: "{{ matrix_bot_baibot_config_file.content | b64decode | from_yaml }}"
# The role supports two mutually-exclusive authentication modes and refuses # The role refuses a configuration that sets both authentication modes. This scenario uses
# a configuration that sets both. This scenario uses the password mode, so # password mode, so the access-token keys must render as nulls, not be omitted or set.
# the access-token keys must be rendered as nulls rather than omitted or
# left with a value.
- name: Assert only the password authentication mode is rendered - name: Assert only the password authentication mode is rendered
ansible.builtin.assert: ansible.builtin.assert:
that: that:
@@ -165,14 +154,12 @@
vars: vars:
matrix_bot_baibot_config: "{{ matrix_bot_baibot_config_file.content | b64decode | from_yaml }}" matrix_bot_baibot_config: "{{ matrix_bot_baibot_config_file.content | b64decode | from_yaml }}"
# The agent presets are the most involved templating in this role: a # The agent presets are the most involved templating in this role: a per-provider template
# per-provider template is rendered to YAML, parsed, merged with an # rendered to YAML, parsed, merged with an extension, nested into the list. Asserted as a
# extension, and dropped into the list as a nested structure. This asserts # whole round trip, key by key.
# the whole round trip, key by key.
# #
# No provider is ever contacted. baibot calls one only when a message asks an # No provider is ever contacted. baibot calls one only when a message asks an agent to do
# agent to do something, and the base URL here resolves nowhere on purpose - # something, and the base URL here resolves nowhere on purpose.
# a scenario must not need an account with an AI provider.
- name: Assert the statically-defined agent survived the provider templating - name: Assert the statically-defined agent survived the provider templating
ansible.builtin.assert: ansible.builtin.assert:
that: that:
@@ -215,10 +202,8 @@
{{ matrix_bot_baibot_role_defaults.matrix_bot_baibot_version }} {{ matrix_bot_baibot_role_defaults.matrix_bot_baibot_version }}
success_msg: "The running container is the version defaults/main.yml pins" success_msg: "The running container is the version defaults/main.yml pins"
# The uid/gid come from outside the role (matrix-base supplies them in a real # The uid/gid come from outside the role and are deliberately not 1000, which the base
# run, molecule-shared/playbook-context.yml here) and are deliberately not # image already uses, so this cannot pass by coinciding with the image's own user.
# 1000, which the base image already uses - so this cannot pass by
# coincidence with whatever the image would have run as.
- name: Assert the container runs as the identity the playbook supplies - name: Assert the container runs as the identity the playbook supplies
ansible.builtin.assert: ansible.builtin.assert:
that: that:
@@ -228,10 +213,8 @@
({{ matrix_bot_baibot_container.stdout }}) ({{ matrix_bot_baibot_container.stdout }})
success_msg: "The container runs as the uid/gid the playbook supplies" success_msg: "The container runs as the uid/gid the playbook supplies"
# baibot keeps its session and crypto store here. The file existing proves # baibot keeps its session and crypto store here. The file existing proves the bind mount
# the bind mount is writable by the user the container runs as - a # is writable by the user the container runs as.
# read-only-root container whose data directory it could not write would
# never have got as far as logging in.
- name: Stat the session file baibot persists - name: Stat the session file baibot persists
ansible.builtin.stat: ansible.builtin.stat:
path: "{{ matrix_bot_baibot_data_path }}/session.json" path: "{{ matrix_bot_baibot_data_path }}/session.json"
@@ -3,11 +3,9 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
--- ---
# The devture base roles carry the variables this role reads # The devture base roles carry the variables this role reads, the same way they do when
# (`devture_systemd_docker_base_*`, `devture_playbook_help_*`), the same way # the playbook runs. `matrix-base` is deliberately NOT included: it does far more than this
# they do when the playbook runs. `matrix-base` is deliberately NOT included: # role needs, and what it would supply comes from molecule-shared/playbook-context.yml.
# it does far more than this role needs, and the variables it would supply come
# from molecule-shared/playbook-context.yml instead.
- name: Include roles for matrix-reminder-bot Molecule tests - name: Include roles for matrix-reminder-bot Molecule tests
hosts: all hosts: all
become: true become: true
@@ -26,8 +24,8 @@
loop_control: loop_control:
loop_var: role_name loop_var: role_name
# The role installs the unit but does not start it - in the playbook that is # The role installs the unit but does not start it; in the playbook that is
# `systemd_service_manager`'s job - so the scenario starts it here. # `systemd_service_manager`'s job.
- name: Ensure matrix-reminder-bot is started - name: Ensure matrix-reminder-bot is started
hosts: all hosts: all
become: true become: true
@@ -29,28 +29,25 @@ provisioner:
all: all:
matrix_bot_matrix_reminder_bot_container_network: matrix-reminder-bot-molecule matrix_bot_matrix_reminder_bot_container_network: matrix-reminder-bot-molecule
# Unlike the bridges, this bot is not an appservice: it logs into the # Unlike the bridges, this bot is not an appservice: it logs in as an ordinary user
# homeserver as an ordinary user with a password. The stub prepare.yml # with a password. The stub answers /_matrix/client/v3/login with an access token,
# stands up answers /_matrix/client/v3/login with an access token, which # which is all the bot needs to reach its sync loop.
# is all the bot needs to get past its login and into its sync loop.
matrix_bot_matrix_reminder_bot_matrix_homeserver_url: http://matrix.molecule.local:8008 matrix_bot_matrix_reminder_bot_matrix_homeserver_url: http://matrix.molecule.local:8008
# Deliberately different from the role's default localpart # Different from the role's default localpart, so verify.yml can tell what the role
# (`bot.matrix-reminder-bot`), so verify.yml can tell what the role
# rendered apart from what it would have rendered anyway. # rendered apart from what it would have rendered anyway.
matrix_bot_matrix_reminder_bot_matrix_user_id_localpart: molecule.reminder-bot matrix_bot_matrix_reminder_bot_matrix_user_id_localpart: molecule.reminder-bot
matrix_bot_matrix_reminder_bot_matrix_user_password: molecule_bot_password_4f2a91 matrix_bot_matrix_reminder_bot_matrix_user_password: molecule_bot_password_4f2a91
# The role has no default here and refuses to run without one. Also # The role has no default here and refuses to run without one. Also different from the
# different from the bot's own fallback (`Etc/UTC`), and it reaches the # bot's own fallback, and it reaches the container twice: the config file and TZ.
# container twice - through the config file and through TZ on the unit.
matrix_bot_matrix_reminder_bot_reminders_timezone: Europe/Sofia matrix_bot_matrix_reminder_bot_reminders_timezone: Europe/Sofia
# The role and the bot both default to `!`. # The role and the bot both default to `!`.
matrix_bot_matrix_reminder_bot_command_prefix: "%%" matrix_bot_matrix_reminder_bot_command_prefix: "%%"
# Both lists default to off with no entries, so turning them on with # Both lists default to off with no entries, so turning them on exercises the
# entries of our own exercises the `_auto + _custom` composition. # `_auto + _custom` composition.
matrix_bot_matrix_reminder_bot_allowlist_enabled: true matrix_bot_matrix_reminder_bot_allowlist_enabled: true
matrix_bot_matrix_reminder_bot_allowlist_regexes_custom: matrix_bot_matrix_reminder_bot_allowlist_regexes_custom:
- "@molecule-allowed:molecule.local" - "@molecule-allowed:molecule.local"
@@ -58,22 +55,19 @@ provisioner:
matrix_bot_matrix_reminder_bot_blocklist_regexes_custom: matrix_bot_matrix_reminder_bot_blocklist_regexes_custom:
- ".*:blocked.molecule.local" - ".*:blocked.molecule.local"
# The device name is hardcoded in the role's config template, so # The device name is hardcoded in the role's template, so overriding it is only
# overriding it is only possible through the extension mechanism. Doing # possible through the extension mechanism - which tests that merge too.
# it here means the merge of template + extension is tested too.
matrix_bot_matrix_reminder_bot_configuration_extension_yaml: | matrix_bot_matrix_reminder_bot_configuration_extension_yaml: |
matrix: matrix:
device_name: Molecule Reminder Bot device_name: Molecule Reminder Bot
# The SQLite database path is moved off the role's default (`bot.db`) so # Moved off the role's default name so verify.yml can assert the bot opened the path
# that verify.yml can assert the bot opened the path the role gave it, # the role gave it, with the default name as a negative control.
# with the default name as a negative control.
matrix_bot_matrix_reminder_bot_sqlite_database_path_local: /matrix/matrix-reminder-bot/data/molecule-reminders.db matrix_bot_matrix_reminder_bot_sqlite_database_path_local: /matrix/matrix-reminder-bot/data/molecule-reminders.db
matrix_bot_matrix_reminder_bot_sqlite_database_path_in_container: /data/molecule-reminders.db matrix_bot_matrix_reminder_bot_sqlite_database_path_in_container: /data/molecule-reminders.db
# verify.yml runs as its own play, where the role's defaults are out of # verify.yml runs as its own play, where the role's defaults are out of scope,
# scope, so the paths it reads are pinned here as literals. They match # so the paths it reads are pinned here to match what the role derives.
# what the role derives from `matrix_base_data_path`.
matrix_bot_matrix_reminder_bot_base_path: /matrix/matrix-reminder-bot matrix_bot_matrix_reminder_bot_base_path: /matrix/matrix-reminder-bot
matrix_bot_matrix_reminder_bot_config_path: /matrix/matrix-reminder-bot/config matrix_bot_matrix_reminder_bot_config_path: /matrix/matrix-reminder-bot/config
matrix_bot_matrix_reminder_bot_data_path: /matrix/matrix-reminder-bot/data matrix_bot_matrix_reminder_bot_data_path: /matrix/matrix-reminder-bot/data
@@ -31,9 +31,8 @@
docker_daemon_options: docker_daemon_options:
storage-driver: fuse-overlayfs storage-driver: fuse-overlayfs
# The role's file tasks set owner/group by name, and Ansible resolves those # The role's file tasks set owner/group by name, which Ansible resolves through the
# through the passwd database - so they have to exist before it runs. In a # passwd database, so they have to exist first. `matrix-base` creates them for real.
# real deployment `matrix-base` creates them.
- name: Ensure the matrix group exists - name: Ensure the matrix group exists
ansible.builtin.group: ansible.builtin.group:
name: "{{ matrix_group_name }}" name: "{{ matrix_group_name }}"
@@ -57,8 +56,8 @@
group: "{{ matrix_group_name }}" group: "{{ matrix_group_name }}"
mode: "0750" mode: "0750"
# The role creates this network itself during converge, but the homeserver # The role creates this network itself during converge, but the stub has to be on it
# stub has to be on it before the bot starts, so it is created here first. # before the bot starts.
- name: Ensure the container network the role attaches to exists - name: Ensure the container network the role attaches to exists
ansible.builtin.command: ansible.builtin.command:
argv: argv:
@@ -72,11 +71,9 @@
- matrix_bot_matrix_reminder_bot_molecule_network.rc != 0 - matrix_bot_matrix_reminder_bot_molecule_network.rc != 0
- "'already exists' not in matrix_bot_matrix_reminder_bot_molecule_network.stderr" - "'already exists' not in matrix_bot_matrix_reminder_bot_molecule_network.stderr"
# This bot is not an appservice - it logs in with the username and password # Not an appservice: it logs in with the username and password the role rendered, retrying
# the role rendered into its configuration, and retries every 15 seconds # every 15 seconds until that succeeds. The stub answers with an access token, which is
# until that succeeds. The shared stub answers the login with an access # enough to reach the sync loop. Nothing is asserted about the stub itself.
# token, which is enough to get it into its sync loop. Nothing is asserted
# about the stub itself; see molecule-shared/homeserver-stub.py.
- name: Ensure the homeserver stub is running - name: Ensure the homeserver stub is running
ansible.builtin.include_tasks: ansible.builtin.include_tasks:
file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/tasks/homeserver-stub.yml" file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/tasks/homeserver-stub.yml"
@@ -3,13 +3,12 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
--- ---
# What this proves: matrix-reminder-bot starts on the configuration the role # Proves matrix-reminder-bot starts on the configuration the role rendered, logs in as the
# rendered, logs into a homeserver as the user the role gave it, opens the # user the role gave it, opens the database at the path the role gave it, and is the version
# database at the path the role gave it, and is the version the role pins. # the role pins.
# #
# The bot has no HTTP surface of its own to probe, so the evidence is what it # The bot has no HTTP surface to probe, so the evidence is what it says about itself in the
# says about itself in the journal plus what it left on disk. It does NOT set # journal plus what it left on disk. It does NOT set real reminders. See docs/molecule-testing.md.
# real reminders and never will. See docs/molecule-testing.md.
- name: Verify matrix-reminder-bot - name: Verify matrix-reminder-bot
hosts: all hosts: all
become: true become: true
@@ -23,10 +22,9 @@
matrix_bot_matrix_reminder_bot_molecule_container_user: "{{ matrix_user_uid }}:{{ matrix_user_gid }}" matrix_bot_matrix_reminder_bot_molecule_container_user: "{{ matrix_user_uid }}:{{ matrix_user_gid }}"
tasks: tasks:
# The version is read out of the role's own defaults rather than pinned in # Read from the role's own defaults rather than pinned in molecule.yml, so the version
# molecule.yml, so that the assertion further down compares the running # assertion compares the running image against what defaults/main.yml ships.
# image against what defaults/main.yml actually ships. Pinning it here # Pinning it here would make that assertion compare the scenario with itself.
# would make that assertion compare the scenario with itself.
- name: Load the role's defaults under a separate name - name: Load the role's defaults under a separate name
ansible.builtin.include_vars: ansible.builtin.include_vars:
file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults/main.yml" file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults/main.yml"
@@ -41,12 +39,10 @@
delay: 5 delay: 5
failed_when: false failed_when: false
# `Restart=always` means a bot crash-looping on a configuration it cannot # `Restart=always` means a bot crash-looping on unreadable config still reports `active`,
# read still reports `active`, so the restart counter is checked too. The # so the restart counter is checked too. The config file is parsed before the bot's own
# config file is parsed before the bot's own catch-all retry loop starts, so # catch-all retry loop starts, so anything wrong in what the role rendered shows up here
# anything wrong in what the role rendered shows up here as restarts. # as restarts. Asserted `is defined` because `| int` turns a missing property into 0.
# Asserted as `is defined` too, because `| int` turns a missing property
# into 0 and would pass vacuously on a systemd that does not expose it.
- name: Assert the service is active and has not been restarting - name: Assert the service is active and has not been restarting
ansible.builtin.assert: ansible.builtin.assert:
that: that:
@@ -60,14 +56,12 @@
automatic restart(s) automatic restart(s)
success_msg: "matrix-bot-matrix-reminder-bot.service is active and has not restarted" success_msg: "matrix-bot-matrix-reminder-bot.service is active and has not restarted"
# The unit runs `docker start --attach`, so the container's output is in the # The unit runs `docker start --attach`, so the container's output is in the journal
# journal despite `--log-driver=none`. That is the only thing this bot # despite `--log-driver=none`. It is the only thing this bot reports about itself.
# reports about itself - it serves nothing over HTTP.
# #
# Filtered rather than tailed: the startup lines are the oldest ones in the # Filtered rather than tailed: startup lines are the OLDEST in the journal, so a
# journal, so a `--lines=N` tail would lose them behind anything the bot # `--lines=N` tail loses them behind anything logged later, and reading it whole pulls
# logs later, and reading the journal whole would pull an unbounded amount # unbounded text into a variable. The filter keeps the failure line too, so the
# of text into a variable. The filter keeps the failure line too, so the
# "did not fail to log in" assertion below still has something to see. # "did not fail to log in" assertion below still has something to see.
- name: Wait for the bot to report that it finished starting up - name: Wait for the bot to report that it finished starting up
ansible.builtin.shell: ansible.builtin.shell:
@@ -83,10 +77,9 @@
delay: 5 delay: 5
failed_when: false failed_when: false
# "Logged in as ..." is only reached after the bot's login call came back as # "Logged in as ..." is only reached once the login call returned something other than a
# something other than a LoginError, so this is the whole chain at once: the # LoginError, so this covers the whole chain at once: homeserver URL, user ID and password
# homeserver URL, the user ID and the password the role rendered were good # were all good enough for a real login round-trip.
# enough for a real login round-trip against the stub.
- name: Assert the bot logged in as the user the role configured - name: Assert the bot logged in as the user the role configured
ansible.builtin.assert: ansible.builtin.assert:
that: that:
@@ -99,9 +92,8 @@
success_msg: >- success_msg: >-
The bot logged in as {{ matrix_bot_matrix_reminder_bot_molecule_user_id }} and finished starting up The bot logged in as {{ matrix_bot_matrix_reminder_bot_molecule_user_id }} and finished starting up
# The role picks the storage engine (SQLite here, Postgres otherwise) by # The role picks the storage engine by building the connection string the bot parses,
# building the connection string the bot parses, and the bot names the type # and the bot names the type it settled on once the database is open.
# it settled on once the database is open.
- name: Assert the bot opened the database engine the role selected - name: Assert the bot opened the database engine the role selected
ansible.builtin.assert: ansible.builtin.assert:
that: that:
@@ -114,9 +106,8 @@
src: "{{ matrix_bot_matrix_reminder_bot_config_path }}/config.yaml" src: "{{ matrix_bot_matrix_reminder_bot_config_path }}/config.yaml"
register: matrix_bot_matrix_reminder_bot_config_file register: matrix_bot_matrix_reminder_bot_config_file
# Every one of these differs from both the role's defaults and the bot's own # Every one differs from both the role's defaults and the bot's own fallbacks, so their
# fallbacks, so their presence means the role rendered this file rather than # presence means the role rendered this file rather than coinciding with it.
# the values coinciding with what would have happened anyway.
- name: Assert the rendered configuration carries this scenario's values - name: Assert the rendered configuration carries this scenario's values
ansible.builtin.assert: ansible.builtin.assert:
that: that:
@@ -133,9 +124,8 @@
vars: vars:
matrix_bot_matrix_reminder_bot_config_rendered: "{{ matrix_bot_matrix_reminder_bot_config_file.content | b64decode }}" matrix_bot_matrix_reminder_bot_config_rendered: "{{ matrix_bot_matrix_reminder_bot_config_file.content | b64decode }}"
# `device_name` is hardcoded in the role's config template, so this value can # `device_name` is hardcoded in the role's template, so this value can only be here if
# only be there if `..._configuration_extension_yaml` was merged over the # `..._configuration_extension_yaml` was merged over it rather than ignored.
# template rather than ignored.
- name: Assert the configuration extension was merged over the template - name: Assert the configuration extension was merged over the template
ansible.builtin.assert: ansible.builtin.assert:
that: that:
@@ -148,9 +138,8 @@
vars: vars:
matrix_bot_matrix_reminder_bot_config_rendered: "{{ matrix_bot_matrix_reminder_bot_config_file.content | b64decode }}" matrix_bot_matrix_reminder_bot_config_rendered: "{{ matrix_bot_matrix_reminder_bot_config_file.content | b64decode }}"
# The bot has no HTTP surface, so where its database landed is the evidence # With no HTTP surface, where the database landed is the evidence that the storage
# that the storage configuration reached the running process rather than # configuration reached the running process and not merely the file on disk.
# merely the file on disk.
- name: Stat the database at the path the scenario configured - name: Stat the database at the path the scenario configured
ansible.builtin.stat: ansible.builtin.stat:
path: "{{ matrix_bot_matrix_reminder_bot_sqlite_database_path_local }}" path: "{{ matrix_bot_matrix_reminder_bot_sqlite_database_path_local }}"
@@ -168,9 +157,8 @@
success_msg: >- success_msg: >-
The database is at the configured path, owned by {{ matrix_user_uid }}:{{ matrix_user_gid }} The database is at the configured path, owned by {{ matrix_user_uid }}:{{ matrix_user_gid }}
# A negative control for the assertion above: the role's own default # Negative control for the assertion above: the role's own default database name must NOT
# database name must NOT appear, or a file at the configured path would not # appear, or a file at the configured path would prove nothing.
# prove the configuration reached the process.
- name: Stat the database name the role would have used by default - name: Stat the database name the role would have used by default
ansible.builtin.stat: ansible.builtin.stat:
path: "{{ matrix_bot_matrix_reminder_bot_data_path }}/bot.db" path: "{{ matrix_bot_matrix_reminder_bot_data_path }}/bot.db"
@@ -186,9 +174,8 @@
configuration reached the bot configuration reached the bot
success_msg: "Only the configured database path was used" success_msg: "Only the configured database path was used"
# matrix-nio writes its encryption store here once a login has succeeded, so # matrix-nio writes its encryption store here once login succeeds, so a populated directory
# a populated directory means the bot could use the store path the role # means the bot could use the store path the role created inside a read-only container.
# created for it inside an otherwise read-only container.
- name: List the encryption store the role created - name: List the encryption store the role created
ansible.builtin.find: ansible.builtin.find:
paths: "{{ matrix_bot_matrix_reminder_bot_data_store_path }}" paths: "{{ matrix_bot_matrix_reminder_bot_data_store_path }}"
@@ -216,9 +203,8 @@
register: matrix_bot_matrix_reminder_bot_container register: matrix_bot_matrix_reminder_bot_container
changed_when: false changed_when: false
# The timezone reaches the container twice - through the config file checked # The timezone reaches the container twice, through the config file checked above and
# above and through TZ on the unit - and the uid/gid come from the playbook # through TZ on the unit. The uid/gid come from the playbook context, not from the image.
# context rather than from anything the image would pick on its own.
- name: Assert the container runs as the role's user with the configured timezone - name: Assert the container runs as the role's user with the configured timezone
ansible.builtin.assert: ansible.builtin.assert:
that: that:
@@ -29,31 +29,29 @@ provisioner:
all: all:
matrix_bridge_heisenbridge_container_network: heisenbridge-molecule matrix_bridge_heisenbridge_container_network: heisenbridge-molecule
# The stub prepare.yml stands up. Heisenbridge talks to it while # The stub prepare.yml stands up. Not a real homeserver, and nothing is asserted
# starting; it is not a real homeserver and nothing is asserted about it. # about it.
matrix_bridge_heisenbridge_homeserver_url: http://matrix.molecule.local:8008 matrix_bridge_heisenbridge_homeserver_url: http://matrix.molecule.local:8008
matrix_bridge_heisenbridge_appservice_token: molecule_as_token_4f2a91 matrix_bridge_heisenbridge_appservice_token: molecule_as_token_4f2a91
# Heisenbridge refuses to start without an owner - it is the Matrix user # Heisenbridge refuses to start without an owner: the Matrix user allowed to
# allowed to administer the bridge. # administer the bridge.
matrix_bridge_heisenbridge_owner: "@molecule-admin:molecule.local" matrix_bridge_heisenbridge_owner: "@molecule-admin:molecule.local"
# Deliberately different from the role's defaults, so verify.yml can tell # Different from the role's defaults, so verify.yml can tell what the role rendered
# what the role rendered apart from what heisenbridge would have chosen. # apart from what heisenbridge would have chosen.
matrix_bridge_heisenbridge_path_prefix: /molecule-heisenbridge matrix_bridge_heisenbridge_path_prefix: /molecule-heisenbridge
# identd binds host port 113, which would collide with anything else on # identd binds host port 113, which would collide with anything else on the machine.
# the machine and is not what this scenario is proving.
matrix_bridge_heisenbridge_identd_enabled: false matrix_bridge_heisenbridge_identd_enabled: false
# Traefik is not deployed here, so the labels the role would render for # Traefik is not deployed here, so the labels the role would render for it are
# it are switched off and their absence is asserted instead. # switched off and their absence is asserted instead.
matrix_bridge_heisenbridge_container_labels_traefik_enabled: false matrix_bridge_heisenbridge_container_labels_traefik_enabled: false
# verify.yml runs as its own play, where role defaults are out of scope. # verify.yml runs as its own play, where role defaults are out of scope. Unlike the
# Unlike the mautrix bridges, heisenbridge keeps everything directly # mautrix bridges, heisenbridge keeps everything directly under its base path.
# under its base path rather than in config/ and data/ subdirectories.
matrix_bridge_heisenbridge_base_path: /matrix/heisenbridge matrix_bridge_heisenbridge_base_path: /matrix/heisenbridge
env: env:
# Workaround for https://github.com/ansible/molecule/issues/4391 # Workaround for https://github.com/ansible/molecule/issues/4391
@@ -31,9 +31,8 @@
docker_daemon_options: docker_daemon_options:
storage-driver: fuse-overlayfs storage-driver: fuse-overlayfs
# The role's file tasks set owner/group by name and Ansible resolves those # The role's file tasks set owner/group by name, which Ansible resolves through the
# through the passwd database, so they have to exist first. matrix-base # passwd database, so they have to exist first. `matrix-base` creates them for real.
# creates them in a real deployment.
- name: Ensure the matrix group exists - name: Ensure the matrix group exists
ansible.builtin.group: ansible.builtin.group:
name: "{{ matrix_group_name }}" name: "{{ matrix_group_name }}"
@@ -70,8 +69,8 @@
- heisenbridge_molecule_network.rc != 0 - heisenbridge_molecule_network.rc != 0
- "'already exists' not in heisenbridge_molecule_network.stderr" - "'already exists' not in heisenbridge_molecule_network.stderr"
# The bridge contacts the homeserver as it starts. It is not being asked to # The bridge contacts the homeserver as it starts. It is not being asked to bridge
# bridge anything - see molecule-shared/homeserver-stub.py. # anything. See molecule-shared/homeserver-stub.py.
- name: Ensure the homeserver stub is running - name: Ensure the homeserver stub is running
ansible.builtin.include_tasks: ansible.builtin.include_tasks:
file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/tasks/homeserver-stub.yml" file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/tasks/homeserver-stub.yml"
@@ -3,9 +3,8 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
--- ---
# What this proves: heisenbridge starts, reads the registration the role # Proves heisenbridge starts, reads the registration the role rendered, and is the version
# rendered, and is the version the role pins. It does NOT connect to IRC and # the role pins. It does NOT connect to IRC. See docs/molecule-testing.md.
# never will. See docs/molecule-testing.md.
- name: Verify heisenbridge - name: Verify heisenbridge
hosts: all hosts: all
become: true become: true
@@ -29,8 +28,8 @@
delay: 5 delay: 5
failed_when: false failed_when: false
# `Restart=always` means a bridge crash-looping on a configuration it cannot # `Restart=always` means a bridge crash-looping on unreadable config still reports
# read still reports `active`, so the restart counter is checked too. # `active`, so the restart counter is checked too.
- name: Assert the service is active and has not been restarting - name: Assert the service is active and has not been restarting
ansible.builtin.assert: ansible.builtin.assert:
that: that:
@@ -44,16 +43,14 @@
automatic restart(s) automatic restart(s)
success_msg: "matrix-heisenbridge.service is active and has not restarted" success_msg: "matrix-heisenbridge.service is active and has not restarted"
# Heisenbridge keeps everything directly under its base path rather than in # Heisenbridge keeps everything directly under its base path, unlike the mautrix bridges.
# config/ and data/ subdirectories the way the mautrix bridges do.
- name: Read the appservice registration the role rendered - name: Read the appservice registration the role rendered
ansible.builtin.slurp: ansible.builtin.slurp:
src: "{{ matrix_bridge_heisenbridge_base_path }}/registration.yaml" src: "{{ matrix_bridge_heisenbridge_base_path }}/registration.yaml"
register: heisenbridge_registration_file register: heisenbridge_registration_file
# The token and the URL both differ from anything heisenbridge would pick on # Token and URL both differ from anything heisenbridge would pick on its own, so their
# its own, so their presence means the role rendered this file rather than # presence means the role rendered this file rather than the bridge generating one.
# the bridge generating one.
- name: Assert the registration carries the scenario's token and namespace - name: Assert the registration carries the scenario's token and namespace
ansible.builtin.assert: ansible.builtin.assert:
that: that:
@@ -64,8 +61,7 @@
vars: vars:
heisenbridge_registration_rendered: "{{ heisenbridge_registration_file.content | b64decode }}" heisenbridge_registration_rendered: "{{ heisenbridge_registration_file.content | b64decode }}"
# The owner is what heisenbridge is told to accept administration from, and # The role passes the owner on the command line rather than through a config file,
# the role passes it on the command line rather than through a config file,
# so the unit is where it can be checked. # so the unit is where it can be checked.
- name: Read the systemd unit the role rendered - name: Read the systemd unit the role rendered
ansible.builtin.slurp: ansible.builtin.slurp:
@@ -82,8 +78,8 @@
vars: vars:
heisenbridge_unit_rendered: "{{ heisenbridge_unit_file.content | b64decode }}" heisenbridge_unit_rendered: "{{ heisenbridge_unit_file.content | b64decode }}"
# identd binds host port 113 when enabled, and this scenario turns it off. # identd binds host port 113 when enabled. Asserting its absence keeps the default from
# Asserting its absence keeps the default from silently becoming "on". # silently becoming "on".
- name: Assert identd is not published while it is disabled - name: Assert identd is not published while it is disabled
ansible.builtin.assert: ansible.builtin.assert:
that: that:
@@ -29,67 +29,57 @@ provisioner:
all: all:
matrix_bridge_hookshot_container_network: hookshot-molecule matrix_bridge_hookshot_container_network: hookshot-molecule
# The stub prepare.yml stands up. Hookshot contacts it while starting; # The stub prepare.yml stands up. Not a real homeserver, and nothing is asserted
# it is not a real homeserver and nothing is asserted about it. # about it.
matrix_bridge_hookshot_homeserver_address: http://matrix.molecule.local:8008 matrix_bridge_hookshot_homeserver_address: http://matrix.molecule.local:8008
# Appservice tokens. These are what Hookshot and the homeserver would # Here these only have to reach the rendered configuration and the registration.
# authenticate to each other with; here they only have to reach the
# rendered configuration and the registration file.
matrix_bridge_hookshot_appservice_token: molecule_as_token_4f2a91 matrix_bridge_hookshot_appservice_token: molecule_as_token_4f2a91
matrix_bridge_hookshot_homeserver_token: molecule_hs_token_9b3e77 matrix_bridge_hookshot_homeserver_token: molecule_hs_token_9b3e77
# Deliberately different from the role's default (`hookshot`), so the # Different from the role's default, so the registration's sender_localpart can only
# registration's sender_localpart can only have come from the role. # have come from the role. prepare.yml tells the stub to claim this same user id.
# prepare.yml tells the homeserver stub to claim this same user id.
matrix_bridge_hookshot_bot_localpart: molecule-hookshot matrix_bridge_hookshot_bot_localpart: molecule-hookshot
# Hookshot's HTTP surface is the point of this scenario. Every port # Hookshot's HTTP surface is the point of this scenario. Every port below differs
# below differs from BOTH the role's default and Hookshot's own, so an # from BOTH the role's default and Hookshot's own, so an answer can only mean the
# answer on one of them can only mean the role's configuration reached # role's configuration reached the process. verify.yml also asserts nothing answers
# the process. verify.yml also asserts that nothing answers on the # on the defaults these replace.
# defaults these replace (9993 appservice, 9000 webhooks, 9001 metrics).
matrix_bridge_hookshot_appservice_port: 9772 matrix_bridge_hookshot_appservice_port: 9772
matrix_bridge_hookshot_webhook_port: 9741 matrix_bridge_hookshot_webhook_port: 9741
# Off in the role's defaults. Enabling it makes the role render a second # Off in the role's defaults. Enabling it renders a second entry in the `listeners`
# entry in Hookshot's `listeners` list, which is the cheapest listener to # list, and /metrics is the cheapest listener to assert *content* on.
# assert *content* on: /metrics answers in a format nothing else would.
matrix_bridge_hookshot_metrics_enabled: true matrix_bridge_hookshot_metrics_enabled: true
matrix_bridge_hookshot_metrics_port: 9752 matrix_bridge_hookshot_metrics_port: 9752
# On in the role's defaults. Turned off here so that the absence of a # On in the role's defaults. Off here so the absence of a listener can be asserted:
# listener can be asserted too - the widgets port staying closed is what # the widgets port staying closed is what tells "the role rendered the listener list"
# tells "the role rendered the listener list" apart from "Hookshot binds # apart from "Hookshot binds everything anyway".
# everything anyway".
matrix_bridge_hookshot_widgets_enabled: false matrix_bridge_hookshot_widgets_enabled: false
# No third-party service is configured in this scenario - see the header # No third-party service is configured here; see the header of verify.yml.
# of verify.yml. GitLab is the one the role enables by default, so it is # GitLab is the one the role enables by default, so it is explicitly switched off.
# explicitly switched off and its absence from the rendered config and
# registration is asserted.
matrix_bridge_hookshot_gitlab_enabled: false matrix_bridge_hookshot_gitlab_enabled: false
# The generic webhooks listener is the only part of Hookshot that needs # The generic webhooks listener needs no account anywhere, so it is what this
# no account anywhere, so it is what this scenario exercises live. The # scenario exercises live. The prefix differs from the role's default.
# prefix differs from the role's default (`_webhooks_`).
matrix_bridge_hookshot_generic_userIdPrefix: _molecule_hook_ # noqa var-naming matrix_bridge_hookshot_generic_userIdPrefix: _molecule_hook_ # noqa var-naming
# Neither the role's default (600) nor Hookshot's own (600). # Neither the role's default (600) nor Hookshot's own (600).
matrix_bridge_hookshot_feeds_pollIntervalSeconds: 907 # noqa var-naming matrix_bridge_hookshot_feeds_pollIntervalSeconds: 907 # noqa var-naming
# The role defaults to `warn`; Hookshot itself defaults to `info`. This # The role defaults to `warn`, Hookshot itself to `info`. A third value, so finding
# is a third value, so finding it in config.yml cannot be a coincidence. # it in config.yml cannot be a coincidence.
matrix_bridge_hookshot_logging_level: debug matrix_bridge_hookshot_logging_level: debug
# Traefik is not deployed here, so the labels the role would render for # Traefik is not deployed here, so the labels the role would render for it are
# it are switched off and their absence is asserted instead. # switched off and their absence is asserted instead.
matrix_bridge_hookshot_container_labels_traefik_enabled: false matrix_bridge_hookshot_container_labels_traefik_enabled: false
# verify.yml runs as its own play, where role defaults are out of scope, # verify.yml runs as its own play, where role defaults are out of scope, so what it
# so what it reads is pinned here. These two match the role's own # reads is pinned here. These two match the role's own defaults on purpose: they name
# defaults on purpose - they name things (a path, a container) rather # things rather than configure them, and nothing is asserted *about* them.
# than configure them, and nothing is asserted *about* them.
matrix_bridge_hookshot_base_path: /matrix/hookshot matrix_bridge_hookshot_base_path: /matrix/hookshot
matrix_bridge_hookshot_identifier: matrix-hookshot matrix_bridge_hookshot_identifier: matrix-hookshot
env: env:
@@ -31,9 +31,8 @@
docker_daemon_options: docker_daemon_options:
storage-driver: fuse-overlayfs storage-driver: fuse-overlayfs
# The role's file tasks set owner/group by name and Ansible resolves those # The role's file tasks set owner/group by name, which Ansible resolves through the
# through the passwd database, so they have to exist first. matrix-base # passwd database, so they have to exist first. `matrix-base` creates them for real.
# creates them in a real deployment.
- name: Ensure the matrix group exists - name: Ensure the matrix group exists
ansible.builtin.group: ansible.builtin.group:
name: "{{ matrix_group_name }}" name: "{{ matrix_group_name }}"
@@ -3,15 +3,14 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
--- ---
# What this proves: Hookshot starts, accepts the config.yml and registration.yml # Proves Hookshot starts on the config.yml and registration.yml the role rendered,
# the role rendered, and opens exactly the HTTP listeners that configuration # and opens exactly the HTTP listeners that configuration described - on the ports
# described - on the ports the role put there, and not on the ones it did not. # the role put there, and not on the ones it did not.
# #
# What it deliberately does NOT do: configure GitHub, GitLab, Jira or Figma. # Deliberately does NOT configure GitHub, GitLab, Jira or Figma. Each needs an account
# Every one of those needs an account and a credential on a third-party service, # and a credential on a third-party service, which is where a scenario stops testing this
# which is the line where a scenario stops testing this repository and starts # repository and starts testing a fake (see docs/molecule-testing.md). The generic webhooks
# testing a fake (see docs/molecule-testing.md). The generic webhooks listener # listener needs no credential from anyone, so it is the one exercised live.
# needs no credential from anyone, so it is the one that gets exercised live.
- name: Verify hookshot - name: Verify hookshot
hosts: all hosts: all
become: true become: true
@@ -21,11 +20,10 @@
gather_facts: false gather_facts: false
tasks: tasks:
# Read from the role's own defaults rather than pinned in molecule.yml, so # From the role's own defaults rather than pinned in molecule.yml, so the version
# the version assertion below compares the running image against what the # assertion compares the running image against what the role ships, not against the
# role ships instead of against the scenario itself. The default ports come # scenario. The default ports come from here for the same reason: "these ports stay
# from here for the same reason: the "these ports stay closed" assertion is # closed" is only meaningful against the ports the role would otherwise have used.
# only meaningful against the ports the role would otherwise have used.
- name: Load the role's defaults under a separate name - name: Load the role's defaults under a separate name
ansible.builtin.include_vars: ansible.builtin.include_vars:
file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults/main.yml" file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults/main.yml"
@@ -40,10 +38,9 @@
delay: 5 delay: 5
failed_when: false failed_when: false
# `Restart=always` means a bridge crash-looping on a configuration it cannot # `Restart=always` means a bridge crash-looping on unreadable config still reports
# read still reports `active`, so the restart counter is checked too. It is # `active`, so the restart counter is checked too. Asserted `is defined` because
# asserted `is defined` because `| int` turns a missing property into 0 and # `| int` turns a missing property into 0 and would pass vacuously.
# would pass vacuously.
- name: Assert the service is active and has not been restarting - name: Assert the service is active and has not been restarting
ansible.builtin.assert: ansible.builtin.assert:
that: that:
@@ -70,9 +67,8 @@
ansible.builtin.set_fact: ansible.builtin.set_fact:
hookshot_config: "{{ hookshot_config_file.content | b64decode | from_yaml }}" hookshot_config: "{{ hookshot_config_file.content | b64decode | from_yaml }}"
# Each of these differs from what Hookshot would use on its own AND from # Each differs from what Hookshot would use on its own AND from the role's defaults,
# what the role defaults to, so finding them here means the role's # so finding them here rules out a coincidence.
# configuration is what Hookshot is running on rather than a coincidence.
- name: Assert the rendered configuration carries this scenario's values - name: Assert the rendered configuration carries this scenario's values
ansible.builtin.assert: ansible.builtin.assert:
that: that:
@@ -86,10 +82,9 @@
fail_msg: "The rendered configuration does not carry the scenario's values" fail_msg: "The rendered configuration does not carry the scenario's values"
success_msg: "The rendered configuration carries the scenario's values" success_msg: "The rendered configuration carries the scenario's values"
# Hookshot's `listeners` list is the role's own construction: it decides # The `listeners` list is the role's own construction, assembled from a handful of
# which resources get a port at all, from a handful of independent switches. # independent switches. Getting it wrong is invisible in a "did it start" test,
# Getting this wrong is invisible in a "did it start" test, which is why it # hence asserting the whole shape rather than key by key.
# is asserted as a whole rather than key by key.
- name: Assert the role rendered exactly the listeners the scenario asked for - name: Assert the role rendered exactly the listeners the scenario asked for
ansible.builtin.assert: ansible.builtin.assert:
that: that:
@@ -103,9 +98,8 @@
vars: vars:
hookshot_listener_ports: "{{ hookshot_config.listeners | map(attribute='port') | map('int') | list }}" hookshot_listener_ports: "{{ hookshot_config.listeners | map(attribute='port') | map('int') | list }}"
# No third-party service is configured here, so none of their sections may # GitLab is the interesting one: the role turns it ON by default, so its absence
# appear. GitLab is the interesting one: the role turns it ON by default, so # proves the scenario's switch reached the template.
# its absence is what proves the scenario's switch reached the template.
- name: Assert no third-party service section was rendered - name: Assert no third-party service section was rendered
ansible.builtin.assert: ansible.builtin.assert:
that: that:
@@ -131,9 +125,8 @@
# The rendered registration # The rendered registration
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# The registration file is the half of the appservice handshake the # The role generates the registration; Hookshot only consumes it. Worth checking
# homeserver reads. The role generates it, Hookshot only consumes it, so it # on its own, as it is the half of the handshake the homeserver reads.
# is worth checking on its own.
- name: Read the appservice registration the role rendered - name: Read the appservice registration the role rendered
ansible.builtin.slurp: ansible.builtin.slurp:
src: "{{ matrix_bridge_hookshot_base_path }}/registration.yml" src: "{{ matrix_bridge_hookshot_base_path }}/registration.yml"
@@ -143,9 +136,8 @@
ansible.builtin.set_fact: ansible.builtin.set_fact:
hookshot_registration: "{{ hookshot_registration_file.content | b64decode | from_yaml }}" hookshot_registration: "{{ hookshot_registration_file.content | b64decode | from_yaml }}"
# `url` is where the homeserver would push transactions, and the role builds # `url` is built from the container name and the appservice port. It has to agree
# it out of the container name and the appservice port. It has to agree with # with `bridge.port` in config.yml, or the two halves silently disagree.
# `bridge.port` in config.yml or the two halves would silently disagree.
- name: Assert the registration carries the scenario's tokens, bot and callback URL - name: Assert the registration carries the scenario's tokens, bot and callback URL
ansible.builtin.assert: ansible.builtin.assert:
that: that:
@@ -156,9 +148,9 @@
fail_msg: "The appservice registration does not carry the scenario's tokens, bot and callback URL" fail_msg: "The appservice registration does not carry the scenario's tokens, bot and callback URL"
success_msg: "The appservice registration carries the scenario's tokens, bot and callback URL" success_msg: "The appservice registration carries the scenario's tokens, bot and callback URL"
# The user namespace is derived from the generic webhook prefix, and the # The user namespace derives from the generic webhook prefix, and the GitLab namespace
# GitLab namespace is conditional on the service being enabled - so this # is conditional on that service being enabled. Checks both switches reach the
# checks that the two switches reach the registration, not just config.yml. # registration, not just config.yml.
- name: Assert the registration namespaces follow the enabled services - name: Assert the registration namespaces follow the enabled services
ansible.builtin.assert: ansible.builtin.assert:
that: that:
@@ -176,8 +168,8 @@
# The listeners, live # The listeners, live
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# A helper container is used because the role publishes no host port, exactly # A helper container, because the role publishes no host port - exactly as in a real
# as in a real deployment; see docs/molecule-testing.md. # deployment. See docs/molecule-testing.md.
- name: Wait for the webhooks listener to answer on the port the role configured - name: Wait for the webhooks listener to answer on the port the role configured
ansible.builtin.command: ansible.builtin.command:
argv: argv:
@@ -205,11 +197,10 @@
delay: 5 delay: 5
failed_when: false failed_when: false
# Hookshot answers an unknown webhook id from its generic-webhook handler, # An unknown webhook id draws a JSON body from the generic-webhook handler that no
# with a JSON body no other component would produce. An Express "Cannot POST" # other component would produce. An Express "Cannot POST" page would mean the port is
# page here would mean the port is Hookshot's but the generic webhooks # Hookshot's but the generic webhooks service was never mounted on it; a refused
# service was never mounted on it; a refused connection would mean the # connection would mean the listener was never opened at all.
# listener the role described was never opened at all.
- name: Assert the generic webhooks service is mounted on that listener - name: Assert the generic webhooks service is mounted on that listener
ansible.builtin.assert: ansible.builtin.assert:
that: that:
@@ -222,9 +213,8 @@
({{ hookshot_webhooks_probe.stdout | default('no output') }}) ({{ hookshot_webhooks_probe.stdout | default('no output') }})
success_msg: "The generic webhooks service answers on the port the role configured" success_msg: "The generic webhooks service answers on the port the role configured"
# Metrics are OFF in the role's defaults, so this listener exists only # Metrics are OFF in the role's defaults, so this listener exists only because the
# because the scenario asked for it - and /metrics answers in a format # scenario asked for it.
# nothing else on that port could have produced.
- name: Probe the metrics listener on the port the role configured - name: Probe the metrics listener on the port the role configured
ansible.builtin.command: ansible.builtin.command:
argv: argv:
@@ -253,8 +243,8 @@
metrics ({{ hookshot_metrics_probe.stdout | default('no output') | truncate(200) }}) metrics ({{ hookshot_metrics_probe.stdout | default('no output') | truncate(200) }})
success_msg: "The metrics listener serves Hookshot's own metrics" success_msg: "The metrics listener serves Hookshot's own metrics"
# The appservice port is not in `listeners` - it comes from `bridge.port` - # The appservice port is not in `listeners`; it comes from `bridge.port`.
# so it is a separate socket, opened by a separate part of the config. # A separate socket, opened by a separate part of the config.
- name: Probe the appservice port the role configured - name: Probe the appservice port the role configured
ansible.builtin.command: ansible.builtin.command:
argv: argv:
@@ -284,10 +274,10 @@
({{ hookshot_appservice_probe.stdout | default('no output') }}) ({{ hookshot_appservice_probe.stdout | default('no output') }})
success_msg: "The appservice API answers on the port the role configured" success_msg: "The appservice API answers on the port the role configured"
# The other half of the story. Every port above is one the scenario chose; # The other half of the story. Every port above is one the scenario chose; these are
# these are the ones the role and Hookshot would have used if the scenario's # the ones the role and Hookshot would have used had the scenario's configuration never
# configuration had never reached the process. If any of them answers, then # reached the process. If any of them answers, the probes above prove much less than
# a passing probe above proves much less than it looks like it does. # they appear to.
- name: Probe the ports the role's defaults would have used - name: Probe the ports the role's defaults would have used
ansible.builtin.command: ansible.builtin.command:
argv: argv:
@@ -29,51 +29,41 @@ provisioner:
all: all:
matrix_bridge_mautrix_discord_container_network: mautrix-discord-molecule matrix_bridge_mautrix_discord_container_network: mautrix-discord-molecule
# The homeserver stub prepare.yml stands up. The bridge contacts it # The stub prepare.yml stands up. Not a real homeserver, and nothing is asserted
# while starting; it is not a real homeserver and nothing is asserted # about it. There is deliberately no Discord on the other side either.
# about it. There is deliberately no Discord on the other side either -
# see docs/molecule-testing.md.
matrix_bridge_mautrix_discord_homeserver_address: http://matrix.molecule.local:8008 matrix_bridge_mautrix_discord_homeserver_address: http://matrix.molecule.local:8008
# sqlite keeps the scenario to one container. The role only requires a # sqlite keeps the scenario to one container. Which database engine the bridge can
# database hostname when the engine is postgres, and testing which # talk to is not what this proves.
# database engine the bridge can talk to is not what this proves.
matrix_bridge_mautrix_discord_database_engine: sqlite matrix_bridge_mautrix_discord_database_engine: sqlite
# Appservice tokens. These are what the bridge and homeserver would # Here these only have to reach the rendered configuration and the registration.
# authenticate to each other with; here they only have to reach the
# rendered configuration and the registration file.
matrix_bridge_mautrix_discord_appservice_token: molecule_as_token_d15c07 matrix_bridge_mautrix_discord_appservice_token: molecule_as_token_d15c07
matrix_bridge_mautrix_discord_homeserver_token: molecule_hs_token_a4e2b8 matrix_bridge_mautrix_discord_homeserver_token: molecule_hs_token_a4e2b8
# Deliberately different from the role's defaults, so verify.yml can # Different from the role's defaults, so verify.yml can tell what the role rendered
# tell what the role rendered apart from what the bridge would have # apart from what the bridge would have chosen.
# defaulted to on its own.
matrix_bridge_mautrix_discord_appservice_bot_username: molecule-discordbot matrix_bridge_mautrix_discord_appservice_bot_username: molecule-discordbot
matrix_bridge_mautrix_discord_homeserver_domain: molecule.local matrix_bridge_mautrix_discord_homeserver_domain: molecule.local
matrix_bridge_mautrix_discord_bridge_command_prefix: "!molecule-discord" matrix_bridge_mautrix_discord_bridge_command_prefix: "!molecule-discord"
# The role defaults to `warn`; the bridge's own shipped configuration # The role defaults to `warn`, the bridge's own shipped configuration to `debug`.
# uses `debug`. `info` is neither. # `info` is neither.
matrix_bridge_mautrix_discord_logging_level: info matrix_bridge_mautrix_discord_logging_level: info
# Unlike most bridge roles here, mautrix-discord *requires* a public # Unlike most bridge roles here, mautrix-discord *requires* a public address:
# address: `validate_config.yml` fails without # `validate_config.yml` fails without one, and it is derived from these three.
# `matrix_bridge_mautrix_discord_bridge_public_address`, which is # Discord fetches avatars over it in relay mode. Nothing reaches it in this scenario,
# derived from these three. Discord fetches avatars over it in relay # but it has to be set for the role to run at all.
# mode; nothing reaches it in this scenario, but it has to be set for
# the role to run at all.
# #
# A non-`/` path prefix and a non-default scheme are chosen so the # A non-`/` path prefix and a non-default scheme are chosen so the avatar-proxy labels
# avatar-proxy labels verify.yml reads can only look the way they do if # verify.yml reads can only look the way they do if the role composed them.
# the role composed them from these values.
matrix_bridge_mautrix_discord_hostname: discord.molecule.local matrix_bridge_mautrix_discord_hostname: discord.molecule.local
matrix_bridge_mautrix_discord_path_prefix: /discord-bridge matrix_bridge_mautrix_discord_path_prefix: /discord-bridge
matrix_bridge_mautrix_discord_scheme: http matrix_bridge_mautrix_discord_scheme: http
matrix_bridge_mautrix_discord_bridge_avatar_proxy_key: molecule_avatar_proxy_key_7c1d matrix_bridge_mautrix_discord_bridge_avatar_proxy_key: molecule_avatar_proxy_key_7c1d
# verify.yml runs as its own play, where role defaults are out of scope, # verify.yml runs as its own play, where the role's defaults are out of scope,
# so the paths it reads are pinned here as literals matching what the # so the paths it reads are pinned here to match what the role derives.
# role derives from matrix_base_data_path.
matrix_bridge_mautrix_discord_base_path: /matrix/mautrix-discord matrix_bridge_mautrix_discord_base_path: /matrix/mautrix-discord
matrix_bridge_mautrix_discord_config_path: /matrix/mautrix-discord/config matrix_bridge_mautrix_discord_config_path: /matrix/mautrix-discord/config
matrix_bridge_mautrix_discord_data_path: /matrix/mautrix-discord/data matrix_bridge_mautrix_discord_data_path: /matrix/mautrix-discord/data
@@ -31,9 +31,8 @@
docker_daemon_options: docker_daemon_options:
storage-driver: fuse-overlayfs storage-driver: fuse-overlayfs
# The role's file tasks set owner/group by name and Ansible resolves those # The role's file tasks set owner/group by name, which Ansible resolves through the
# through the passwd database, so they have to exist first. matrix-base # passwd database, so they have to exist first. `matrix-base` creates them for real.
# creates them in a real deployment.
- name: Ensure the matrix group exists - name: Ensure the matrix group exists
ansible.builtin.group: ansible.builtin.group:
name: "{{ matrix_group_name }}" name: "{{ matrix_group_name }}"
@@ -70,9 +69,8 @@
- mautrix_discord_molecule_network.rc != 0 - mautrix_discord_molecule_network.rc != 0
- "'already exists' not in mautrix_discord_molecule_network.stderr" - "'already exists' not in mautrix_discord_molecule_network.stderr"
# The bridge contacts the homeserver as it starts, and refuses to run if # The bridge contacts the homeserver as it starts and refuses to run if /whoami does not
# /whoami does not name the bot user it was configured as - see # name the bot user it was configured as. See molecule-shared/homeserver-stub.py.
# molecule-shared/homeserver-stub.py.
- name: Ensure the homeserver stub is running - name: Ensure the homeserver stub is running
ansible.builtin.include_tasks: ansible.builtin.include_tasks:
file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/tasks/homeserver-stub.yml" file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/tasks/homeserver-stub.yml"
@@ -3,13 +3,12 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
--- ---
# What this proves: the bridge starts, reads the configuration and registration # Proves the bridge starts, reads the configuration and registration the role rendered, opens
# the role rendered, opens its appservice port, and is the version the role # its appservice port, is the version the role pins, and composed the avatar-proxy labels out
# pins. It also proves the role composed the avatar-proxy labels out of the # of the hostname, scheme and path prefix it was given.
# hostname, scheme and path prefix it was given. It does NOT bridge anything - #
# there is no Discord on the other side and there is deliberately never going to # It does NOT bridge anything: there is no Discord on the other side, and deliberately never
# be one, because that would need a Discord account. See # will be, because that would need a Discord account. See docs/molecule-testing.md.
# docs/molecule-testing.md.
- name: Verify mautrix-discord - name: Verify mautrix-discord
hosts: all hosts: all
become: true become: true
@@ -19,17 +18,15 @@
gather_facts: false gather_facts: false
vars: vars:
# The role derives this from scheme + hostname + path prefix, and role # The role derives this from scheme + hostname + path prefix. Role defaults are out of
# defaults are out of scope in this play, so it is recomposed here from the # scope in this play, so it is recomposed from the same three values molecule.yml pinned.
# same three values the scenario pinned in molecule.yml.
mautrix_discord_expected_public_address: >- mautrix_discord_expected_public_address: >-
{{ matrix_bridge_mautrix_discord_scheme }}://{{ matrix_bridge_mautrix_discord_hostname }}{{ matrix_bridge_mautrix_discord_path_prefix }} {{ matrix_bridge_mautrix_discord_scheme }}://{{ matrix_bridge_mautrix_discord_hostname }}{{ matrix_bridge_mautrix_discord_path_prefix }}
mautrix_discord_expected_avatar_proxy_path_prefix: "{{ matrix_bridge_mautrix_discord_path_prefix }}/mautrix-discord/avatar" mautrix_discord_expected_avatar_proxy_path_prefix: "{{ matrix_bridge_mautrix_discord_path_prefix }}/mautrix-discord/avatar"
tasks: tasks:
# Read from the role's own defaults rather than pinned in molecule.yml, so # From the role's own defaults rather than pinned in molecule.yml, so the version
# the version assertion below compares the running image against what the # assertion compares the running image against what the role ships, not the scenario.
# role ships instead of against the scenario itself.
- name: Load the role's defaults under a separate name - name: Load the role's defaults under a separate name
ansible.builtin.include_vars: ansible.builtin.include_vars:
file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults/main.yml" file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults/main.yml"
@@ -44,10 +41,9 @@
delay: 5 delay: 5
failed_when: false failed_when: false
# `Restart=always` means a bridge crash-looping on a configuration it cannot # `Restart=always` means a bridge crash-looping on unreadable config still reports
# read still reports `active`, so the restart counter is checked too. It is # `active`, so the restart counter is checked too. Asserted `is defined` because
# asserted `is defined` because `| int` turns a missing property into 0 and # `| int` turns a missing property into 0 and would pass vacuously.
# would pass vacuously.
- name: Assert the service is active and has not been restarting - name: Assert the service is active and has not been restarting
ansible.builtin.assert: ansible.builtin.assert:
that: that:
@@ -61,9 +57,8 @@
automatic restart(s) automatic restart(s)
success_msg: "matrix-mautrix-discord.service is active and has not restarted" success_msg: "matrix-mautrix-discord.service is active and has not restarted"
# The appservice port is the bridge's own listener, the one a homeserver # The appservice listener is where a homeserver would push transactions. It opening at all
# would push transactions to. It opening at all means the bridge got through # means the bridge got through reading its configuration and setting itself up.
# reading its configuration and setting itself up.
- name: Wait for the bridge to open its appservice port - name: Wait for the bridge to open its appservice port
ansible.builtin.command: ansible.builtin.command:
argv: argv:
@@ -99,13 +94,12 @@
src: "{{ matrix_bridge_mautrix_discord_config_path }}/config.yaml" src: "{{ matrix_bridge_mautrix_discord_config_path }}/config.yaml"
register: mautrix_discord_config_file register: mautrix_discord_config_file
# Each of these differs from what the bridge would use on its own, so their # Each differs from what the bridge would use on its own, so their presence rules out a
# presence means the role's configuration is what the bridge is running on # coincidence. The public address in particular is composed by the role out of three
# rather than something that happened to agree with it. The public address # separate variables.
# in particular is composed by the role out of three separate variables.
# #
# Asserted against the parsed document rather than by substring, so a value # Asserted against the parsed document rather than by substring, so a value landing under
# landing under the wrong key cannot pass. # the wrong key cannot pass.
- name: Assert the rendered configuration carries this scenario's values - name: Assert the rendered configuration carries this scenario's values
ansible.builtin.assert: ansible.builtin.assert:
that: that:
@@ -124,10 +118,8 @@
vars: vars:
mautrix_discord_config: "{{ mautrix_discord_config_file.content | b64decode | from_yaml }}" mautrix_discord_config: "{{ mautrix_discord_config_file.content | b64decode | from_yaml }}"
# The registration file is the half of the appservice handshake the # The role generates the registration; the bridge only consumes it. `sender_localpart`
# homeserver reads, and it is generated by the role rather than by the # carries the role's own `_bot_` prefixing, not something the bridge would produce.
# bridge, so it is worth checking on its own. `sender_localpart` is the
# role's own `_bot_` prefixing, not something the bridge would produce.
- name: Read the appservice registration the role rendered - name: Read the appservice registration the role rendered
ansible.builtin.slurp: ansible.builtin.slurp:
src: "{{ matrix_bridge_mautrix_discord_config_path }}/registration.yaml" src: "{{ matrix_bridge_mautrix_discord_config_path }}/registration.yaml"
@@ -147,9 +139,8 @@
mautrix_discord_registration: "{{ mautrix_discord_registration_file.content | b64decode | from_yaml }}" mautrix_discord_registration: "{{ mautrix_discord_registration_file.content | b64decode | from_yaml }}"
mautrix_discord_bot_user_regex: "^@{{ matrix_bridge_mautrix_discord_appservice_bot_username | regex_escape }}:{{ matrix_bridge_mautrix_discord_homeserver_domain | regex_escape }}$" mautrix_discord_bot_user_regex: "^@{{ matrix_bridge_mautrix_discord_appservice_bot_username | regex_escape }}:{{ matrix_bridge_mautrix_discord_homeserver_domain | regex_escape }}$"
# sqlite was chosen in molecule.yml, so the bridge should have created its # Cheap proof that the data path reached the process and is writable by the uid the role
# database under the role's data path. This is the cheap proof that the data # runs it as.
# path reached the process and is writable by the uid the role runs it as.
- name: Look for the bridge's sqlite database under the role's data path - name: Look for the bridge's sqlite database under the role's data path
ansible.builtin.stat: ansible.builtin.stat:
path: "{{ matrix_bridge_mautrix_discord_data_path }}/mautrix-discord.db" path: "{{ matrix_bridge_mautrix_discord_data_path }}/mautrix-discord.db"
@@ -188,9 +179,9 @@
{{ mautrix_discord_role_defaults.matrix_bridge_mautrix_discord_version }} {{ mautrix_discord_role_defaults.matrix_bridge_mautrix_discord_version }}
success_msg: "The running container is the version defaults/main.yml pins" success_msg: "The running container is the version defaults/main.yml pins"
# The avatar proxy is this role's own reverse-proxy wiring: the labels only # The avatar proxy is this role's own reverse-proxy wiring. The labels only appear because
# appear because a public address was configured, and their hostname and # a public address was configured, and the role composes their hostname and path prefix
# path prefix are composed by the role rather than copied from a variable. # rather than copying them from a variable.
- name: Read the labels the role rendered - name: Read the labels the role rendered
ansible.builtin.slurp: ansible.builtin.slurp:
src: "{{ matrix_bridge_mautrix_discord_base_path }}/labels" src: "{{ matrix_bridge_mautrix_discord_base_path }}/labels"
@@ -211,10 +202,9 @@
vars: vars:
mautrix_discord_labels_rendered: "{{ mautrix_discord_labels.content | b64decode }}" mautrix_discord_labels_rendered: "{{ mautrix_discord_labels.content | b64decode }}"
# The label file is fed to `docker create --label-file`, so a label the role # The label file is fed to `docker create --label-file`, so a wrongly rendered label is
# renders wrongly is not merely cosmetic - it would stop the container from # not cosmetic: it stops the container being created at all. Reading them back off the
# being created at all. Reading them back off the running container proves # running container proves Docker accepted them.
# Docker accepted them.
- name: Read the labels Docker attached to the running container - name: Read the labels Docker attached to the running container
ansible.builtin.command: ansible.builtin.command:
argv: argv:
@@ -29,66 +29,51 @@ provisioner:
all: all:
matrix_bridge_mautrix_meta_messenger_container_network: mautrix-meta-messenger-molecule matrix_bridge_mautrix_meta_messenger_container_network: mautrix-meta-messenger-molecule
# The homeserver stub prepare.yml stands up. The bridge contacts it # The stub prepare.yml stands up. Not a real homeserver, and nothing is asserted
# while starting; it is not a real homeserver and nothing is asserted
# about it. # about it.
matrix_bridge_mautrix_meta_messenger_homeserver_address: http://matrix.molecule.local:8008 matrix_bridge_mautrix_meta_messenger_homeserver_address: http://matrix.molecule.local:8008
matrix_bridge_mautrix_meta_messenger_homeserver_domain: molecule.local matrix_bridge_mautrix_meta_messenger_homeserver_domain: molecule.local
# sqlite keeps the scenario to one container. The role only requires a # sqlite keeps the scenario to one container. Which database engine the bridge can
# database hostname when the engine is postgres, and testing which # talk to is not what this proves.
# database engine the bridge can talk to is not what this proves.
matrix_bridge_mautrix_meta_messenger_database_engine: sqlite3-fk-wal matrix_bridge_mautrix_meta_messenger_database_engine: sqlite3-fk-wal
# The URI the role derives for `sqlite3-fk-wal` is deliberately NOT # The URI the role derives is deliberately NOT overridden here. It used to build
# overridden here. It used to build `sqlite:///` + the in-container path, # `sqlite:///` + the in-container path, which go-sqlite3 takes as a plain filename
# which go-sqlite3 takes as a plain filename rather than parsing as a URL, # rather than parsing as a URL, so the bridge died at startup. This scenario caught
# so the bridge died at startup with `unable to open database file`. This # that, and leaving the role's own value in place is what keeps it caught.
# scenario is what caught it. Leaving the role's own value in place is
# what keeps it caught.
# Appservice tokens. These are what the bridge and homeserver would # Here these only have to reach the rendered configuration and the registration.
# authenticate to each other with; here they only have to reach the
# rendered configuration and the registration file.
matrix_bridge_mautrix_meta_messenger_appservice_token: molecule_meta_as_token_5c81de matrix_bridge_mautrix_meta_messenger_appservice_token: molecule_meta_as_token_5c81de
matrix_bridge_mautrix_meta_messenger_homeserver_token: molecule_meta_hs_token_a70f24 matrix_bridge_mautrix_meta_messenger_homeserver_token: molecule_meta_hs_token_a70f24
# The one variable that makes this role family unusual: a single # What makes this role family unusual: one upstream codebase serves several Meta
# upstream codebase serves several Meta networks, and this variable is # networks, and this variable picks which. It reaches the rendered configuration in
# what picks which one. It reaches the rendered configuration in several # four places at once - appservice id, ghost username prefix, bot displayname and the
# places at once - the appservice id, the ghost username prefix, the bot # bridge's `tor` switch - so a value other than the role's default is what tells
# displayname and the bridge's `tor` switch - so a value other than the # verify.yml the role propagated the choice rather than everything agreeing by accident.
# role's default `messenger` is what tells verify.yml that the role
# propagated the choice rather than everything merely agreeing by
# accident. `facebook-tor` is the only one of the three modes that also
# flips a boolean in the configuration, which is why it is the one used.
# #
# Nothing logs in during the scenario, so the bridge never opens a # `facebook-tor` is used because it is the only mode that also flips a boolean.
# connection to Meta (over Tor or otherwise). See docs/molecule-testing.md # Nothing logs in during the scenario, so the bridge never connects to Meta.
# for why a scenario stops short of that.
matrix_bridge_mautrix_meta_messenger_meta_mode: facebook-tor matrix_bridge_mautrix_meta_messenger_meta_mode: facebook-tor
# Deliberately different from the role's defaults (`messengerbot`, # Different from the role's defaults and from what the bridge would pick on its own,
# `!fb`, `(FB)`, `warn`) and from what the bridge would pick on its own, # so verify.yml can tell what the role rendered apart from a coincidence.
# so verify.yml can tell what the role rendered apart from a
# coincidence.
matrix_bridge_mautrix_meta_messenger_appservice_username: molecule-metabot matrix_bridge_mautrix_meta_messenger_appservice_username: molecule-metabot
matrix_bridge_mautrix_meta_messenger_bridge_command_prefix: "!molecule-meta" matrix_bridge_mautrix_meta_messenger_bridge_command_prefix: "!molecule-meta"
matrix_bridge_mautrix_meta_messenger_bridge_displayname_suffix: "(Molecule)" matrix_bridge_mautrix_meta_messenger_bridge_displayname_suffix: "(Molecule)"
matrix_bridge_mautrix_meta_messenger_logging_min_level: debug matrix_bridge_mautrix_meta_messenger_logging_min_level: debug
# The bridge's HTTP API exposure. Traefik is not deployed here, so # Traefik is not deployed here, so nothing routes to it. What is tested is that the
# nothing routes to it; what is being tested is that the role turns # role turns these three variables into both the container's Traefik labels and the
# these three variables into both the container's Traefik labels and the
# `appservice.public_address` the bridge itself reads. # `appservice.public_address` the bridge itself reads.
matrix_bridge_mautrix_meta_messenger_exposure_enabled: true matrix_bridge_mautrix_meta_messenger_exposure_enabled: true
matrix_bridge_mautrix_meta_messenger_exposure_hostname: bridges.molecule.local matrix_bridge_mautrix_meta_messenger_exposure_hostname: bridges.molecule.local
matrix_bridge_mautrix_meta_messenger_exposure_path_prefix: /bridges/meta-messenger matrix_bridge_mautrix_meta_messenger_exposure_path_prefix: /bridges/meta-messenger
matrix_bridge_mautrix_meta_messenger_scheme: https matrix_bridge_mautrix_meta_messenger_scheme: https
# verify.yml runs as its own play, where role defaults are out of scope, # verify.yml runs as its own play, where the role's defaults are out of scope,
# so the paths it reads are pinned here as literals matching what the # so the paths it reads are pinned here to match what the role derives.
# role derives from matrix_base_data_path.
matrix_bridge_mautrix_meta_messenger_base_path: /matrix/mautrix-meta-messenger matrix_bridge_mautrix_meta_messenger_base_path: /matrix/mautrix-meta-messenger
matrix_bridge_mautrix_meta_messenger_config_path: /matrix/mautrix-meta-messenger/config matrix_bridge_mautrix_meta_messenger_config_path: /matrix/mautrix-meta-messenger/config
matrix_bridge_mautrix_meta_messenger_data_path: /matrix/mautrix-meta-messenger/data matrix_bridge_mautrix_meta_messenger_data_path: /matrix/mautrix-meta-messenger/data
@@ -31,9 +31,8 @@
docker_daemon_options: docker_daemon_options:
storage-driver: fuse-overlayfs storage-driver: fuse-overlayfs
# The role's file tasks set owner/group by name and Ansible resolves those # The role's file tasks set owner/group by name, which Ansible resolves through the
# through the passwd database, so they have to exist first. matrix-base # passwd database, so they have to exist first. `matrix-base` creates them for real.
# creates them in a real deployment.
- name: Ensure the matrix group exists - name: Ensure the matrix group exists
ansible.builtin.group: ansible.builtin.group:
name: "{{ matrix_group_name }}" name: "{{ matrix_group_name }}"
@@ -72,11 +71,9 @@
- mautrix_meta_messenger_molecule_network.rc != 0 - mautrix_meta_messenger_molecule_network.rc != 0
- "'already exists' not in mautrix_meta_messenger_molecule_network.stderr" - "'already exists' not in mautrix_meta_messenger_molecule_network.stderr"
# The bridge contacts the homeserver as it starts, and calls /whoami before # The bridge calls /whoami before it will run at all, exiting if the id it gets back is
# it will run at all - it exits if the id it gets back is not the bot user # not the bot user it was configured as. It is not being asked to bridge anything: there
# it was configured as. It is not being asked to bridge anything; there is # is no Meta account in this scenario, and deliberately never will be.
# no Meta account anywhere in this scenario and there is deliberately never
# going to be one. See molecule-shared/homeserver-stub.py.
- name: Ensure the homeserver stub is running - name: Ensure the homeserver stub is running
ansible.builtin.include_tasks: ansible.builtin.include_tasks:
file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/tasks/homeserver-stub.yml" file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/tasks/homeserver-stub.yml"
@@ -3,29 +3,24 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
--- ---
# What this proves: the bridge starts, reads the configuration and registration # Proves the bridge starts, reads the configuration and registration the role rendered, opens
# the role rendered, opens its appservice port, and is the Messenger-side image # its appservice port, and is the Messenger-side image at the version the role pins.
# at the version the role pins.
# #
# The extra thing worth proving for this role in particular is that # The extra thing worth proving here is that `..._meta_mode` reaches every place it feeds: one
# `matrix_bridge_mautrix_meta_messenger_meta_mode` reaches every place it feeds: # upstream codebase serves several Meta networks, and the mode picks which. The scenario runs
# one upstream codebase serves several Meta networks, and the mode is what picks # `facebook-tor` rather than the role's default, so the assertions can tell the two apart.
# which one. The scenario runs the bridge in `facebook-tor` mode rather than the
# role's default `messenger`, so the assertions below can tell the two apart.
# #
# It does NOT bridge anything - there is no Facebook or Messenger account on the # It does NOT bridge anything: no Facebook or Messenger account is on the other side, and
# other side and there is deliberately never going to be one. See # deliberately never will be. See docs/molecule-testing.md.
# docs/molecule-testing.md.
- name: Verify mautrix-meta-messenger - name: Verify mautrix-meta-messenger
hosts: all hosts: all
become: true become: true
vars_files: vars_files:
- "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/vars.yml" - "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/vars.yml"
- "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/playbook-context.yml" - "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/playbook-context.yml"
# Lazily evaluated, so they are only resolved by the tasks that come after the # Lazily evaluated, so they resolve only in the tasks after the matching slurp.
# matching slurp. Deliberately not set_fact: the rendered configuration # Deliberately not set_fact: the rendered configuration contains the bridge's own Go
# contains the bridge's own Go templates (`{{.}}` in `username_template`), and # templates, and a stored fact gets templated again on every lookup, which would try to
# a stored fact gets templated again on every lookup, which would try to
# evaluate those as Jinja. # evaluate those as Jinja.
vars: vars:
mautrix_meta_messenger_config: "{{ mautrix_meta_messenger_config_file.content | b64decode | from_yaml }}" mautrix_meta_messenger_config: "{{ mautrix_meta_messenger_config_file.content | b64decode | from_yaml }}"
@@ -34,9 +29,8 @@
gather_facts: false gather_facts: false
tasks: tasks:
# Read from the role's own defaults rather than pinned in molecule.yml, so # From the role's own defaults rather than pinned in molecule.yml, so the version
# the version assertion below compares the running image against what the # assertion compares the running image against what the role ships, not the scenario.
# role ships instead of against the scenario itself.
- name: Load the role's defaults under a separate name - name: Load the role's defaults under a separate name
ansible.builtin.include_vars: ansible.builtin.include_vars:
file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults/main.yml" file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults/main.yml"
@@ -51,10 +45,9 @@
delay: 5 delay: 5
failed_when: false failed_when: false
# `Restart=always` means a bridge crash-looping on a configuration it cannot # `Restart=always` means a bridge crash-looping on unreadable config still reports
# read still reports `active`, so the restart counter is checked too. It is # `active`, so the restart counter is checked too. Asserted `is defined` because
# asserted `is defined` because `| int` turns a missing property into 0 and # `| int` turns a missing property into 0 and would pass vacuously.
# would pass vacuously.
- name: Assert the service is active and has not been restarting - name: Assert the service is active and has not been restarting
ansible.builtin.assert: ansible.builtin.assert:
that: that:
@@ -68,10 +61,9 @@
automatic restart(s) automatic restart(s)
success_msg: "matrix-mautrix-meta-messenger.service is active and has not restarted" success_msg: "matrix-mautrix-meta-messenger.service is active and has not restarted"
# The appservice port is the bridge's own listener, the one a homeserver # The appservice listener is where a homeserver would push transactions. It opening at all
# would push transactions to. It opening at all means the bridge got through # means the bridge got through reading its configuration, through its /whoami check, and
# reading its configuration, through its /whoami check against the # through setting itself up.
# homeserver, and through setting itself up.
- name: Wait for the bridge to open its appservice port - name: Wait for the bridge to open its appservice port
ansible.builtin.command: ansible.builtin.command:
argv: argv:
@@ -107,9 +99,8 @@
src: "{{ matrix_bridge_mautrix_meta_messenger_config_path }}/config.yaml" src: "{{ matrix_bridge_mautrix_meta_messenger_config_path }}/config.yaml"
register: mautrix_meta_messenger_config_file register: mautrix_meta_messenger_config_file
# Each of these differs from what the bridge would use on its own, so their # Each differs from what the bridge would use on its own, so their presence rules out
# presence means the role's configuration is what the bridge is running on # a coincidence.
# rather than something that happened to agree with it.
- name: Assert the rendered configuration carries this scenario's values - name: Assert the rendered configuration carries this scenario's values
ansible.builtin.assert: ansible.builtin.assert:
that: that:
@@ -120,21 +111,19 @@
- mautrix_meta_messenger_config.appservice.hs_token == matrix_bridge_mautrix_meta_messenger_homeserver_token - mautrix_meta_messenger_config.appservice.hs_token == matrix_bridge_mautrix_meta_messenger_homeserver_token
- mautrix_meta_messenger_config.bridge.command_prefix == matrix_bridge_mautrix_meta_messenger_bridge_command_prefix - mautrix_meta_messenger_config.bridge.command_prefix == matrix_bridge_mautrix_meta_messenger_bridge_command_prefix
- mautrix_meta_messenger_config.logging.min_level == matrix_bridge_mautrix_meta_messenger_logging_min_level - mautrix_meta_messenger_config.logging.min_level == matrix_bridge_mautrix_meta_messenger_logging_min_level
# matrix_admin is empty in the shared context, so the only per-domain # matrix_admin is empty in the shared context, so the only per-domain permission
# permission left is the one the role derives from the homeserver domain. # left is the one the role derives from the homeserver domain.
- mautrix_meta_messenger_config.bridge.permissions[matrix_bridge_mautrix_meta_messenger_homeserver_domain] == 'user' - mautrix_meta_messenger_config.bridge.permissions[matrix_bridge_mautrix_meta_messenger_homeserver_domain] == 'user'
# From the shared context rather than from this scenario, but the role # From the shared context rather than this scenario, but the role is what has to
# is what has to carry it into the configuration. # carry it into the configuration.
- mautrix_meta_messenger_config.encryption.allow == matrix_bridges_encryption_enabled - mautrix_meta_messenger_config.encryption.allow == matrix_bridges_encryption_enabled
fail_msg: "The rendered configuration does not carry the scenario's values" fail_msg: "The rendered configuration does not carry the scenario's values"
success_msg: "The rendered configuration carries the scenario's values" success_msg: "The rendered configuration carries the scenario's values"
# The mode is the variable that makes this role family unusual, and it is # The mode is not written into the configuration as-is: the role expands it into an
# not written into the configuration as-is: the role expands it into an # appservice id, a ghost username prefix, a bot displayname and the bridge's `tor` switch.
# appservice id, a ghost username prefix, a bot displayname and the bridge's # Each holds a different value under the role's default mode, so together they prove the
# `tor` switch. Each of these holds a different value under the role's # choice propagated rather than being ignored.
# default `messenger` mode, so together they are what proves the choice
# propagated rather than being ignored.
- name: Assert the configuration reflects the Meta mode the scenario selected - name: Assert the configuration reflects the Meta mode the scenario selected
ansible.builtin.assert: ansible.builtin.assert:
that: that:
@@ -173,9 +162,8 @@
{{ mautrix_meta_messenger_config.appservice.public_address | default('unset') }} {{ mautrix_meta_messenger_config.appservice.public_address | default('unset') }}
success_msg: "The rendered configuration carries the public address the exposure settings imply" success_msg: "The rendered configuration carries the public address the exposure settings imply"
# The registration file is the half of the appservice handshake the # The role generates the registration; the bridge only consumes it. Worth checking on its
# homeserver reads, and it is generated by the role rather than by the # own, as it is the half of the handshake the homeserver reads.
# bridge, so it is worth checking on its own.
- name: Read the appservice registration the role rendered - name: Read the appservice registration the role rendered
ansible.builtin.slurp: ansible.builtin.slurp:
src: "{{ matrix_bridge_mautrix_meta_messenger_config_path }}/registration.yaml" src: "{{ matrix_bridge_mautrix_meta_messenger_config_path }}/registration.yaml"
@@ -192,10 +180,9 @@
fail_msg: "The appservice registration does not carry the scenario's tokens, id and bot user" fail_msg: "The appservice registration does not carry the scenario's tokens, id and bot user"
success_msg: "The appservice registration carries the scenario's tokens, id and bot user" success_msg: "The appservice registration carries the scenario's tokens, id and bot user"
# The namespaces are regexes the role assembles, so rather than comparing # The namespaces are regexes the role assembles. Comparing them as strings would only
# them as strings - which would only re-derive the role's own escaping - # re-derive the role's own escaping, so they are checked by what they match: the bot,
# they are checked by what they do: cover the bot, cover this mode's ghosts, # this mode's ghosts, and not the ghosts of the mode the role would have defaulted to.
# and not cover the ghosts of the mode the role would have defaulted to.
- name: Assert the registration namespaces cover the bot and this mode's ghost users - name: Assert the registration namespaces cover the bot and this mode's ghost users
ansible.builtin.assert: ansible.builtin.assert:
that: that:
@@ -214,8 +201,8 @@
mautrix_meta_messenger_messenger_ghost_mxid: "@messenger_1234567890:{{ matrix_bridge_mautrix_meta_messenger_homeserver_domain }}" mautrix_meta_messenger_messenger_ghost_mxid: "@messenger_1234567890:{{ matrix_bridge_mautrix_meta_messenger_homeserver_domain }}"
mautrix_meta_messenger_bot_mxid: "@{{ matrix_bridge_mautrix_meta_messenger_appservice_username }}:{{ matrix_bridge_mautrix_meta_messenger_homeserver_domain }}" mautrix_meta_messenger_bot_mxid: "@{{ matrix_bridge_mautrix_meta_messenger_appservice_username }}:{{ matrix_bridge_mautrix_meta_messenger_homeserver_domain }}"
# The cheap proof that the data path reached the process and is writable by # Cheap proof that the data path reached the process and is writable by the uid the role
# the uid the role runs the container as. # runs the container as.
- name: Look for the bridge's sqlite database under the role's data path - name: Look for the bridge's sqlite database under the role's data path
ansible.builtin.stat: ansible.builtin.stat:
path: "{{ matrix_bridge_mautrix_meta_messenger_data_path }}/mautrix-meta.db" path: "{{ matrix_bridge_mautrix_meta_messenger_data_path }}/mautrix-meta.db"
@@ -244,10 +231,9 @@
register: mautrix_meta_messenger_image register: mautrix_meta_messenger_image
changed_when: false changed_when: false
# Both Meta bridges are published to the same image repository, with # Both Meta bridges publish to the same image repository, with Instagram's tags carrying
# Instagram's tags carrying an `ig-` prefix, so the tag is compared whole # an `ig-` prefix. Compared whole rather than by substring, so pulling the other bridge's
# rather than by substring: an `ig-` prefix would mean this role pulled the # image would fail here.
# other bridge's image.
- name: Assert the running container is the Messenger image at the version defaults/main.yml pins - name: Assert the running container is the Messenger image at the version defaults/main.yml pins
ansible.builtin.assert: ansible.builtin.assert:
that: that:
@@ -263,11 +249,10 @@
src: "{{ matrix_bridge_mautrix_meta_messenger_base_path }}/labels" src: "{{ matrix_bridge_mautrix_meta_messenger_base_path }}/labels"
register: mautrix_meta_messenger_labels register: mautrix_meta_messenger_labels
# Traefik is not running here, so what is checked is what the role wrote, # Traefik is not running here, so this checks what the role wrote, not what a reverse
# not what a reverse-proxy would do with it. The port matters: it is # proxy would do with it. The port matters: it is hardcoded in the role's templates rather
# hardcoded in the role's templates rather than derived from a variable, so # than derived from a variable, so nothing else here would catch it drifting from the port
# nothing else in this scenario would catch it drifting from the port the # the bridge actually listens on.
# bridge actually listens on.
- name: Assert the labels route the exposure hostname and path prefix to the appservice port - name: Assert the labels route the exposure hostname and path prefix to the appservice port
ansible.builtin.assert: ansible.builtin.assert:
that: that:
@@ -29,35 +29,29 @@ provisioner:
all: all:
matrix_bridge_mautrix_whatsapp_container_network: mautrix-whatsapp-molecule matrix_bridge_mautrix_whatsapp_container_network: mautrix-whatsapp-molecule
# The homeserver stub prepare.yml stands up. The bridge contacts it # The stub prepare.yml stands up. Not a real homeserver, and nothing is asserted
# while starting; it is not a real homeserver and nothing is asserted
# about it. # about it.
matrix_bridge_mautrix_whatsapp_homeserver_address: http://matrix.molecule.local:8008 matrix_bridge_mautrix_whatsapp_homeserver_address: http://matrix.molecule.local:8008
# sqlite keeps the scenario to one container. The role only requires a # sqlite keeps the scenario to one container. Which database engine the bridge can
# database hostname when the engine is postgres, and testing which # talk to is not what this proves.
# database engine the bridge can talk to is not what this proves.
matrix_bridge_mautrix_whatsapp_database_engine: sqlite matrix_bridge_mautrix_whatsapp_database_engine: sqlite
# Appservice tokens. These are what the bridge and homeserver would # Here these only have to reach the rendered configuration and the registration.
# authenticate to each other with; here they only have to reach the
# rendered configuration and the registration file.
matrix_bridge_mautrix_whatsapp_appservice_token: molecule_as_token_4f2a91 matrix_bridge_mautrix_whatsapp_appservice_token: molecule_as_token_4f2a91
matrix_bridge_mautrix_whatsapp_homeserver_token: molecule_hs_token_9b3e77 matrix_bridge_mautrix_whatsapp_homeserver_token: molecule_hs_token_9b3e77
# Deliberately different from the role's defaults, so verify.yml can # Different from the role's defaults, so verify.yml can tell what the role rendered
# tell what the role rendered apart from what the bridge would have # apart from what the bridge would have chosen.
# defaulted to on its own.
matrix_bridge_mautrix_whatsapp_appservice_bot_username: molecule-whatsappbot matrix_bridge_mautrix_whatsapp_appservice_bot_username: molecule-whatsappbot
matrix_bridge_mautrix_whatsapp_homeserver_domain: molecule.local matrix_bridge_mautrix_whatsapp_homeserver_domain: molecule.local
# Traefik is not deployed here, so the labels the role would render for # Traefik is not deployed here, so the labels the role would render for it are
# it are switched off and their absence is asserted instead. # switched off and their absence is asserted instead.
matrix_bridge_mautrix_whatsapp_container_labels_traefik_enabled: false matrix_bridge_mautrix_whatsapp_container_labels_traefik_enabled: false
# verify.yml runs as its own play, where role defaults are out of scope, # verify.yml runs as its own play, where the role's defaults are out of scope,
# so the paths it reads are pinned here as literals matching what the # so the paths it reads are pinned here to match what the role derives.
# role derives from matrix_base_data_path.
matrix_bridge_mautrix_whatsapp_base_path: /matrix/mautrix-whatsapp matrix_bridge_mautrix_whatsapp_base_path: /matrix/mautrix-whatsapp
matrix_bridge_mautrix_whatsapp_config_path: /matrix/mautrix-whatsapp/config matrix_bridge_mautrix_whatsapp_config_path: /matrix/mautrix-whatsapp/config
matrix_bridge_mautrix_whatsapp_data_path: /matrix/mautrix-whatsapp/data matrix_bridge_mautrix_whatsapp_data_path: /matrix/mautrix-whatsapp/data
@@ -31,9 +31,8 @@
docker_daemon_options: docker_daemon_options:
storage-driver: fuse-overlayfs storage-driver: fuse-overlayfs
# The role's file tasks set owner/group by name and Ansible resolves those # The role's file tasks set owner/group by name, which Ansible resolves through the
# through the passwd database, so they have to exist first. matrix-base # passwd database, so they have to exist first. `matrix-base` creates them for real.
# creates them in a real deployment.
- name: Ensure the matrix group exists - name: Ensure the matrix group exists
ansible.builtin.group: ansible.builtin.group:
name: "{{ matrix_group_name }}" name: "{{ matrix_group_name }}"
@@ -70,8 +69,8 @@
- mautrix_whatsapp_molecule_network.rc != 0 - mautrix_whatsapp_molecule_network.rc != 0
- "'already exists' not in mautrix_whatsapp_molecule_network.stderr" - "'already exists' not in mautrix_whatsapp_molecule_network.stderr"
# The bridge contacts the homeserver as it starts. It is not being asked to # The bridge contacts the homeserver as it starts. It is not being asked to bridge
# bridge anything - see molecule-shared/homeserver-stub.py. # anything. See molecule-shared/homeserver-stub.py.
- name: Ensure the homeserver stub is running - name: Ensure the homeserver stub is running
ansible.builtin.include_tasks: ansible.builtin.include_tasks:
file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/tasks/homeserver-stub.yml" file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/tasks/homeserver-stub.yml"
@@ -3,10 +3,11 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
--- ---
# What this proves: the bridge starts, reads the configuration and registration # Proves the bridge starts, reads the configuration and registration the role rendered, opens
# the role rendered, opens its appservice port, and is the version the role # its appservice port, and is the version the role pins.
# pins. It does NOT bridge anything - there is no WhatsApp on the other side and #
# there is deliberately never going to be one. See docs/molecule-testing.md. # It does NOT bridge anything: there is no WhatsApp on the other side, and deliberately never
# will be. See docs/molecule-testing.md.
- name: Verify mautrix-whatsapp - name: Verify mautrix-whatsapp
hosts: all hosts: all
become: true become: true
@@ -16,9 +17,8 @@
gather_facts: false gather_facts: false
tasks: tasks:
# Read from the role's own defaults rather than pinned in molecule.yml, so # From the role's own defaults rather than pinned in molecule.yml, so the version
# the version assertion below compares the running image against what the # assertion compares the running image against what the role ships, not the scenario.
# role ships instead of against the scenario itself.
- name: Load the role's defaults under a separate name - name: Load the role's defaults under a separate name
ansible.builtin.include_vars: ansible.builtin.include_vars:
file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults/main.yml" file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults/main.yml"
@@ -33,10 +33,9 @@
delay: 5 delay: 5
failed_when: false failed_when: false
# `Restart=always` means a bridge crash-looping on a configuration it cannot # `Restart=always` means a bridge crash-looping on unreadable config still reports
# read still reports `active`, so the restart counter is checked too. It is # `active`, so the restart counter is checked too. Asserted `is defined` because
# asserted `is defined` because `| int` turns a missing property into 0 and # `| int` turns a missing property into 0 and would pass vacuously.
# would pass vacuously.
- name: Assert the service is active and has not been restarting - name: Assert the service is active and has not been restarting
ansible.builtin.assert: ansible.builtin.assert:
that: that:
@@ -50,9 +49,8 @@
automatic restart(s) automatic restart(s)
success_msg: "matrix-mautrix-whatsapp.service is active and has not restarted" success_msg: "matrix-mautrix-whatsapp.service is active and has not restarted"
# The appservice port is the bridge's own listener, the one a homeserver # The appservice listener is where a homeserver would push transactions. It opening at all
# would push transactions to. It opening at all means the bridge got through # means the bridge got through reading its configuration and setting itself up.
# reading its configuration and setting itself up.
- name: Wait for the bridge to open its appservice port - name: Wait for the bridge to open its appservice port
ansible.builtin.command: ansible.builtin.command:
argv: argv:
@@ -88,9 +86,8 @@
src: "{{ matrix_bridge_mautrix_whatsapp_config_path }}/config.yaml" src: "{{ matrix_bridge_mautrix_whatsapp_config_path }}/config.yaml"
register: mautrix_whatsapp_config_file register: mautrix_whatsapp_config_file
# Each of these differs from what the bridge would use on its own, so their # Each differs from what the bridge would use on its own, so their presence rules out
# presence means the role's configuration is what the bridge is running on # a coincidence.
# rather than something that happened to agree with it.
- name: Assert the rendered configuration carries this scenario's values - name: Assert the rendered configuration carries this scenario's values
ansible.builtin.assert: ansible.builtin.assert:
that: that:
@@ -102,9 +99,8 @@
vars: vars:
mautrix_whatsapp_config_rendered: "{{ mautrix_whatsapp_config_file.content | b64decode }}" mautrix_whatsapp_config_rendered: "{{ mautrix_whatsapp_config_file.content | b64decode }}"
# The registration file is the half of the appservice handshake the # The role generates the registration; the bridge only consumes it. Worth checking on its
# homeserver reads, and it is generated by the role rather than by the # own, as it is the half of the handshake the homeserver reads.
# bridge, so it is worth checking on its own.
- name: Read the appservice registration the role rendered - name: Read the appservice registration the role rendered
ansible.builtin.slurp: ansible.builtin.slurp:
src: "{{ matrix_bridge_mautrix_whatsapp_config_path }}/registration.yaml" src: "{{ matrix_bridge_mautrix_whatsapp_config_path }}/registration.yaml"
@@ -121,9 +117,8 @@
vars: vars:
mautrix_whatsapp_registration_rendered: "{{ mautrix_whatsapp_registration_file.content | b64decode }}" mautrix_whatsapp_registration_rendered: "{{ mautrix_whatsapp_registration_file.content | b64decode }}"
# sqlite was chosen in molecule.yml, so the bridge should have created its # Cheap proof that the data path reached the process and is writable by the uid the role
# database under the role's data path. This is the cheap proof that the data # runs it as.
# path reached the process and is writable by the uid the role runs it as.
- name: Look for the bridge's sqlite database under the role's data path - name: Look for the bridge's sqlite database under the role's data path
ansible.builtin.stat: ansible.builtin.stat:
path: "{{ matrix_bridge_mautrix_whatsapp_data_path }}/mautrix-whatsapp.db" path: "{{ matrix_bridge_mautrix_whatsapp_data_path }}/mautrix-whatsapp.db"