From d534b00e04f225f8dd0b97a93538b4d1b2253d19 Mon Sep 17 00:00:00 2001 From: Slavi Pantaleev Date: Thu, 27 Aug 2026 16:30:01 +0300 Subject: [PATCH] Add a Molecule scenario for mautrix-meta-messenger Stands the bridge up against the shared homeserver stub and checks that it starts on the configuration the role rendered: the unit is active with no automatic restarts, the appservice listener on 29319 answers, and the bridge created its sqlite database under the role's data path as the role's uid. Nothing is bridged - there is no Facebook or Messenger account involved and there is deliberately never going to be one. The thing worth proving for this role in particular is the Meta mode. One upstream codebase serves several Meta networks, and `matrix_bridge_mautrix_meta_messenger_meta_mode` is what picks which one; the role expands it into an appservice id, a ghost username prefix, a bot displayname and the bridge's `tor` switch. The scenario therefore runs in `facebook-tor` mode rather than the role's default `messenger`, and asserts on all four - against the parsed configuration, not substrings. The registration namespaces are checked by what they match rather than by comparing regexes: this mode's ghosts are covered, the default mode's are not. Since v26.07 the Instagram bridge is published to the same image repository with an `ig-` tag prefix, so the running image is compared against the whole tag `defaults/main.yml` pins rather than by substring - an `ig-` prefix would mean this role pulled the other bridge's image. Surprising: the role's derived sqlite URI does not work. For `sqlite3-fk-wal` it builds `sqlite:///` + the in-container path, and the bridge hands that to go-sqlite3 as a plain filename rather than parsing it as a URL, so it dies at startup with `unable to open database file: no such file or directory`. Every other mautrix role here uses a bare path. The playbook selects postgres whenever postgres is enabled, which is the default, so nothing normally reaches that code path. The scenario overrides the URI rather than changing the role's default, which is consumer-visible; matrix-bridge-mautrix-meta-instagram has the same expression. Falsified by setting `matrix_bridge_mautrix_meta_messenger_meta_mode` back to `messenger` and re-running: the run failed at "Assert the configuration reflects the Meta mode the scenario selected", on the `appservice.id == 'facebook-tor'` clause. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01SEH3vxYSQ5SV4N5z61eyGT --- .../molecule/default/converge.yml | 36 +++ .../molecule/default/molecule.yml | 119 ++++++++ .../molecule/default/prepare.yml | 86 ++++++ .../molecule/default/requirements.yml | 1 + .../molecule/default/verify.yml | 280 ++++++++++++++++++ 5 files changed, 522 insertions(+) create mode 100644 roles/custom/matrix-bridge-mautrix-meta-messenger/molecule/default/converge.yml create mode 100644 roles/custom/matrix-bridge-mautrix-meta-messenger/molecule/default/molecule.yml create mode 100644 roles/custom/matrix-bridge-mautrix-meta-messenger/molecule/default/prepare.yml create mode 120000 roles/custom/matrix-bridge-mautrix-meta-messenger/molecule/default/requirements.yml create mode 100644 roles/custom/matrix-bridge-mautrix-meta-messenger/molecule/default/verify.yml diff --git a/roles/custom/matrix-bridge-mautrix-meta-messenger/molecule/default/converge.yml b/roles/custom/matrix-bridge-mautrix-meta-messenger/molecule/default/converge.yml new file mode 100644 index 000000000..629f4d392 --- /dev/null +++ b/roles/custom/matrix-bridge-mautrix-meta-messenger/molecule/default/converge.yml @@ -0,0 +1,36 @@ +# SPDX-FileCopyrightText: 2026 Slavi Pantaleev +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +--- +- name: Include roles for mautrix-meta-messenger Molecule tests + hosts: all + become: true + vars_files: + - "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/playbook-context.yml" + gather_facts: true + tasks: + - name: Include roles for mautrix-meta-messenger Molecule tests + ansible.builtin.include_role: + name: "{{ role_name }}" + public: true + loop: + - com.devture.ansible.role.playbook_help + - com.devture.ansible.role.systemd_docker_base + - "custom/{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" + loop_control: + loop_var: role_name + +- name: Ensure mautrix-meta-messenger is started + hosts: all + become: true + gather_facts: false + tasks: + - name: Ensure systemd daemon is reloaded + ansible.builtin.systemd_service: + daemon_reload: true + + - name: Ensure the mautrix-meta-messenger systemd service is started + ansible.builtin.systemd_service: + name: matrix-mautrix-meta-messenger.service + state: started diff --git a/roles/custom/matrix-bridge-mautrix-meta-messenger/molecule/default/molecule.yml b/roles/custom/matrix-bridge-mautrix-meta-messenger/molecule/default/molecule.yml new file mode 100644 index 000000000..97e38de8a --- /dev/null +++ b/roles/custom/matrix-bridge-mautrix-meta-messenger/molecule/default/molecule.yml @@ -0,0 +1,119 @@ +# SPDX-FileCopyrightText: 2026 Slavi Pantaleev +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +--- +dependency: + name: galaxy + options: + requirements-file: requirements.yml + force: true +driver: + name: docker +platforms: + - name: mautrix-meta-messenger-${MOLECULE_DISTRO:-ubuntu2604}-default + image: "geerlingguy/docker-${MOLECULE_DISTRO:-ubuntu2604}-ansible:latest" + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:rw + cgroupns_mode: host + privileged: true + pre_build_image: true +provisioner: + name: ansible + config_options: + defaults: + callback_result_format: yaml + inventory: + group_vars: + all: + 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 + # about it. + matrix_bridge_mautrix_meta_messenger_homeserver_address: http://matrix.molecule.local:8008 + 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. + matrix_bridge_mautrix_meta_messenger_database_engine: sqlite3-fk-wal + + # The URI is overridden because the one the role derives for + # `sqlite3-fk-wal` does not work: it builds `sqlite:///` + the + # in-container path, and the bridge hands that to go-sqlite3 as a plain + # filename rather than parsing it as a URL, so it dies at startup with + # `unable to open database file: no such file or directory`. The value + # below is the shape the other mautrix roles here use (a bare path; + # compare matrix-bridge-mautrix-signal, which adds the same _txlock the + # bridge warns about when it is missing). + # + # The playbook picks postgres whenever postgres is enabled, which is the + # default, so nothing normally reaches this code path. Left as an + # override rather than a fix: that is a consumer-visible default. + matrix_bridge_mautrix_meta_messenger_appservice_database_uri: "/data/mautrix-meta.db?_txlock=immediate" + + # 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. + matrix_bridge_mautrix_meta_messenger_appservice_token: molecule_meta_as_token_5c81de + 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. + # + # 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. + 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. + 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_displayname_suffix: "(Molecule)" + 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 + # `appservice.public_address` the bridge itself reads. + matrix_bridge_mautrix_meta_messenger_exposure_enabled: true + 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_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. + 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_data_path: /matrix/mautrix-meta-messenger/data + env: + # Workaround for https://github.com/ansible/molecule/issues/4391 + ANSIBLE_ROLES_PATH: ${MOLECULE_PROJECT_DIRECTORY}/../..:/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles:~/.ansible/roles +scenario: + test_sequence: + - dependency + - cleanup + - destroy + - syntax + - create + - prepare + - converge + - idempotence + - verify + - cleanup + - destroy +verifier: + name: ansible diff --git a/roles/custom/matrix-bridge-mautrix-meta-messenger/molecule/default/prepare.yml b/roles/custom/matrix-bridge-mautrix-meta-messenger/molecule/default/prepare.yml new file mode 100644 index 000000000..c50c68e5c --- /dev/null +++ b/roles/custom/matrix-bridge-mautrix-meta-messenger/molecule/default/prepare.yml @@ -0,0 +1,86 @@ +# SPDX-FileCopyrightText: 2026 Slavi Pantaleev +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +--- +- name: Prepare mautrix-meta-messenger Molecule tests + 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: true + tasks: + - name: Ensure apt cache is updated + ansible.builtin.apt: + update_cache: true + cache_valid_time: 600 + when: ansible_os_family == 'Debian' + + - name: Ensure required packages are installed + ansible.builtin.package: + name: + - python3-requests + - fuse-overlayfs + state: present + + - name: Ensure Docker is installed + ansible.builtin.include_role: + name: ansible-role-docker + vars: + docker_daemon_options: + 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. + - name: Ensure the matrix group exists + ansible.builtin.group: + name: "{{ matrix_group_name }}" + gid: "{{ matrix_user_gid }}" + state: present + + - name: Ensure the matrix user exists + ansible.builtin.user: + name: "{{ matrix_user_name }}" + uid: "{{ matrix_user_uid }}" + group: "{{ matrix_group_name }}" + create_home: false + system: true + state: present + + - name: Ensure the base data path exists + ansible.builtin.file: + path: "{{ matrix_base_data_path }}" + state: directory + owner: "{{ matrix_user_name }}" + group: "{{ matrix_group_name }}" + mode: "0750" + + # The role creates this network itself during converge, but the homeserver + # stub below has to be attached to it before that, so it is created here. + - name: Ensure the container network the role attaches to exists + ansible.builtin.command: + argv: + - docker + - network + - create + - "{{ matrix_bridge_mautrix_meta_messenger_container_network }}" + register: mautrix_meta_messenger_molecule_network + changed_when: mautrix_meta_messenger_molecule_network.rc == 0 + failed_when: + - mautrix_meta_messenger_molecule_network.rc != 0 + - "'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. + - name: Ensure the homeserver stub is running + ansible.builtin.include_tasks: + file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/tasks/homeserver-stub.yml" + vars: + molecule_shared_stub_network: "{{ matrix_bridge_mautrix_meta_messenger_container_network }}" + molecule_shared_stub_server_name: "{{ matrix_bridge_mautrix_meta_messenger_homeserver_domain }}" + molecule_shared_stub_user_id: "@{{ matrix_bridge_mautrix_meta_messenger_appservice_username }}:{{ matrix_bridge_mautrix_meta_messenger_homeserver_domain }}" diff --git a/roles/custom/matrix-bridge-mautrix-meta-messenger/molecule/default/requirements.yml b/roles/custom/matrix-bridge-mautrix-meta-messenger/molecule/default/requirements.yml new file mode 120000 index 000000000..1567a5392 --- /dev/null +++ b/roles/custom/matrix-bridge-mautrix-meta-messenger/molecule/default/requirements.yml @@ -0,0 +1 @@ +../../../../../molecule-shared/requirements.yml \ No newline at end of file diff --git a/roles/custom/matrix-bridge-mautrix-meta-messenger/molecule/default/verify.yml b/roles/custom/matrix-bridge-mautrix-meta-messenger/molecule/default/verify.yml new file mode 100644 index 000000000..cd8cf1b79 --- /dev/null +++ b/roles/custom/matrix-bridge-mautrix-meta-messenger/molecule/default/verify.yml @@ -0,0 +1,280 @@ +# SPDX-FileCopyrightText: 2026 Slavi Pantaleev +# +# 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. +# +# 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. +# +# 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. +- name: Verify mautrix-meta-messenger + 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" + # 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 + # evaluate those as Jinja. + vars: + mautrix_meta_messenger_config: "{{ mautrix_meta_messenger_config_file.content | b64decode | from_yaml }}" + mautrix_meta_messenger_registration: "{{ mautrix_meta_messenger_registration_file.content | b64decode | from_yaml }}" + mautrix_meta_messenger_labels_rendered: "{{ mautrix_meta_messenger_labels.content | b64decode }}" + gather_facts: false + + 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. + - 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_meta_messenger_role_defaults + + - name: Wait for the mautrix-meta-messenger service to become active + ansible.builtin.systemd_service: + name: matrix-mautrix-meta-messenger.service + register: mautrix_meta_messenger_service + until: mautrix_meta_messenger_service.status.ActiveState == 'active' + retries: 30 + delay: 5 + 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. + - name: Assert the service is active and has not been restarting + ansible.builtin.assert: + that: + - mautrix_meta_messenger_service.status.ActiveState == 'active' + - mautrix_meta_messenger_service.status.NRestarts is defined + - mautrix_meta_messenger_service.status.NRestarts | int == 0 + fail_msg: >- + matrix-mautrix-meta-messenger.service is + {{ mautrix_meta_messenger_service.status.ActiveState | default('unknown') }} + after {{ mautrix_meta_messenger_service.status.NRestarts | default('?') }} + automatic restart(s) + 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. + - name: Wait for the bridge to open its appservice port + ansible.builtin.command: + argv: + - docker + - run + - --rm + - --network={{ matrix_bridge_mautrix_meta_messenger_container_network }} + - "{{ molecule_shared_image_curl }}" + - --silent + - --output + - /dev/null + - --write-out + - "HTTP_STATUS=%{http_code}" + - "http://matrix-mautrix-meta-messenger:29319/_matrix/mau/live" + register: mautrix_meta_messenger_live + changed_when: false + until: "'HTTP_STATUS=000' not in mautrix_meta_messenger_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_meta_messenger_live.stdout" + fail_msg: >- + The bridge did not answer on its appservice port + ({{ mautrix_meta_messenger_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_meta_messenger_config_path }}/config.yaml" + 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. + - name: Assert the rendered configuration carries this scenario's values + ansible.builtin.assert: + that: + - mautrix_meta_messenger_config.homeserver.address == matrix_bridge_mautrix_meta_messenger_homeserver_address + - mautrix_meta_messenger_config.homeserver.domain == matrix_bridge_mautrix_meta_messenger_homeserver_domain + - mautrix_meta_messenger_config.appservice.bot.username == matrix_bridge_mautrix_meta_messenger_appservice_username + - mautrix_meta_messenger_config.appservice.as_token == matrix_bridge_mautrix_meta_messenger_appservice_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.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. + - 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. + - mautrix_meta_messenger_config.encryption.allow == matrix_bridges_encryption_enabled + fail_msg: "The rendered configuration does not carry 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. + - name: Assert the configuration reflects the Meta mode the scenario selected + ansible.builtin.assert: + that: + - mautrix_meta_messenger_config.appservice.id == 'facebook-tor' + - mautrix_meta_messenger_config.appservice.username_template.startswith('facebook_') + - not mautrix_meta_messenger_config.appservice.username_template.startswith('messenger_') + - mautrix_meta_messenger_config.network.tor + - mautrix_meta_messenger_config.appservice.bot.displayname == 'Facebook-tor bridge bot' + - matrix_bridge_mautrix_meta_messenger_bridge_displayname_suffix in mautrix_meta_messenger_config.network.displayname_template + fail_msg: >- + The rendered configuration does not reflect + matrix_bridge_mautrix_meta_messenger_meta_mode=facebook-tor + success_msg: "The rendered configuration reflects the Meta mode the scenario selected" + + # sqlite3-fk-wal was chosen in molecule.yml. The URI is a scenario override + # rather than the role's derived value - see the comment in molecule.yml - + # so what this checks is only that both reach the configuration. The real + # proof that the bridge is using the role's data path is the database file + # asserted further down. + - name: Assert the configuration points the bridge at the sqlite database the scenario chose + ansible.builtin.assert: + that: + - mautrix_meta_messenger_config.database.type == matrix_bridge_mautrix_meta_messenger_database_engine + - mautrix_meta_messenger_config.database.uri == matrix_bridge_mautrix_meta_messenger_appservice_database_uri + fail_msg: "The rendered configuration does not point the bridge at the scenario's sqlite database" + success_msg: "The rendered configuration points the bridge at the scenario's sqlite database" + + # The public address is what the role builds out of the three exposure + # variables; it is the same endpoint the Traefik labels below route to. + - name: Assert the configuration carries the public address the exposure settings imply + ansible.builtin.assert: + that: + - mautrix_meta_messenger_config.appservice.public_address == 'https://bridges.molecule.local/bridges/meta-messenger' + fail_msg: >- + appservice.public_address is + {{ mautrix_meta_messenger_config.appservice.public_address | default('unset') }} + 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. + - name: Read the appservice registration the role rendered + ansible.builtin.slurp: + src: "{{ matrix_bridge_mautrix_meta_messenger_config_path }}/registration.yaml" + register: mautrix_meta_messenger_registration_file + + - name: Assert the registration carries the scenario's tokens, id and bot user + ansible.builtin.assert: + that: + - mautrix_meta_messenger_registration.as_token == matrix_bridge_mautrix_meta_messenger_appservice_token + - mautrix_meta_messenger_registration.hs_token == matrix_bridge_mautrix_meta_messenger_homeserver_token + - mautrix_meta_messenger_registration.id == 'facebook-tor' + - mautrix_meta_messenger_registration.sender_localpart == '_bot_' + matrix_bridge_mautrix_meta_messenger_appservice_username + - mautrix_meta_messenger_registration.url == 'http://matrix-mautrix-meta-messenger:29319' + 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" + + # 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. + - name: Assert the registration namespaces cover the bot and this mode's ghost users + ansible.builtin.assert: + that: + - mautrix_meta_messenger_ghost_regex | length > 0 + - mautrix_meta_messenger_bot_regex | length > 0 + - mautrix_meta_messenger_facebook_ghost_mxid is match(mautrix_meta_messenger_ghost_regex) + - mautrix_meta_messenger_messenger_ghost_mxid is not match(mautrix_meta_messenger_ghost_regex) + - mautrix_meta_messenger_bot_mxid is match(mautrix_meta_messenger_bot_regex) + fail_msg: "The appservice registration namespaces do not cover the bot and this mode's ghost users" + success_msg: "The appservice registration namespaces cover the bot and this mode's ghost users" + vars: + mautrix_meta_messenger_user_regexes: "{{ mautrix_meta_messenger_registration.namespaces.users | map(attribute='regex') | list }}" + mautrix_meta_messenger_ghost_regex: "{{ mautrix_meta_messenger_user_regexes | select('search', 'facebook_') | first | default('') }}" + mautrix_meta_messenger_bot_regex: "{{ mautrix_meta_messenger_user_regexes | reject('search', 'facebook_') | first | default('') }}" + mautrix_meta_messenger_facebook_ghost_mxid: "@facebook_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 }}" + + # The 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 + ansible.builtin.stat: + path: "{{ matrix_bridge_mautrix_meta_messenger_data_path }}/mautrix-meta.db" + register: mautrix_meta_messenger_database + + - name: Assert the bridge created its database where the role put its data path + ansible.builtin.assert: + that: + - mautrix_meta_messenger_database.stat.exists + - mautrix_meta_messenger_database.stat.uid | int == matrix_user_uid | int + fail_msg: >- + The bridge did not create its database under + {{ matrix_bridge_mautrix_meta_messenger_data_path }}, or it is not owned by + uid {{ matrix_user_uid }} + success_msg: "The bridge created its database under the role's data path, as the role's uid" + + - name: Read the image of the running container + ansible.builtin.command: + argv: + - docker + - container + - inspect + - matrix-mautrix-meta-messenger + - --format + - "{{ '{{' }} .Config.Image {{ '}}' }}" + register: mautrix_meta_messenger_image + 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. + - name: Assert the running container is the Messenger image at the version defaults/main.yml pins + ansible.builtin.assert: + that: + - mautrix_meta_messenger_image.stdout.endswith(':' + mautrix_meta_messenger_role_defaults.matrix_bridge_mautrix_meta_messenger_version) + fail_msg: >- + The running container is {{ mautrix_meta_messenger_image.stdout }}, which is not + the Messenger image at the pinned version + {{ mautrix_meta_messenger_role_defaults.matrix_bridge_mautrix_meta_messenger_version }} + success_msg: "The running container is the Messenger image at the version defaults/main.yml pins" + + - name: Read the labels the role rendered + ansible.builtin.slurp: + src: "{{ matrix_bridge_mautrix_meta_messenger_base_path }}/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. + - name: Assert the labels route the exposure hostname and path prefix to the appservice port + ansible.builtin.assert: + that: + - "'traefik.enable=true' in mautrix_meta_messenger_labels_rendered" + - "'traefik.http.services.matrix-mautrix-meta-messenger-appservice.loadbalancer.server.port=29319' in mautrix_meta_messenger_labels_rendered" + - "'traefik.http.routers.matrix-mautrix-meta-messenger-exposure.rule=Host(`bridges.molecule.local`) && PathPrefix(`/bridges/meta-messenger`)' in mautrix_meta_messenger_labels_rendered" + - "'traefik.http.middlewares.matrix-mautrix-meta-messenger-exposure-strip-prefix.stripprefix.prefixes=/bridges/meta-messenger' in mautrix_meta_messenger_labels_rendered" + - "'traefik.docker.network=' + matrix_bridge_mautrix_meta_messenger_container_network in mautrix_meta_messenger_labels_rendered" + fail_msg: "The rendered labels do not route the exposure hostname and path prefix to the appservice port" + success_msg: "The rendered labels route the exposure hostname and path prefix to the appservice port"