Files
matrix-docker-ansible-deploy/roles/custom/matrix-bridge-mautrix-whatsapp/molecule/default/verify.yml
T
Slavi PantaleevandClaude Opus 5 85f80a3c7e Test the Molecule scenarios against Postgres rather than sqlite
`group_vars/matrix_servers` selects postgres whenever postgres is enabled, which
is the default, so postgres is what essentially every deployment runs. The
scenarios were testing sqlite - a path almost nobody is on.

How little that path is used is not a guess: the mautrix-meta bridges could not
start at all under sqlite, and nobody reported it. Testing the engine users are
actually on is worth more than keeping coverage of the one they are not, so no
scenario is left behind on sqlite.

Four of the eight scenarios have a database and are converted; the other four
have none and are untouched.

molecule-shared/tasks/postgres.yml stands Postgres up on the scenario's network,
with the data directory on a tmpfs since it is thrown away with the container.
The image is pinned at the major the postgres role deploys to new installations
and left to Renovate: when a new major lands, the PR bumping that pin runs every
scenario against it, which is the earliest warning we get that a component does
not cope.

Each scenario gives its database and user names that differ from the role's
defaults, so the component reaching the database proves the role built its
connection string out of them. The assertions moved from "a file appeared at the
path we configured" to "these tables exist", which is strictly stronger: tables
can only appear once the component has resolved the hostname, authenticated with
the credentials the role rendered, and run its migrations to completion.

Costs about 10 seconds per affected scenario (115s to 125s locally for
mautrix-whatsapp), on jobs that run in parallel.

Gotcha worth recording: since Postgres 18 the image puts PGDATA in a versioned
subdirectory and refuses to start if it finds a mount at the old
/var/lib/postgresql/data, so the tmpfs is mounted at /var/lib/postgresql.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SEH3vxYSQ5SV4N5z61eyGT
2026-08-27 18:02:53 +03:00

187 lines
8.5 KiB
YAML

# SPDX-FileCopyrightText: 2026 Slavi Pantaleev
#
# SPDX-License-Identifier: AGPL-3.0-or-later
---
# 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
hosts: all
become: true
vars_files:
- "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/vars.yml"
- "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/playbook-context.yml"
gather_facts: false
tasks:
# 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
ansible.builtin.include_vars:
file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults/main.yml"
name: mautrix_whatsapp_role_defaults
- name: Wait for the mautrix-whatsapp service to become active
ansible.builtin.systemd_service:
name: matrix-mautrix-whatsapp.service
register: mautrix_whatsapp_service
until: mautrix_whatsapp_service.status.ActiveState == 'active'
retries: 30
delay: 5
failed_when: false
# `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
ansible.builtin.assert:
that:
- mautrix_whatsapp_service.status.ActiveState == 'active'
- mautrix_whatsapp_service.status.NRestarts is defined
- mautrix_whatsapp_service.status.NRestarts | int == 0
fail_msg: >-
matrix-mautrix-whatsapp.service is
{{ mautrix_whatsapp_service.status.ActiveState | default('unknown') }}
after {{ mautrix_whatsapp_service.status.NRestarts | default('?') }}
automatic restart(s)
success_msg: "matrix-mautrix-whatsapp.service is active and has not restarted"
# 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
ansible.builtin.command:
argv:
- docker
- run
- --rm
- --network={{ matrix_bridge_mautrix_whatsapp_container_network }}
- "{{ molecule_shared_image_curl }}"
- --silent
- --output
- /dev/null
- --write-out
- "HTTP_STATUS=%{http_code}"
- "http://matrix-mautrix-whatsapp:8080/_matrix/mau/live"
register: mautrix_whatsapp_live
changed_when: false
until: "'HTTP_STATUS=000' not in mautrix_whatsapp_live.stdout"
retries: 24
delay: 5
failed_when: false
- name: Assert the bridge answers on its appservice port
ansible.builtin.assert:
that:
- "'HTTP_STATUS=000' not in mautrix_whatsapp_live.stdout"
fail_msg: >-
The bridge did not answer on its appservice port
({{ mautrix_whatsapp_live.stdout | default('no output') }})
success_msg: "The bridge answers on its appservice port"
- name: Read the configuration the role rendered
ansible.builtin.slurp:
src: "{{ matrix_bridge_mautrix_whatsapp_config_path }}/config.yaml"
register: mautrix_whatsapp_config_file
# 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
ansible.builtin.assert:
that:
- matrix_bridge_mautrix_whatsapp_homeserver_address in mautrix_whatsapp_config_rendered
- matrix_bridge_mautrix_whatsapp_appservice_bot_username in mautrix_whatsapp_config_rendered
- matrix_bridge_mautrix_whatsapp_appservice_token in mautrix_whatsapp_config_rendered
fail_msg: "The rendered configuration does not carry the scenario's values"
success_msg: "The rendered configuration carries the scenario's values"
vars:
mautrix_whatsapp_config_rendered: "{{ mautrix_whatsapp_config_file.content | b64decode }}"
# 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
ansible.builtin.slurp:
src: "{{ matrix_bridge_mautrix_whatsapp_config_path }}/registration.yaml"
register: mautrix_whatsapp_registration_file
- name: Assert the registration carries the scenario's tokens and bot user
ansible.builtin.assert:
that:
- matrix_bridge_mautrix_whatsapp_appservice_token in mautrix_whatsapp_registration_rendered
- matrix_bridge_mautrix_whatsapp_homeserver_token in mautrix_whatsapp_registration_rendered
- matrix_bridge_mautrix_whatsapp_appservice_bot_username in mautrix_whatsapp_registration_rendered
fail_msg: "The appservice registration does not carry the scenario's tokens and bot user"
success_msg: "The appservice registration carries the scenario's tokens and bot user"
vars:
mautrix_whatsapp_registration_rendered: "{{ mautrix_whatsapp_registration_file.content | b64decode }}"
# Stronger than the file-on-disk check sqlite allowed: the bridge can only have created
# tables here by resolving the hostname, authenticating with the credentials the role
# rendered, and running its migrations to completion.
- name: List the tables the bridge created in Postgres
ansible.builtin.command:
argv:
- docker
- exec
- matrix-postgres-molecule
- psql
- --username={{ matrix_bridge_mautrix_whatsapp_database_username }}
- --dbname={{ matrix_bridge_mautrix_whatsapp_database_name }}
- --tuples-only
- --no-align
- --command=SELECT tablename FROM pg_tables WHERE schemaname = 'public'
register: mautrix_whatsapp_tables
changed_when: false
- name: Assert the bridge migrated its schema into the database the role pointed it at
ansible.builtin.assert:
that:
- mautrix_whatsapp_tables.rc == 0
- "'version' in mautrix_whatsapp_table_names"
- mautrix_whatsapp_table_names | length > 5
fail_msg: >-
The bridge did not create its schema in
{{ matrix_bridge_mautrix_whatsapp_database_name }}
(found {{ mautrix_whatsapp_table_names | length }} table(s))
success_msg: "The bridge migrated its schema into the database the role pointed it at"
vars:
mautrix_whatsapp_table_names: "{{ mautrix_whatsapp_tables.stdout_lines | select | list }}"
- name: Read the image of the running container
ansible.builtin.command:
argv:
- docker
- container
- inspect
- matrix-mautrix-whatsapp
- --format
- "{{ '{{' }} .Config.Image {{ '}}' }}"
register: mautrix_whatsapp_image
changed_when: false
- name: Assert the running container is the version defaults/main.yml pins
ansible.builtin.assert:
that:
- mautrix_whatsapp_role_defaults.matrix_bridge_mautrix_whatsapp_version in mautrix_whatsapp_image.stdout
fail_msg: >-
The running container is {{ mautrix_whatsapp_image.stdout }}, which does
not carry the pinned version
{{ mautrix_whatsapp_role_defaults.matrix_bridge_mautrix_whatsapp_version }}
success_msg: "The running container is the version defaults/main.yml pins"
- name: Read the labels the role rendered
ansible.builtin.slurp:
src: "{{ matrix_bridge_mautrix_whatsapp_base_path }}/labels"
register: mautrix_whatsapp_labels
- name: Assert no Traefik labels are emitted while Traefik support is disabled
ansible.builtin.assert:
that:
- "'traefik.' not in (mautrix_whatsapp_labels.content | b64decode)"
fail_msg: >-
Traefik labels were emitted even though
matrix_bridge_mautrix_whatsapp_container_labels_traefik_enabled is false
success_msg: "No Traefik labels are emitted while Traefik support is disabled"