# SPDX-FileCopyrightText: 2026 Slavi Pantaleev # # SPDX-License-Identifier: AGPL-3.0-or-later --- # Proves a single main Synapse process starts on the non-default listener the role # rendered, serves real Matrix APIs, migrates the configured Postgres database, and # carries no worker, Redis, S3, MAS, email, or other optional-integration topology. - name: Verify Synapse 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 vars: matrix_synapse_config: "{{ matrix_synapse_config_file.content | b64decode | from_yaml }}" matrix_synapse_http_listeners: >- {{ matrix_synapse_config.listeners | selectattr('type', 'equalto', 'http') | list }} matrix_synapse_http_listener: "{{ matrix_synapse_http_listeners | first | default({}) }}" tasks: # The image version comes from the role, not from the scenario, so an assertion cannot # remain green merely because both the scenario and runtime are pinned to the same typo. - name: Load the role's defaults under a separate name ansible.builtin.include_vars: file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults/main.yml" name: matrix_synapse_role_defaults - name: Wait for the Synapse service to become active ansible.builtin.systemd_service: name: matrix-synapse.service register: matrix_synapse_service until: matrix_synapse_service.status.ActiveState == 'active' retries: 30 delay: 5 failed_when: false - name: Assert the Synapse service is stable ansible.builtin.assert: that: - matrix_synapse_service.status.ActiveState == 'active' - matrix_synapse_service.status.NRestarts is defined - matrix_synapse_service.status.NRestarts | int == 0 fail_msg: >- matrix-synapse.service is {{ matrix_synapse_service.status.ActiveState | default('unknown') }} after {{ matrix_synapse_service.status.NRestarts | default('?') }} restart(s) success_msg: "matrix-synapse.service is active and has not restarted" # Probe over the role's own network. No host port is published, matching deployment. - name: Request the Synapse health endpoint ansible.builtin.command: argv: - docker - run - --rm - --network={{ matrix_synapse_container_network }} - "{{ molecule_shared_image_curl }}" - --silent - --show-error - --write-out - "\nHTTP_STATUS=%{http_code}" - http://matrix-synapse:{{ matrix_synapse_container_client_api_port }}/health register: matrix_synapse_health changed_when: false until: "'HTTP_STATUS=200' in matrix_synapse_health.stdout" retries: 24 delay: 5 failed_when: false - name: Request the Synapse Matrix versions endpoint ansible.builtin.command: argv: - docker - run - --rm - --network={{ matrix_synapse_container_network }} - "{{ molecule_shared_image_curl }}" - --silent - --show-error - --write-out - "\nHTTP_STATUS=%{http_code}" - http://matrix-synapse:{{ matrix_synapse_container_client_api_port }}/_matrix/client/versions register: matrix_synapse_versions_response changed_when: false until: "'HTTP_STATUS=200' in matrix_synapse_versions_response.stdout" retries: 24 delay: 5 failed_when: false - name: Assert Synapse serves its health endpoint ansible.builtin.assert: that: - matrix_synapse_health.rc == 0 - "'HTTP_STATUS=200' in matrix_synapse_health.stdout" - matrix_synapse_health.stdout.startswith('OK') fail_msg: >- Synapse did not serve /health on port {{ matrix_synapse_container_client_api_port }} success_msg: "Synapse serves /health" - name: Assert Synapse serves its Matrix versions endpoint ansible.builtin.assert: that: - matrix_synapse_versions_response.rc == 0 - "'HTTP_STATUS=200' in matrix_synapse_versions_response.stdout" fail_msg: >- Synapse did not serve /_matrix/client/versions on port {{ matrix_synapse_container_client_api_port }} success_msg: "Synapse serves /_matrix/client/versions" - name: Parse the Matrix versions response ansible.builtin.set_fact: matrix_synapse_versions: >- {{ matrix_synapse_versions_response.stdout | regex_replace('\nHTTP_STATUS=200$', '') | from_json }} - name: Assert the versions response is a real Matrix API document ansible.builtin.assert: that: - matrix_synapse_versions.versions is sequence - matrix_synapse_versions.versions | length > 0 - "'v1.1' in matrix_synapse_versions.versions" - matrix_synapse_versions.unstable_features is mapping fail_msg: "The versions endpoint did not return a Matrix versions document" success_msg: "The versions endpoint returns a Matrix versions document" # The role moved the listener away from 8008. Refusing the upstream default port proves # the successful requests above did not accidentally hit an unchanged default listener. - name: Request the upstream-default Synapse port ansible.builtin.command: argv: - docker - run - --rm - --network={{ matrix_synapse_container_network }} - "{{ molecule_shared_image_curl }}" - --silent - --output - /dev/null - --connect-timeout - '2' - --max-time - '3' - http://matrix-synapse:8008/health register: matrix_synapse_default_listener changed_when: false failed_when: false - name: Assert Synapse does not listen on the upstream-default port ansible.builtin.assert: that: - matrix_synapse_default_listener.rc != 0 fail_msg: "Synapse still answers on port 8008, so the non-default listener is not proven" success_msg: "Only the role-configured client listener answers" - name: Read the homeserver configuration the role rendered ansible.builtin.slurp: src: "{{ matrix_synapse_config_dir_path }}/homeserver.yaml" register: matrix_synapse_config_file - name: Read the homeserver configuration identity ansible.builtin.stat: path: "{{ matrix_synapse_config_dir_path }}/homeserver.yaml" register: matrix_synapse_config_stat - name: Assert the parsed homeserver identity and behavior ansible.builtin.assert: that: - matrix_synapse_config.server_name == matrix_domain - matrix_synapse_config.public_baseurl == matrix_synapse_public_baseurl - matrix_synapse_config.max_upload_size == (matrix_synapse_max_upload_size_mb | string) + 'M' - not matrix_synapse_config.presence.enabled - not matrix_synapse_config.url_preview_enabled - not matrix_synapse_config.allow_public_rooms_over_federation - matrix_synapse_config.user_directory.search_all_users - matrix_synapse_config.user_directory.prefer_local_users - matrix_synapse_config.trusted_key_servers == [] fail_msg: "The parsed homeserver configuration does not carry the scenario's values" success_msg: "The parsed homeserver configuration carries the scenario's values" - name: Assert the parsed homeserver secrets ansible.builtin.assert: that: - matrix_synapse_config.macaroon_secret_key == matrix_synapse_macaroon_secret_key - matrix_synapse_config.registration_shared_secret == matrix_synapse_registration_shared_secret - matrix_synapse_config.form_secret == matrix_synapse_form_secret - matrix_synapse_config.password_config.pepper == matrix_synapse_password_config_pepper fail_msg: "The parsed homeserver configuration does not carry the scenario's secrets" success_msg: "The parsed homeserver configuration carries the scenario's secrets" no_log: true - name: Assert the parsed client listener topology ansible.builtin.assert: that: - matrix_synapse_config.listeners | length == 1 - matrix_synapse_http_listeners | length == 1 - matrix_synapse_http_listener.port == matrix_synapse_container_client_api_port - not matrix_synapse_http_listener.tls - matrix_synapse_http_listener.bind_addresses == ['::'] - matrix_synapse_http_listener.resources | length == 1 - matrix_synapse_http_listener.resources[0].names == ['client'] fail_msg: "The parsed homeserver configuration has an unexpected listener topology" success_msg: "The parsed homeserver configuration has one non-default client listener" - name: Assert the parsed Postgres configuration ansible.builtin.assert: that: - matrix_synapse_config.database.name == 'psycopg2' - matrix_synapse_config.database.args.host == matrix_synapse_database_host - matrix_synapse_config.database.args.port == matrix_synapse_database_port - matrix_synapse_config.database.args.user == matrix_synapse_database_user - matrix_synapse_config.database.args.password == matrix_synapse_database_password - matrix_synapse_config.database.args.database == matrix_synapse_database_database - matrix_synapse_config.database.args.cp_min == matrix_synapse_database_cp_min - matrix_synapse_config.database.args.cp_max == matrix_synapse_database_cp_max fail_msg: "The parsed homeserver configuration does not point at scenario Postgres" success_msg: "The parsed homeserver configuration points at scenario Postgres" no_log: true - name: Assert optional Synapse topology is absent ansible.builtin.assert: that: - not matrix_synapse_config.redis.enabled - matrix_synapse_config.media_storage_providers == [] - matrix_synapse_config.modules == [] - "'matrix_authentication_service' not in matrix_synapse_config" - "'email' not in matrix_synapse_config" - "'worker_app' not in matrix_synapse_config" fail_msg: "An excluded worker, Redis, S3, MAS, email, or module integration is configured" success_msg: "The configuration contains only the intended main-process topology" - name: Assert the homeserver configuration file identity ansible.builtin.assert: that: - matrix_synapse_config_stat.stat.uid | int == matrix_user_uid | int - matrix_synapse_config_stat.stat.gid | int == matrix_user_gid | int - matrix_synapse_config_stat.stat.mode == '0644' fail_msg: "homeserver.yaml has the wrong ownership or mode" success_msg: "homeserver.yaml has the playbook identity and expected mode" # These tables appear only after hostname resolution, authentication, and genuine Synapse # migrations against the exact non-default database prepared for the scenario. - name: List the tables Synapse created in Postgres ansible.builtin.command: argv: - docker - exec - matrix-postgres-molecule - psql - --username={{ matrix_synapse_database_user }} - --dbname={{ matrix_synapse_database_database }} - --tuples-only - --no-align - --command=SELECT tablename FROM pg_tables WHERE schemaname = 'public' register: matrix_synapse_tables changed_when: false - name: Assert Synapse migrated its Postgres schema ansible.builtin.assert: that: - matrix_synapse_tables.rc == 0 - "'schema_version' in matrix_synapse_table_names" - "'users' in matrix_synapse_table_names" - "'rooms' in matrix_synapse_table_names" - "'events' in matrix_synapse_table_names" - "'state_groups' in matrix_synapse_table_names" - matrix_synapse_table_names | length > 50 fail_msg: >- Synapse did not migrate its schema in {{ matrix_synapse_database_database }} (found {{ matrix_synapse_table_names | length }} table(s)) success_msg: "Synapse genuinely migrated the configured Postgres database" vars: matrix_synapse_table_names: "{{ matrix_synapse_tables.stdout_lines | select | list }}" - name: Inspect the running Synapse container ansible.builtin.command: argv: - docker - container - inspect - matrix-synapse register: matrix_synapse_container_inspect changed_when: false - name: Parse the running Synapse container inspection ansible.builtin.set_fact: matrix_synapse_container: "{{ (matrix_synapse_container_inspect.stdout | from_json) | first }}" - name: Assert the running container uses the exact pinned image ansible.builtin.assert: that: - matrix_synapse_container.Config.Image == 'ghcr.io/element-hq/synapse:' + matrix_synapse_role_defaults.matrix_synapse_version fail_msg: "The running container does not use the exact image defaults/main.yml pins" success_msg: "The running container uses the exact image defaults/main.yml pins" - name: Assert the running container uses the scenario identity ansible.builtin.assert: that: - matrix_synapse_container.Config.User == (matrix_user_uid | string) + ':' + (matrix_user_gid | string) fail_msg: "The running container does not use the playbook-supplied identity" success_msg: "The running container uses the playbook-supplied identity" - name: Assert the running container security isolation ansible.builtin.assert: that: - matrix_synapse_container.HostConfig.ReadonlyRootfs - "'ALL' in matrix_synapse_container.HostConfig.CapDrop" fail_msg: "The running container does not have read-only, capability-dropped isolation" success_msg: "The running container has read-only, capability-dropped isolation" - name: Assert the running container has a read-only config mount ansible.builtin.assert: that: - matrix_synapse_config_mount | length > 0 - not matrix_synapse_config_mount.RW | default(true) fail_msg: "The running container does not mount its configuration read-only" success_msg: "The running container mounts its configuration read-only" vars: matrix_synapse_config_mount: >- {{ matrix_synapse_container.Mounts | selectattr('Destination', 'equalto', '/data') | first | default({}) }} - name: Assert the running container has a writable storage mount ansible.builtin.assert: that: - matrix_synapse_storage_mount | length > 0 - matrix_synapse_storage_mount.RW | default(false) fail_msg: "The running container does not mount its media storage writable" success_msg: "The running container mounts its media storage writable" vars: matrix_synapse_storage_mount: >- {{ matrix_synapse_container.Mounts | selectattr('Destination', 'equalto', '/matrix-media-store-parent') | first | default({}) }} - name: Assert the running container is healthy ansible.builtin.assert: that: - matrix_synapse_container.State.Health.Status == 'healthy' fail_msg: "The running container is not healthy" success_msg: "The running container is healthy" - name: Assert the running container has only its private network ansible.builtin.assert: that: - matrix_synapse_container_network in matrix_synapse_container.NetworkSettings.Networks - matrix_synapse_container.NetworkSettings.Networks | length == 1 fail_msg: "The running container has unexpected network attachments" success_msg: "The running container has only its private network" - name: Assert the running container publishes no host ports ansible.builtin.assert: that: - matrix_synapse_container.HostConfig.PortBindings | default({}, true) | length == 0 fail_msg: "The running container unexpectedly publishes a host port" success_msg: "The running container publishes no host ports"