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_01SEH3vxYSQ5SV4N5z61eyGTpull/5575/head
| @@ -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 | |||||
| exit if it is unreachable, so a scenario cannot get them running without one. | |||||
| Standing up a real Synapse for every role would dominate the run time and drag | |||||
| 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 | |||||
| the blandest plausible response in each case. It is deliberately permissive: an | |||||
| unknown path returns `{}` with a 200 rather than a 404, because the goal is to | |||||
| 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 | |||||
| scenario should assert *about*. Assert on what the role rendered and on what the | |||||
| component reports about itself. If a scenario starts needing this stub to behave | |||||
| like a real homeserver, that scenario has outgrown what these tests are for. | |||||
| Most components talk to a homeserver while starting up and exit if it is unreachable, | |||||
| so a scenario cannot get them running without one. A real Synapse for every role would | |||||
| dominate the run time and drag in Postgres, and the scenarios are not testing Synapse. | |||||
| This answers the handful of endpoints components touch during startup, with the blandest | |||||
| plausible response in each case. Deliberately permissive: an unknown path returns `{}` with | |||||
| a 200 rather than a 404, because the goal is to get the component past its startup checks. | |||||
| What it is NOT: an authentication check, a room state machine, or anything a scenario should | |||||
| assert *about*. Assert on what the role rendered and what the component reports about itself. | |||||
| A scenario that needs this stub to behave like a real homeserver has outgrown what these | |||||
| 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 | |||||
| # startup (matrix-alertmanager-receiver, for one) fail if the rooms they were | |||||
| # configured with are missing, so a scenario passes its own room IDs in. | |||||
| # Rooms reported as already joined. Components that resolve a room mapping at startup | |||||
| # (matrix-alertmanager-receiver, for one) fail if the rooms they were configured with | |||||
| # 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 | |||||
| # matrix-sdk: baibot and the other bots) ask for a 30s timeout and immediately | |||||
| # ask again when the call returns, so answering instantly would spin them into a | |||||
| # hot loop that eats the test machine. Honouring the requested timeout, capped | |||||
| # here, keeps an idle bot idle. | |||||
| # Longest a /sync call is held open. Long-polling clients ask for a 30s timeout and | |||||
| # immediately ask again when the call returns, so answering instantly spins them into a hot | |||||
| # loop that eats the test machine. Honouring the requested timeout, capped 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 | |||||
| # appservice with a token log in here. Matched loosely on purpose: | |||||
| # clients differ on the API version prefix (matrix-nio has shipped both | |||||
| # /_matrix/client/r0/login and /_matrix/client/v3/login over time), and a | |||||
| # login that falls through to the catch-all `{}` below looks to the | |||||
| # client like bad credentials. | |||||
| # Where bots authenticating with a username and password log in, rather than as an | |||||
| # appservice with a token. Matched loosely on purpose, because clients differ on the | |||||
| # API version prefix, and a login falling 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 | |||||
| # probe of an endpoint not listed here still gets past it. | |||||
| # Anything unrecognised: an empty object, so a component probing an endpoint | |||||
| # 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 | |||||
| # need to see what the component is actually asking for. | |||||
| # Quiet by default. STUB_VERBOSE=1 when a scenario will not start and you need | |||||
| # 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") | ||||
| @@ -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 | |||||
| # defaults. In a real run `matrix-base` and `group_vars/matrix_servers` provide | |||||
| # them; in a scenario they have to come from somewhere, and including | |||||
| # `matrix-base` itself does far more than a role scenario needs. | |||||
| # The variables a role reads from its surroundings rather than from its own defaults. | |||||
| # In a real run `matrix-base` and `group_vars/matrix_servers` provide them. | |||||
| # | # | ||||
| # 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 | |||||
| # of testing a role with values it would not have chosen for itself. | |||||
| # Gotcha: `vars_files` outranks inventory `group_vars`, so a scenario cannot override these there. | |||||
| # | # | ||||
| # Keep this to variables that come from OUTSIDE the role under test. Anything the | |||||
| # role defines belongs in the scenario, not here. | |||||
| # Keep this to variables that come from OUTSIDE the role under test. | |||||
| # 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 | |||||
| # id is what proves a role used the one it was given rather than coinciding with | |||||
| # the image's own. | |||||
| # Deliberately not 1000: the base images already have a user there, so a distinct id | |||||
| # is what proves a role used the one it was given rather than coinciding with 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 | |||||
| # administrator into a room skip doing so when it is empty, which is what a | |||||
| # scenario wants. | |||||
| # Empty in the playbook's own defaults too. Components that would invite an administrator | |||||
| # into a room skip doing so when it is empty. | |||||
| 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 | |||||
| # scenario. The values match the playbook's own defaults: encryption off, no | |||||
| # relay, no MSC4190. A bridge scenario that wants to prove one of these reaches | |||||
| # Every bridge role reads these, so they live here rather than in each bridge's scenario. | |||||
| # The values match the playbook's own defaults. A scenario proving 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 | |||||
| # matrix_domain rather than left as Jinja, so a scenario can read them in | |||||
| # verify.yml without the role's defaults being in scope. | |||||
| # 18 of the roles here read one of these. Rendered against the scenario's matrix_domain | |||||
| # rather than left as Jinja, so verify.yml can read them without the role's defaults 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,7 +1,5 @@ | |||||
| --- | --- | ||||
| # Shared by every role scenario under roles/custom/*/molecule/, referenced from | |||||
| # each scenario's molecule.yml. Kept in one place so the pins cannot drift | |||||
| # apart across roles. | |||||
| # Shared by every role scenario, 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 | ||||
| @@ -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 | |||||
| # component contacts a homeserver while starting up. | |||||
| # Stands up a stand-in homeserver on a container network, for scenarios whose component | |||||
| # 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 | |||||
| # importantly, for what it is not. | |||||
| # See molecule-shared/homeserver-stub.py for what it answers and, more importantly, 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 | |||||
| # hostname rather than a container name. | |||||
| # The alias is what the component resolves, so its configuration can name a hostname | |||||
| # 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 | |||||
| # returned is not the bot user they were configured as, so a scenario | |||||
| # bridging anything has to tell the stub who it should claim to be. | |||||
| # Appservices call /whoami on startup and refuse to run if the id returned is not | |||||
| # the bot user they were configured as, so a scenario bridging anything has to tell | |||||
| # 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 | |||||
| # log every request it is asked for, which is both how you find out why a | |||||
| # component will not start and - for a component that exposes no port of | |||||
| # its own - the only place a scenario can observe it acting on what the | |||||
| # role configured. | |||||
| # Set molecule_shared_stub_verbose to "1" to log every request the stub is asked for. | |||||
| # How you find out why a component will not start, and for a component with no port of | |||||
| # its own, the only place to 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 }}" | ||||
| @@ -1,13 +1,11 @@ | |||||
| --- | --- | ||||
| # Helper container images the scenarios use for probing. They live here rather | |||||
| # than inline in each verify.yml so that there is one pin per image instead of | |||||
| # one per role, and so Renovate can see them (see the customManager in | |||||
| # .github/renovate.json). | |||||
| # Helper container images the scenarios use for probing. Here rather than inline in each | |||||
| # verify.yml, so there is one pin per image instead of one per role, and so Renovate can see | |||||
| # them. See the customManager in .github/renovate.json. | |||||
| # Used to reach a role's container over its own container network. A helper is | |||||
| # needed because the role publishes no host port - exactly as in a real | |||||
| # deployment - and publishing one for the test would collide between scenarios | |||||
| # running in parallel. | |||||
| # Used to reach a role's container over its own container network, because the role publishes | |||||
| # no host port - exactly as in a real deployment. Publishing one for the test would collide | |||||
| # between scenarios 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 | |||||
| # (`devture_systemd_docker_base_*`, `devture_playbook_help_*`), the same way | |||||
| # they do when the playbook runs. `matrix-base` is deliberately NOT included: | |||||
| # it does far more than this role needs, and the two variables it would supply | |||||
| # are set directly in molecule.yml instead. | |||||
| # The devture base roles carry the variables this role reads, the same way they do when | |||||
| # the playbook runs. `matrix-base` is deliberately NOT included: it does far more than this | |||||
| # role needs, and the two variables it would supply are set directly in molecule.yml. | |||||
| - 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 | |||||
| # `systemd_service_manager`'s job - so the scenario starts it here. | |||||
| # The role installs the unit but does not start it; in the playbook that is | |||||
| # `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 | |||||
| # of scope, so the paths it reads are pinned here as literals. They | |||||
| # match what the role derives from matrix_base_data_path above. | |||||
| # verify.yml runs as its own play, where the role's defaults are out of scope, | |||||
| # so the paths it reads are pinned here to match what the role derives. | |||||
| 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 | |||||
| # render for it are switched off and their absence is asserted instead. | |||||
| # Traefik is not deployed here, so the labels the role would render for it are | |||||
| # 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, | |||||
| # metrics disabled, alerts under /alerts), so that `verify.yml` can tell | |||||
| # what the role rendered apart from what the application would have done | |||||
| # on its own. | |||||
| # Different from the role's own defaults, so verify.yml can tell 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 | |||||
| # rooms and exits 1 if that fails - so prepare.yml stands up a stub for it | |||||
| # The homeserver IS reached at startup: the service fetches its joined rooms and | |||||
| # 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 | |||||
| # through the passwd database - so they have to exist before it runs. In a | |||||
| # real deployment `matrix-base` creates them. | |||||
| # The role's file tasks set owner/group by name, which Ansible resolves through the | |||||
| # passwd database, so they have to exist first. `matrix-base` creates them for real. | |||||
| - 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 - | |||||
| # 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. The shared stub is enough; see molecule-shared/homeserver-stub.py. | |||||
| # The service 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. | |||||
| # 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 | |||||
| # molecule.yml, so that the assertion further down compares the running | |||||
| # image against what defaults/main.yml actually ships. Pinning it here | |||||
| # would make that assertion compare the scenario with itself. | |||||
| # Read from the role's own defaults rather than pinned in molecule.yml, so the version | |||||
| # assertion compares the running image against what defaults/main.yml ships. | |||||
| # Pinning it here 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`, | |||||
| # so the restart counter is checked alongside it. 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. | |||||
| # `Restart=always` means a crash-looping container still reports `active`, so the restart | |||||
| # counter is checked too. Asserted `is defined` because `| int` turns a missing property | |||||
| # into 0 and 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: | ||||
| @@ -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 | |||||
| # role publishes no host port, exactly as it does in a real deployment, | |||||
| # where Traefik reaches it over the network instead. | |||||
| # Probed from inside the container network rather than the host, because the role | |||||
| # publishes no host port - exactly as in a real deployment, where Traefik reaches it | |||||
| # 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 | |||||
| # 200 here is only reachable if the configuration the role rendered is what | |||||
| # the process is actually running on. | |||||
| # Port and metrics path are both non-default here, so a 200 is only reachable if what | |||||
| # the role rendered is what the process is 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 | |||||
| # path must NOT answer, or a 200 on the configured path would prove nothing | |||||
| # about the configuration having been applied. | |||||
| # Negative control for the assertion above: the role's own default metrics path must NOT | |||||
| # answer, or a 200 on the configured path would prove nothing. | |||||
| - 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 | |||||
| # (`devture_systemd_docker_base_*`, `devture_playbook_help_*`), the same way | |||||
| # they do when the playbook runs. `matrix-base` is deliberately NOT included: | |||||
| # it does far more than this role needs, and what it would supply comes from | |||||
| # molecule-shared/playbook-context.yml instead. | |||||
| # The devture base roles carry the variables this role reads, the same way they do when | |||||
| # the playbook runs. `matrix-base` is deliberately NOT included: it does far more than this | |||||
| # role needs, and what it would supply comes from molecule-shared/playbook-context.yml. | |||||
| - 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 | |||||
| # `systemd_service_manager`'s job - so the scenario starts it here. | |||||
| # The role installs the unit but does not start it; in the playbook that is | |||||
| # `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 | |||||
| # of scope, so the paths it reads are pinned here as literals. They | |||||
| # match what the role derives from matrix_base_data_path. | |||||
| # verify.yml runs as its own play, where the role's defaults are out of scope, | |||||
| # so the paths it reads are pinned here to match what the role derives. | |||||
| 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 | |||||
| # password and then syncs, so a homeserver has to answer for it to get | |||||
| # anywhere. prepare.yml stands up the shared stub for that. | |||||
| # baibot is a plain Matrix client, not an appservice: it logs in with a password | |||||
| # and then syncs, so a homeserver has to answer for it to get anywhere. | |||||
| 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`, | |||||
| # name `baibot`, prefix `!bai`, self-introduction on) AND from baibot's | |||||
| # own built-in defaults, so that a passing assertion cannot be explained | |||||
| # by "it would have happened anyway". | |||||
| # Deliberately different from the role's defaults AND from baibot's own, so 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 | |||||
| # bot loaded. verify.yml reads it. | |||||
| # `debug` rather than the role's `info`, so the journal carries what the bot loaded. | |||||
| 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 | |||||
| # account. It does not have to: providers are contacted only when a | |||||
| # message asks an agent to do something, never at startup. So a static | |||||
| # agent is defined with a placeholder key and a base URL that resolves | |||||
| # nowhere. Nothing is ever called, and the agent still has to survive | |||||
| # baibot's startup parsing of `agents.static_definitions` - which is | |||||
| # what proves the role's provider templating produced something the bot | |||||
| # accepts. | |||||
| # A scenario must not need an AI provider account, and does not have to: providers are | |||||
| # contacted only when a message asks an agent to do something, never at startup. | |||||
| # So the agent below carries a placeholder key and a base URL that resolves nowhere. | |||||
| # Nothing is ever called, yet the definition still has to survive baibot's startup | |||||
| # parsing - which is 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 | |||||
| # through the passwd database - so they have to exist before it runs. In a | |||||
| # real deployment `matrix-base` creates them. | |||||
| # The role's file tasks set owner/group by name, which Ansible resolves through the | |||||
| # passwd database, so they have to exist first. `matrix-base` creates them for real. | |||||
| - 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 | |||||
| # never gets past login and the unit crash-loops. The shared stub is enough | |||||
| # - see molecule-shared/homeserver-stub.py for what it is not. | |||||
| # baibot logs in and then syncs forever; with no homeserver answering it never gets past | |||||
| # login. The shared stub is enough - 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 | |||||
| # from what the homeserver hands back at login, and everything it does | |||||
| # afterwards (its profile, its own-message filtering) hangs off that. | |||||
| # Told to claim the bot's own MXID, because the bot resolves who it is from what the | |||||
| # homeserver hands back at login, and its profile and own-message filtering hang 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 | |||||
| # molecule.yml, so that the assertion further down compares the running | |||||
| # image against what defaults/main.yml actually ships. Pinning it here | |||||
| # would make that assertion compare the scenario with itself. | |||||
| # Read from the role's own defaults rather than pinned in molecule.yml, so the version | |||||
| # assertion compares the running image against what defaults/main.yml ships. | |||||
| # Pinning it here 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`, | |||||
| # so the restart counter is checked alongside it. 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. | |||||
| # `Restart=always` means a crash-looping container still reports `active`, so the restart | |||||
| # counter is checked too. Asserted `is defined` because `| int` turns a missing property | |||||
| # into 0 and 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: | ||||
| @@ -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 - | |||||
| # so what it says about itself has to come from its output. The unit runs | |||||
| # `docker start --attach`, so `--log-driver=none` on the container does not | |||||
| # stop the journal from carrying it. | |||||
| # baibot is a Matrix client, not a server, so what it says about itself has to come from | |||||
| # its output. The unit runs `docker start --attach`, so `--log-driver=none` does not stop | |||||
| # the journal from carrying it. | |||||
| # | # | ||||
| # `Syncing..` is the line that matters, and it is what carries this scenario | |||||
| # rather than the unit check above. baibot does not exit when its startup | |||||
| # 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 reaches its message loop. Point the avatar at a file that is not | |||||
| # there and the assertion above still passes; this one does not. | |||||
| # `Syncing..` is what carries this scenario, not the unit check above. baibot does not exit | |||||
| # when startup 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 | |||||
| # reaches its message loop. Point the avatar at a missing file 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 | |||||
| # role's default (`baibot`) nor what the stub reports the account already has | |||||
| # (`stub`), so the bot naming this as what it wants can only have come from | |||||
| # the configuration the role rendered. | |||||
| # The scenario's display name is neither the role's default nor what the stub reports the | |||||
| # account already has, so the bot wanting it can only have come from what 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 | |||||
| # (`warn,mxlink=info,baibot=debug`), so proving it arrived means proving that | |||||
| # different targets ended up at different levels - a single global level | |||||
| # would satisfy neither half of this. | |||||
| # `logging` is one string carrying per-target levels, so proving it arrived means proving | |||||
| # different targets ended up at different levels. A single global level satisfies neither half. | |||||
| # | # | ||||
| # First clause: baibot's own records appear at DEBUG, which the role's | |||||
| # default of `info` would not produce. | |||||
| # First clause: baibot's own records appear at DEBUG, which the role's default of `info` | |||||
| # would not produce. | |||||
| # | # | ||||
| # Second clause is the control, and it is not vacuous: at DEBUG the crates | |||||
| # underneath (matrix-sdk and its spans, hyper, eyeball) are extremely | |||||
| # talkative - raising the catch-all level turns these two records into | |||||
| # roughly a hundred. Their silence is the `warn` catch-all being enforced. | |||||
| # Second clause is the control, and it is not vacuous. At DEBUG the crates underneath are | |||||
| # extremely talkative, so raising the catch-all turns these two records into 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 | |||||
| # emit no DEBUG records at all on a first run, so asserting their absence | |||||
| # passed just as happily with mxlink set to `debug`. | |||||
| # The trap here: a control on mxlink was tried first, and mxlink emits no DEBUG records at | |||||
| # all on a first run - so asserting their absence passed just as happily with mxlink set | |||||
| # 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 | |||||
| # a configuration that sets both. This scenario uses the password mode, so | |||||
| # the access-token keys must be rendered as nulls rather than omitted or | |||||
| # left with a value. | |||||
| # The role refuses a configuration that sets both authentication modes. This scenario uses | |||||
| # password mode, so the access-token keys must render as nulls, not be omitted or set. | |||||
| - 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 | |||||
| # per-provider template is rendered to YAML, parsed, merged with an | |||||
| # extension, and dropped into the list as a nested structure. This asserts | |||||
| # the whole round trip, key by key. | |||||
| # The agent presets are the most involved templating in this role: a per-provider template | |||||
| # rendered to YAML, parsed, merged with an extension, nested into the list. Asserted as a | |||||
| # whole round trip, key by key. | |||||
| # | # | ||||
| # No provider is ever contacted. baibot calls one only when a message asks an | |||||
| # agent to do something, and the base URL here resolves nowhere on purpose - | |||||
| # a scenario must not need an account with an AI provider. | |||||
| # No provider is ever contacted. baibot calls one only when a message asks an agent to do | |||||
| # something, and the base URL here resolves nowhere on purpose. | |||||
| - 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 | |||||
| # run, molecule-shared/playbook-context.yml here) and are deliberately not | |||||
| # 1000, which the base image already uses - so this cannot pass by | |||||
| # coincidence with whatever the image would have run as. | |||||
| # The uid/gid come from outside the role and are deliberately not 1000, which the base | |||||
| # image already uses, so this cannot pass by coinciding with the image's own user. | |||||
| - 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 | |||||
| # the bind mount is writable by the user the container runs as - a | |||||
| # read-only-root container whose data directory it could not write would | |||||
| # never have got as far as logging in. | |||||
| # baibot keeps its session and crypto store here. The file existing proves the bind mount | |||||
| # is writable by the user the container runs as. | |||||
| - 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 | |||||
| # (`devture_systemd_docker_base_*`, `devture_playbook_help_*`), the same way | |||||
| # they do when the playbook runs. `matrix-base` is deliberately NOT included: | |||||
| # it does far more than this role needs, and the variables it would supply come | |||||
| # from molecule-shared/playbook-context.yml instead. | |||||
| # The devture base roles carry the variables this role reads, the same way they do when | |||||
| # the playbook runs. `matrix-base` is deliberately NOT included: it does far more than this | |||||
| # role needs, and what it would supply comes from molecule-shared/playbook-context.yml. | |||||
| - 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 | |||||
| # `systemd_service_manager`'s job - so the scenario starts it here. | |||||
| # The role installs the unit but does not start it; in the playbook that is | |||||
| # `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 | |||||
| # homeserver as an ordinary user with a password. The stub prepare.yml | |||||
| # stands up answers /_matrix/client/v3/login with an access token, which | |||||
| # is all the bot needs to get past its login and into its sync loop. | |||||
| # Unlike the bridges, this bot is not an appservice: it logs in as an ordinary user | |||||
| # with a password. The stub answers /_matrix/client/v3/login with an access token, | |||||
| # which is all the bot needs to reach 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 | |||||
| # (`bot.matrix-reminder-bot`), so verify.yml can tell what the role | |||||
| # Different from the role's default localpart, 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 | |||||
| # different from the bot's own fallback (`Etc/UTC`), and it reaches the | |||||
| # container twice - through the config file and through TZ on the unit. | |||||
| # The role has no default here and refuses to run without one. Also different from the | |||||
| # bot's own fallback, and it reaches the container twice: the config file and TZ. | |||||
| 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 | |||||
| # entries of our own exercises the `_auto + _custom` composition. | |||||
| # Both lists default to off with no entries, so turning them on exercises the | |||||
| # `_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 | |||||
| # overriding it is only possible through the extension mechanism. Doing | |||||
| # it here means the merge of template + extension is tested too. | |||||
| # The device name is hardcoded in the role's template, so overriding it is only | |||||
| # possible through the extension mechanism - which tests that merge 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 | |||||
| # that verify.yml can assert the bot opened the path the role gave it, | |||||
| # with the default name as a negative control. | |||||
| # Moved off the role's default name so verify.yml can assert the bot opened the path | |||||
| # the role gave it, 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 | |||||
| # scope, so the paths it reads are pinned here as literals. They match | |||||
| # what the role derives from `matrix_base_data_path`. | |||||
| # verify.yml runs as its own play, where the role's defaults are out of scope, | |||||
| # so the paths it reads are pinned here to match what the role derives. | |||||
| 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 | |||||
| # through the passwd database - so they have to exist before it runs. In a | |||||
| # real deployment `matrix-base` creates them. | |||||
| # The role's file tasks set owner/group by name, which Ansible resolves through the | |||||
| # passwd database, so they have to exist first. `matrix-base` creates them for real. | |||||
| - 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 | |||||
| # stub has to be on it before the bot starts, so it is created here first. | |||||
| # The role creates this network itself during converge, but the stub has to be on it | |||||
| # 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 | |||||
| # the role rendered into its configuration, and retries every 15 seconds | |||||
| # until that succeeds. The shared stub answers the login with an access | |||||
| # token, which is enough to get it into its sync loop. Nothing is asserted | |||||
| # about the stub itself; see molecule-shared/homeserver-stub.py. | |||||
| # Not an appservice: it logs in with the username and password the role rendered, retrying | |||||
| # every 15 seconds until that succeeds. The stub answers with an access token, which is | |||||
| # enough to reach the sync loop. Nothing is asserted about the stub itself. | |||||
| - 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 | |||||
| # rendered, logs into a homeserver as the user the role gave it, opens the | |||||
| # database at the path the role gave it, and is the version the role pins. | |||||
| # Proves matrix-reminder-bot starts on the configuration the role rendered, logs in as the | |||||
| # user the role gave it, opens the database at the path the role gave it, and is the version | |||||
| # the role pins. | |||||
| # | # | ||||
| # The bot has no HTTP surface of its own to probe, so the evidence is what it | |||||
| # says about itself in the journal plus what it left on disk. It does NOT set | |||||
| # real reminders and never will. See docs/molecule-testing.md. | |||||
| # The bot has no HTTP surface to probe, so the evidence is what it says about itself in the | |||||
| # journal plus what it left on disk. It does NOT set real reminders. 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 | |||||
| # molecule.yml, so that the assertion further down compares the running | |||||
| # image against what defaults/main.yml actually ships. Pinning it here | |||||
| # would make that assertion compare the scenario with itself. | |||||
| # Read from the role's own defaults rather than pinned in molecule.yml, so the version | |||||
| # assertion compares the running image against what defaults/main.yml ships. | |||||
| # Pinning it here 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 | |||||
| # read still reports `active`, so the restart counter is checked too. The | |||||
| # config file is parsed before the bot's own catch-all retry loop starts, so | |||||
| # anything wrong in what the role rendered shows up here as restarts. | |||||
| # 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. | |||||
| # `Restart=always` means a bot crash-looping on unreadable config still reports `active`, | |||||
| # so the restart counter is checked too. The config file is parsed before the bot's own | |||||
| # catch-all retry loop starts, so anything wrong in what the role rendered shows up here | |||||
| # as restarts. Asserted `is defined` because `| int` turns a missing property into 0. | |||||
| - 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 | |||||
| # journal despite `--log-driver=none`. That is the only thing this bot | |||||
| # reports about itself - it serves nothing over HTTP. | |||||
| # The unit runs `docker start --attach`, so the container's output is in the journal | |||||
| # despite `--log-driver=none`. It is the only thing this bot reports about itself. | |||||
| # | # | ||||
| # Filtered rather than tailed: the startup lines are the oldest ones in the | |||||
| # journal, so a `--lines=N` tail would lose them behind anything the bot | |||||
| # logs later, and reading the journal whole would pull an unbounded amount | |||||
| # of text into a variable. The filter keeps the failure line too, so the | |||||
| # Filtered rather than tailed: startup lines are the OLDEST in the journal, so a | |||||
| # `--lines=N` tail loses them behind anything logged later, and reading it whole pulls | |||||
| # unbounded 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 | |||||
| # something other than a LoginError, so this is the whole chain at once: the | |||||
| # homeserver URL, the user ID and the password the role rendered were good | |||||
| # enough for a real login round-trip against the stub. | |||||
| # "Logged in as ..." is only reached once the login call returned something other than a | |||||
| # LoginError, so this covers the whole chain at once: homeserver URL, user ID and password | |||||
| # were all good enough for a real login round-trip. | |||||
| - 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 | |||||
| # building the connection string the bot parses, and the bot names the type | |||||
| # it settled on once the database is open. | |||||
| # The role picks the storage engine by building the connection string the bot parses, | |||||
| # and the bot names the type 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 | |||||
| # fallbacks, so their presence means the role rendered this file rather than | |||||
| # the values coinciding with what would have happened anyway. | |||||
| # Every one differs from both the role's defaults and the bot's own fallbacks, so their | |||||
| # presence means the role rendered this file rather than coinciding 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: | ||||
| @@ -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 | |||||
| # only be there if `..._configuration_extension_yaml` was merged over the | |||||
| # template rather than ignored. | |||||
| # `device_name` is hardcoded in the role's template, so this value can only be here if | |||||
| # `..._configuration_extension_yaml` was merged over it 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 | |||||
| # that the storage configuration reached the running process rather than | |||||
| # merely the file on disk. | |||||
| # With no HTTP surface, where the database landed is the evidence that the storage | |||||
| # configuration reached the running process and not 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 | |||||
| # database name must NOT appear, or a file at the configured path would not | |||||
| # prove the configuration reached the process. | |||||
| # Negative control for the assertion above: the role's own default database name must NOT | |||||
| # appear, or a file at the configured path would prove nothing. | |||||
| - 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 | |||||
| # a populated directory means the bot could use the store path the role | |||||
| # created for it inside an otherwise read-only container. | |||||
| # matrix-nio writes its encryption store here once login succeeds, so a populated directory | |||||
| # means the bot could use the store path the role created inside a 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 | |||||
| # above and through TZ on the unit - and the uid/gid come from the playbook | |||||
| # context rather than from anything the image would pick on its own. | |||||
| # The timezone reaches the container twice, through the config file checked above and | |||||
| # through TZ on the unit. The uid/gid come from the playbook context, not from the image. | |||||
| - 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 | |||||
| # starting; it is not a real homeserver and nothing is asserted about it. | |||||
| # The stub prepare.yml stands up. Not a real homeserver, and nothing is asserted | |||||
| # 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 | |||||
| # allowed to administer the bridge. | |||||
| # Heisenbridge refuses to start without an owner: the Matrix user allowed to | |||||
| # 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 | |||||
| # what the role rendered apart from what heisenbridge would have chosen. | |||||
| # Different from the role's defaults, so verify.yml can tell what the role rendered | |||||
| # 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 | |||||
| # the machine and is not what this scenario is proving. | |||||
| # identd binds host port 113, which would collide with anything else on the machine. | |||||
| 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 | |||||
| # it are switched off and their absence is asserted instead. | |||||
| # Traefik is not deployed here, so the labels the role would render for it are | |||||
| # 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. | |||||
| # Unlike the mautrix bridges, heisenbridge keeps everything directly | |||||
| # under its base path rather than in config/ and data/ subdirectories. | |||||
| # verify.yml runs as its own play, where role defaults are out of scope. Unlike the | |||||
| # mautrix bridges, heisenbridge keeps everything directly under its base path. | |||||
| 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 | |||||
| # through the passwd database, so they have to exist first. matrix-base | |||||
| # creates them in a real deployment. | |||||
| # The role's file tasks set owner/group by name, which Ansible resolves through the | |||||
| # passwd database, so they have to exist first. `matrix-base` creates them for real. | |||||
| - 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 | |||||
| # bridge anything - see molecule-shared/homeserver-stub.py. | |||||
| # The bridge contacts the homeserver as it starts. It is not being asked to bridge | |||||
| # 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 | |||||
| # rendered, and is the version the role pins. It does NOT connect to IRC and | |||||
| # never will. See docs/molecule-testing.md. | |||||
| # Proves heisenbridge starts, reads the registration the role rendered, and is the version | |||||
| # the role pins. It does NOT connect to IRC. 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 | |||||
| # read still reports `active`, so the restart counter is checked too. | |||||
| # `Restart=always` means a bridge crash-looping on unreadable config still reports | |||||
| # `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 | |||||
| # config/ and data/ subdirectories the way the mautrix bridges do. | |||||
| # Heisenbridge keeps everything directly under its base path, unlike the mautrix bridges. | |||||
| - 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 | |||||
| # its own, so their presence means the role rendered this file rather than | |||||
| # the bridge generating one. | |||||
| # Token and URL both differ from anything heisenbridge would pick on its own, so their | |||||
| # presence means the role rendered this file rather than 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 it on the command line rather than through a config file, | |||||
| # The role passes the owner 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. | |||||
| # Asserting its absence keeps the default from silently becoming "on". | |||||
| # identd binds host port 113 when enabled. Asserting its absence keeps the default from | |||||
| # 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; | |||||
| # it is not a real homeserver and nothing is asserted about it. | |||||
| # The stub prepare.yml stands up. Not a real homeserver, and nothing is asserted | |||||
| # 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 | |||||
| # authenticate to each other with; here they only have to reach the | |||||
| # rendered configuration and the registration file. | |||||
| # Here these only have to reach the rendered configuration and the registration. | |||||
| 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 | |||||
| # registration's sender_localpart can only have come from the role. | |||||
| # prepare.yml tells the homeserver stub to claim this same user id. | |||||
| # Different from the role's default, so the registration's sender_localpart can only | |||||
| # have come from the role. prepare.yml tells the 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 | |||||
| # below differs from BOTH the role's default and Hookshot's own, so an | |||||
| # answer on one of them can only mean the role's configuration reached | |||||
| # the process. verify.yml also asserts that nothing answers on the | |||||
| # defaults these replace (9993 appservice, 9000 webhooks, 9001 metrics). | |||||
| # Hookshot's HTTP surface is the point of this scenario. Every port below differs | |||||
| # from BOTH the role's default and Hookshot's own, so an answer can only mean the | |||||
| # role's configuration reached the process. verify.yml also asserts nothing answers | |||||
| # on the defaults these replace. | |||||
| 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 | |||||
| # entry in Hookshot's `listeners` list, which is the cheapest listener to | |||||
| # assert *content* on: /metrics answers in a format nothing else would. | |||||
| # Off in the role's defaults. Enabling it renders a second entry in the `listeners` | |||||
| # list, and /metrics is the cheapest listener to assert *content* on. | |||||
| 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 | |||||
| # listener can be asserted too - the widgets port staying closed is what | |||||
| # tells "the role rendered the listener list" apart from "Hookshot binds | |||||
| # everything anyway". | |||||
| # On in the role's defaults. Off here so the absence of a listener can be asserted: | |||||
| # the widgets port staying closed is what tells "the role rendered the listener list" | |||||
| # apart from "Hookshot binds 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 | |||||
| # of verify.yml. GitLab is the one the role enables by default, so it is | |||||
| # explicitly switched off and its absence from the rendered config and | |||||
| # registration is asserted. | |||||
| # No third-party service is configured here; see the header of verify.yml. | |||||
| # GitLab is the one the role enables by default, so it is explicitly switched off. | |||||
| matrix_bridge_hookshot_gitlab_enabled: false | matrix_bridge_hookshot_gitlab_enabled: false | ||||
| # The generic webhooks listener is the only part of Hookshot that needs | |||||
| # no account anywhere, so it is what this scenario exercises live. The | |||||
| # prefix differs from the role's default (`_webhooks_`). | |||||
| # The generic webhooks listener needs no account anywhere, so it is what this | |||||
| # scenario exercises live. The prefix differs from the role's default. | |||||
| 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 | |||||
| # is a third value, so finding it in config.yml cannot be a coincidence. | |||||
| # The role defaults to `warn`, Hookshot itself to `info`. A third value, so finding | |||||
| # 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 | |||||
| # it are switched off and their absence is asserted instead. | |||||
| # Traefik is not deployed here, so the labels the role would render for it are | |||||
| # 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, | |||||
| # so what it reads is pinned here. These two match the role's own | |||||
| # defaults on purpose - they name things (a path, a container) rather | |||||
| # than configure them, and nothing is asserted *about* them. | |||||
| # verify.yml runs as its own play, where role defaults are out of scope, so what it | |||||
| # reads is pinned here. These two match the role's own defaults on purpose: they name | |||||
| # things rather 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 | |||||
| # through the passwd database, so they have to exist first. matrix-base | |||||
| # creates them in a real deployment. | |||||
| # The role's file tasks set owner/group by name, which Ansible resolves through the | |||||
| # passwd database, so they have to exist first. `matrix-base` creates them for real. | |||||
| - 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 | |||||
| # the role rendered, and opens exactly the HTTP listeners that configuration | |||||
| # described - on the ports the role put there, and not on the ones it did not. | |||||
| # Proves Hookshot starts on the config.yml and registration.yml the role rendered, | |||||
| # and opens exactly the HTTP listeners that configuration described - on the ports | |||||
| # the role put there, and not on the ones it did not. | |||||
| # | # | ||||
| # What it deliberately does NOT do: configure GitHub, GitLab, Jira or Figma. | |||||
| # Every one of those needs an account and a credential on a third-party service, | |||||
| # which is the line where a scenario stops testing this repository and starts | |||||
| # testing a fake (see docs/molecule-testing.md). The generic webhooks listener | |||||
| # needs no credential from anyone, so it is the one that gets exercised live. | |||||
| # Deliberately does NOT configure GitHub, GitLab, Jira or Figma. Each needs an account | |||||
| # and a credential on a third-party service, which is where a scenario stops testing this | |||||
| # repository and starts testing a fake (see docs/molecule-testing.md). The generic webhooks | |||||
| # listener needs no credential from anyone, so it is the one 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 | |||||
| # the version assertion below compares the running image against what the | |||||
| # role ships instead of against the scenario itself. The default ports come | |||||
| # from here for the same reason: the "these ports stay closed" assertion is | |||||
| # only meaningful against the ports the role would otherwise have used. | |||||
| # From the role's own defaults rather than pinned in molecule.yml, so the version | |||||
| # assertion compares the running image against what the role ships, not against the | |||||
| # scenario. The default ports come from here for the same reason: "these ports stay | |||||
| # closed" is 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 | |||||
| # read still reports `active`, so the restart counter is checked too. It is | |||||
| # asserted `is defined` because `| int` turns a missing property into 0 and | |||||
| # would pass vacuously. | |||||
| # `Restart=always` means a bridge crash-looping on unreadable config still reports | |||||
| # `active`, so the restart counter is checked too. Asserted `is defined` because | |||||
| # `| int` turns a missing property into 0 and 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 | |||||
| # what the role defaults to, so finding them here means the role's | |||||
| # configuration is what Hookshot is running on rather than a coincidence. | |||||
| # Each differs from what Hookshot would use on its own AND from the role's defaults, | |||||
| # so finding them here rules out 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 | |||||
| # which resources get a port at all, from a handful of independent switches. | |||||
| # Getting this wrong is invisible in a "did it start" test, which is why it | |||||
| # is asserted as a whole rather than key by key. | |||||
| # The `listeners` list is the role's own construction, assembled from a handful of | |||||
| # independent switches. Getting it wrong is invisible in a "did it start" test, | |||||
| # hence asserting the whole shape 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 | |||||
| # appear. GitLab is the interesting one: the role turns it ON by default, so | |||||
| # its absence is what proves the scenario's switch reached the template. | |||||
| # GitLab is the interesting one: the role turns it ON by default, so its absence | |||||
| # 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 | |||||
| # homeserver reads. The role generates it, Hookshot only consumes it, so it | |||||
| # is worth checking on its own. | |||||
| # The role generates the registration; Hookshot only consumes it. Worth checking | |||||
| # on its own, as it is the half of the handshake the homeserver reads. | |||||
| - 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 | |||||
| # it out of the container name and the appservice port. It has to agree with | |||||
| # `bridge.port` in config.yml or the two halves would silently disagree. | |||||
| # `url` is built from the container name and the appservice port. It has to agree | |||||
| # with `bridge.port` in config.yml, or the two halves 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 | |||||
| # GitLab namespace is conditional on the service being enabled - so this | |||||
| # checks that the two switches reach the registration, not just config.yml. | |||||
| # The user namespace derives from the generic webhook prefix, and the GitLab namespace | |||||
| # is conditional on that service being enabled. Checks both switches reach the | |||||
| # 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 | |||||
| # as in a real deployment; see docs/molecule-testing.md. | |||||
| # A helper container, because the role publishes no host port - exactly as in a real | |||||
| # 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, | |||||
| # with a JSON body no other component would produce. An Express "Cannot POST" | |||||
| # page here would mean the port is Hookshot's but the generic webhooks | |||||
| # service was never mounted on it; a refused connection would mean the | |||||
| # listener the role described was never opened at all. | |||||
| # An unknown webhook id draws a JSON body from the generic-webhook handler that no | |||||
| # other component would produce. An Express "Cannot POST" page would mean the port is | |||||
| # Hookshot's but the generic webhooks service was never mounted on it; a refused | |||||
| # connection would mean the listener 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 | |||||
| # because the scenario asked for it - and /metrics answers in a format | |||||
| # nothing else on that port could have produced. | |||||
| # Metrics are OFF in the role's defaults, so this listener exists only because the | |||||
| # scenario asked for it. | |||||
| - 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` - | |||||
| # so it is a separate socket, opened by a separate part of the config. | |||||
| # The appservice port is not in `listeners`; it comes from `bridge.port`. | |||||
| # 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; | |||||
| # these are the ones the role and Hookshot would have used if the scenario's | |||||
| # configuration had never reached the process. If any of them answers, then | |||||
| # a passing probe above proves much less than it looks like it does. | |||||
| # The other half of the story. Every port above is one the scenario chose; these are | |||||
| # the ones the role and Hookshot would have used had the scenario's configuration never | |||||
| # reached the process. If any of them answers, the probes above prove much less than | |||||
| # 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 | |||||
| # while starting; it is not a real homeserver and nothing is asserted | |||||
| # about it. There is deliberately no Discord on the other side either - | |||||
| # see docs/molecule-testing.md. | |||||
| # The stub prepare.yml stands up. Not a real homeserver, and nothing is asserted | |||||
| # about it. There is deliberately no Discord on the other side either. | |||||
| 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 | |||||
| # database hostname when the engine is postgres, and testing which | |||||
| # database engine the bridge can talk to is not what this proves. | |||||
| # sqlite keeps the scenario to one container. Which 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 | |||||
| # authenticate to each other with; here they only have to reach the | |||||
| # rendered configuration and the registration file. | |||||
| # Here these only have to reach the rendered configuration and the registration. | |||||
| 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 | |||||
| # tell what the role rendered apart from what the bridge would have | |||||
| # defaulted to on its own. | |||||
| # Different from the role's defaults, so verify.yml can tell what the role rendered | |||||
| # apart from what the bridge would have chosen. | |||||
| 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 | |||||
| # uses `debug`. `info` is neither. | |||||
| # The role defaults to `warn`, the bridge's own shipped configuration to `debug`. | |||||
| # `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 | |||||
| # address: `validate_config.yml` fails without | |||||
| # `matrix_bridge_mautrix_discord_bridge_public_address`, which is | |||||
| # derived from these three. Discord fetches avatars over it in relay | |||||
| # mode; nothing reaches it in this scenario, but it has to be set for | |||||
| # the role to run at all. | |||||
| # Unlike most bridge roles here, mautrix-discord *requires* a public address: | |||||
| # `validate_config.yml` fails without one, and it is derived from these three. | |||||
| # Discord fetches avatars over it in relay 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 | |||||
| # avatar-proxy labels verify.yml reads can only look the way they do if | |||||
| # the role composed them from these values. | |||||
| # A non-`/` path prefix and a non-default scheme are chosen so the avatar-proxy labels | |||||
| # verify.yml reads can only look the way they do if the role composed them. | |||||
| 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, | |||||
| # so the paths it reads are pinned here as literals matching what the | |||||
| # role derives from matrix_base_data_path. | |||||
| # verify.yml runs as its own play, where the role's defaults are out of scope, | |||||
| # so the paths it reads are pinned here to match what the role derives. | |||||
| 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 | |||||
| # through the passwd database, so they have to exist first. matrix-base | |||||
| # creates them in a real deployment. | |||||
| # The role's file tasks set owner/group by name, which Ansible resolves through the | |||||
| # passwd database, so they have to exist first. `matrix-base` creates them for real. | |||||
| - 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 | |||||
| # /whoami does not name the bot user it was configured as - see | |||||
| # molecule-shared/homeserver-stub.py. | |||||
| # The bridge contacts the homeserver as it starts and refuses to run if /whoami does not | |||||
| # name the bot user it was configured as. 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: the bridge starts, reads the configuration and registration | |||||
| # the role rendered, opens its appservice port, and is the version the role | |||||
| # pins. It also proves the role composed the avatar-proxy labels out of the | |||||
| # 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 | |||||
| # be one, because that would need a Discord account. See | |||||
| # docs/molecule-testing.md. | |||||
| # Proves the bridge starts, reads the configuration and registration the role rendered, opens | |||||
| # its appservice port, is the version the role pins, and composed the avatar-proxy labels out | |||||
| # of the hostname, scheme and path prefix it was given. | |||||
| # | |||||
| # It does NOT bridge anything: there is no Discord on the other side, and deliberately never | |||||
| # will be, because that would need a Discord account. See 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 | |||||
| # defaults are out of scope in this play, so it is recomposed here from the | |||||
| # same three values the scenario pinned in molecule.yml. | |||||
| # The role derives this from scheme + hostname + path prefix. Role defaults are out of | |||||
| # scope in this play, so it is recomposed from the same three values molecule.yml pinned. | |||||
| 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 | |||||
| # the version assertion below compares the running image against what the | |||||
| # role ships instead of against the scenario itself. | |||||
| # From the role's own defaults rather than pinned in molecule.yml, so the version | |||||
| # assertion compares the running image against what the role ships, not the scenario. | |||||
| - 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 | |||||
| # read still reports `active`, so the restart counter is checked too. It is | |||||
| # asserted `is defined` because `| int` turns a missing property into 0 and | |||||
| # would pass vacuously. | |||||
| # `Restart=always` means a bridge crash-looping on unreadable config still reports | |||||
| # `active`, so the restart counter is checked too. Asserted `is defined` because | |||||
| # `| int` turns a missing property into 0 and 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 | |||||
| # would push transactions to. It opening at all means the bridge got through | |||||
| # reading its configuration and setting itself up. | |||||
| # The appservice listener is where a homeserver would push transactions. It opening at all | |||||
| # means the bridge got through 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 | |||||
| # presence means the role's configuration is what the bridge is running on | |||||
| # rather than something that happened to agree with it. The public address | |||||
| # in particular is composed by the role out of three separate variables. | |||||
| # Each differs from what the bridge would use on its own, so their presence rules out a | |||||
| # coincidence. The public address in particular is composed by the role out of three | |||||
| # separate variables. | |||||
| # | # | ||||
| # Asserted against the parsed document rather than by substring, so a value | |||||
| # landing under the wrong key cannot pass. | |||||
| # Asserted against the parsed document rather than by substring, so a value landing under | |||||
| # 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 | |||||
| # homeserver reads, and it is generated by the role rather than by the | |||||
| # bridge, so it is worth checking on its own. `sender_localpart` is the | |||||
| # role's own `_bot_` prefixing, not something the bridge would produce. | |||||
| # The role generates the registration; the bridge only consumes it. `sender_localpart` | |||||
| # carries 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 | |||||
| # database under the role's data path. This is the cheap proof that the data | |||||
| # path reached the process and is writable by the uid the role runs it as. | |||||
| # Cheap proof that the data 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 | |||||
| # appear because a public address was configured, and their hostname and | |||||
| # path prefix are composed by the role rather than copied from a variable. | |||||
| # The avatar proxy is this role's own reverse-proxy wiring. The labels only appear because | |||||
| # a public address was configured, and the role composes their hostname and path prefix | |||||
| # 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 | |||||
| # renders wrongly is not merely cosmetic - it would stop the container from | |||||
| # being created at all. Reading them back off the running container proves | |||||
| # Docker accepted them. | |||||
| # The label file is fed to `docker create --label-file`, so a wrongly rendered label is | |||||
| # not cosmetic: it stops the container being created at all. Reading them back off the | |||||
| # running container proves 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 | |||||
| # while starting; it is not a real homeserver and nothing is asserted | |||||
| # The stub prepare.yml stands up. 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 | |||||
| # database hostname when the engine is postgres, and testing which | |||||
| # database engine the bridge can talk to is not what this proves. | |||||
| # sqlite keeps the scenario to one container. Which 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 | |||||
| # overridden here. It used to build `sqlite:///` + the in-container path, | |||||
| # which go-sqlite3 takes as a plain filename rather than parsing as a URL, | |||||
| # so the bridge died at startup with `unable to open database file`. This | |||||
| # scenario is what caught it. Leaving the role's own value in place is | |||||
| # what keeps it caught. | |||||
| # The URI the role derives is deliberately NOT overridden here. It used to build | |||||
| # `sqlite:///` + the in-container path, which go-sqlite3 takes as a plain filename | |||||
| # rather than parsing as a URL, so the bridge died at startup. This scenario caught | |||||
| # that, and leaving the role's own value in place is what keeps it caught. | |||||
| # Appservice tokens. These are what the bridge and homeserver would | |||||
| # authenticate to each other with; here they only have to reach the | |||||
| # rendered configuration and the registration file. | |||||
| # Here these only have to reach the rendered configuration and the registration. | |||||
| 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 | |||||
| # upstream codebase serves several Meta networks, and this variable is | |||||
| # what picks which one. It reaches the rendered configuration in several | |||||
| # places at once - the appservice id, the ghost username prefix, the bot | |||||
| # displayname and the bridge's `tor` switch - so a value other than the | |||||
| # 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. | |||||
| # What makes this role family unusual: one upstream codebase serves several Meta | |||||
| # networks, and this variable picks which. It reaches the rendered configuration in | |||||
| # four places at once - appservice id, ghost username prefix, bot displayname and the | |||||
| # bridge's `tor` switch - so a value other than the role's default is what tells | |||||
| # verify.yml the role propagated the choice rather than everything agreeing by accident. | |||||
| # | # | ||||
| # Nothing logs in during the scenario, so the bridge never opens a | |||||
| # connection to Meta (over Tor or otherwise). See docs/molecule-testing.md | |||||
| # for why a scenario stops short of that. | |||||
| # `facebook-tor` is used because it is the only mode that also flips a boolean. | |||||
| # Nothing logs in during the scenario, so the bridge never connects to Meta. | |||||
| 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`, | |||||
| # `!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. | |||||
| # Different from the role's defaults and from what the bridge would pick on its own, | |||||
| # 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 | |||||
| # nothing routes to it; what is being tested is that the role turns | |||||
| # these three variables into both the container's Traefik labels and the | |||||
| # Traefik is not deployed here, so nothing routes to it. What is tested is that the | |||||
| # role turns 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, | |||||
| # so the paths it reads are pinned here as literals matching what the | |||||
| # role derives from matrix_base_data_path. | |||||
| # verify.yml runs as its own play, where the role's defaults are out of scope, | |||||
| # so the paths it reads are pinned here to match what the role derives. | |||||
| 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 | |||||
| # through the passwd database, so they have to exist first. matrix-base | |||||
| # creates them in a real deployment. | |||||
| # The role's file tasks set owner/group by name, which Ansible resolves through the | |||||
| # passwd database, so they have to exist first. `matrix-base` creates them for real. | |||||
| - 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 | |||||
| # it will run at all - it exits if the id it gets back is not the bot user | |||||
| # it was configured as. It is not being asked to bridge anything; there is | |||||
| # no Meta account anywhere in this scenario and there is deliberately never | |||||
| # going to be one. See molecule-shared/homeserver-stub.py. | |||||
| # The bridge calls /whoami before it will run at all, exiting if the id it gets back is | |||||
| # not the bot user it was configured as. It is not being asked to bridge anything: there | |||||
| # is no Meta account in this scenario, and deliberately never will be. | |||||
| - 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 | |||||
| # the role rendered, opens its appservice port, and is the Messenger-side image | |||||
| # at the version the role pins. | |||||
| # Proves the bridge starts, reads the configuration and registration the role rendered, opens | |||||
| # its appservice port, and is the Messenger-side image at the version the role pins. | |||||
| # | # | ||||
| # The extra thing worth proving for this role in particular is that | |||||
| # `matrix_bridge_mautrix_meta_messenger_meta_mode` reaches every place it feeds: | |||||
| # one upstream codebase serves several Meta networks, and the mode is what picks | |||||
| # 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. | |||||
| # The extra thing worth proving here is that `..._meta_mode` reaches every place it feeds: one | |||||
| # upstream codebase serves several Meta networks, and the mode picks which. The scenario runs | |||||
| # `facebook-tor` rather than the role's default, so the assertions can tell the two apart. | |||||
| # | # | ||||
| # It does NOT bridge anything - there is no Facebook or Messenger account on the | |||||
| # other side and there is deliberately never going to be one. See | |||||
| # docs/molecule-testing.md. | |||||
| # It does NOT bridge anything: no Facebook or Messenger account is on the other side, and | |||||
| # deliberately never will be. See 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 | |||||
| # matching slurp. Deliberately not set_fact: the rendered configuration | |||||
| # contains the bridge's own Go templates (`{{.}}` in `username_template`), and | |||||
| # a stored fact gets templated again on every lookup, which would try to | |||||
| # Lazily evaluated, so they resolve only in the tasks after the matching slurp. | |||||
| # Deliberately not set_fact: the rendered configuration contains the bridge's own Go | |||||
| # templates, and 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 | |||||
| # the version assertion below compares the running image against what the | |||||
| # role ships instead of against the scenario itself. | |||||
| # From the role's own defaults rather than pinned in molecule.yml, so the version | |||||
| # assertion compares the running image against what the role ships, not the scenario. | |||||
| - 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 | |||||
| # read still reports `active`, so the restart counter is checked too. It is | |||||
| # asserted `is defined` because `| int` turns a missing property into 0 and | |||||
| # would pass vacuously. | |||||
| # `Restart=always` means a bridge crash-looping on unreadable config still reports | |||||
| # `active`, so the restart counter is checked too. Asserted `is defined` because | |||||
| # `| int` turns a missing property into 0 and 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 | |||||
| # would push transactions to. It opening at all means the bridge got through | |||||
| # reading its configuration, through its /whoami check against the | |||||
| # homeserver, and through setting itself up. | |||||
| # The appservice listener is where a homeserver would push transactions. It opening at all | |||||
| # means the bridge got through reading its configuration, through its /whoami check, 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 | |||||
| # presence means the role's configuration is what the bridge is running on | |||||
| # rather than something that happened to agree with it. | |||||
| # Each differs from what the bridge would use on its own, so their presence rules out | |||||
| # 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: | ||||
| @@ -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 | |||||
| # permission left is the one the role derives from the homeserver domain. | |||||
| # matrix_admin is empty in the shared context, so the only per-domain permission | |||||
| # 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 | |||||
| # is what has to carry it into the configuration. | |||||
| # From the shared context rather than this scenario, but the role is what has to | |||||
| # 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 | |||||
| # 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. Each of these holds a different value under the role's | |||||
| # default `messenger` mode, so together they are what proves the choice | |||||
| # propagated rather than being ignored. | |||||
| # The mode is 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. | |||||
| # Each holds a different value under the role's default mode, so together they prove 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 | |||||
| # homeserver reads, and it is generated by the role rather than by the | |||||
| # bridge, so it is worth checking on its own. | |||||
| # The role generates the registration; the bridge only consumes it. Worth checking on its | |||||
| # own, as it is the half of the handshake the homeserver reads. | |||||
| - 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 | |||||
| # them as strings - which would only re-derive the role's own escaping - | |||||
| # they are checked by what they do: cover the bot, cover this mode's ghosts, | |||||
| # and not cover the ghosts of the mode the role would have defaulted to. | |||||
| # The namespaces are regexes the role assembles. Comparing them as strings would only | |||||
| # re-derive the role's own escaping, so they are checked by what they match: the bot, | |||||
| # this mode's ghosts, and not 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 | |||||
| # the uid the role runs the container as. | |||||
| # Cheap proof that the data path reached the process and is writable by the uid the role | |||||
| # 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 | |||||
| # Instagram's tags carrying an `ig-` prefix, so the tag is compared whole | |||||
| # rather than by substring: an `ig-` prefix would mean this role pulled the | |||||
| # other bridge's image. | |||||
| # Both Meta bridges publish to the same image repository, with Instagram's tags carrying | |||||
| # an `ig-` prefix. Compared whole rather than by substring, so pulling the other bridge's | |||||
| # image would fail here. | |||||
| - 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, | |||||
| # not what a reverse-proxy would do with it. The port matters: it is | |||||
| # hardcoded in the role's templates rather than derived from a variable, so | |||||
| # nothing else in this scenario would catch it drifting from the port the | |||||
| # bridge actually listens on. | |||||
| # Traefik is not running here, so this checks what the role wrote, not what a reverse | |||||
| # proxy would do with it. The port matters: it is hardcoded in the role's templates rather | |||||
| # than derived from a variable, so nothing else here would catch it drifting from the port | |||||
| # the 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 | |||||
| # while starting; it is not a real homeserver and nothing is asserted | |||||
| # The stub prepare.yml stands up. 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 | |||||
| # database hostname when the engine is postgres, and testing which | |||||
| # database engine the bridge can talk to is not what this proves. | |||||
| # sqlite keeps the scenario to one container. Which 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 | |||||
| # authenticate to each other with; here they only have to reach the | |||||
| # rendered configuration and the registration file. | |||||
| # Here these only have to reach the rendered configuration and the registration. | |||||
| 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 | |||||
| # tell what the role rendered apart from what the bridge would have | |||||
| # defaulted to on its own. | |||||
| # Different from the role's defaults, so verify.yml can tell what the role rendered | |||||
| # apart from what the bridge would have chosen. | |||||
| 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 | |||||
| # it are switched off and their absence is asserted instead. | |||||
| # Traefik is not deployed here, so the labels the role would render for it are | |||||
| # 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, | |||||
| # so the paths it reads are pinned here as literals matching what the | |||||
| # role derives from matrix_base_data_path. | |||||
| # verify.yml runs as its own play, where the role's defaults are out of scope, | |||||
| # so the paths it reads are pinned here to match what the role derives. | |||||
| 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 | |||||
| # through the passwd database, so they have to exist first. matrix-base | |||||
| # creates them in a real deployment. | |||||
| # The role's file tasks set owner/group by name, which Ansible resolves through the | |||||
| # passwd database, so they have to exist first. `matrix-base` creates them for real. | |||||
| - 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 | |||||
| # bridge anything - see molecule-shared/homeserver-stub.py. | |||||
| # The bridge contacts the homeserver as it starts. It is not being asked to bridge | |||||
| # 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 | |||||
| # the role rendered, opens its appservice port, and is the version the role | |||||
| # 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. | |||||
| # Proves the bridge starts, reads the configuration and registration the role rendered, opens | |||||
| # its appservice port, and is the version the role pins. | |||||
| # | |||||
| # 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 | |||||
| # the version assertion below compares the running image against what the | |||||
| # role ships instead of against the scenario itself. | |||||
| # From the role's own defaults rather than pinned in molecule.yml, so the version | |||||
| # assertion compares the running image against what the role ships, not the scenario. | |||||
| - 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 | |||||
| # read still reports `active`, so the restart counter is checked too. It is | |||||
| # asserted `is defined` because `| int` turns a missing property into 0 and | |||||
| # would pass vacuously. | |||||
| # `Restart=always` means a bridge crash-looping on unreadable config still reports | |||||
| # `active`, so the restart counter is checked too. Asserted `is defined` because | |||||
| # `| int` turns a missing property into 0 and 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 | |||||
| # would push transactions to. It opening at all means the bridge got through | |||||
| # reading its configuration and setting itself up. | |||||
| # The appservice listener is where a homeserver would push transactions. It opening at all | |||||
| # means the bridge got through 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 | |||||
| # presence means the role's configuration is what the bridge is running on | |||||
| # rather than something that happened to agree with it. | |||||
| # Each differs from what the bridge would use on its own, so their presence rules out | |||||
| # 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: | ||||
| @@ -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 | |||||
| # homeserver reads, and it is generated by the role rather than by the | |||||
| # bridge, so it is worth checking on its own. | |||||
| # The role generates the registration; the bridge only consumes it. Worth checking on its | |||||
| # own, as it is the half of the handshake the homeserver reads. | |||||
| - 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 | |||||
| # database under the role's data path. This is the cheap proof that the data | |||||
| # path reached the process and is writable by the uid the role runs it as. | |||||
| # Cheap proof that the data 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" | ||||