├── .dir-locals.el ├── .dockerignore ├── .github ├── CODEOWNERS └── workflows │ ├── docker.yml │ └── pipeline.yml ├── .gitignore ├── CONTRIBUTING.rst ├── DEVELOP.rst ├── LICENSE ├── README.rst ├── STYLE-GUIDE.rst ├── cpanfile ├── docker ├── README.md ├── base.Dockerfile ├── bootstrap.sh ├── dendrite.Dockerfile └── synapse.Dockerfile ├── docs └── dendrite-setup.md ├── install-deps.pl ├── keys ├── ca.crt ├── ca.key ├── tls-selfsigned.crt ├── tls-selfsigned.key └── tls.dh ├── lib ├── Protocol │ ├── Matrix.pm │ └── Matrix │ │ └── HTTP │ │ └── Federation.pm └── SyTest │ ├── ApplicationService.pm │ ├── Assertions.pm │ ├── CarpByFile.pm │ ├── Crypto.pm │ ├── Federation │ ├── AuthChecks.pm │ ├── Client.pm │ ├── Datastore.pm │ ├── Protocol.pm │ ├── Room.pm │ ├── Server.pm │ └── _Base.pm │ ├── HTTPClient.pm │ ├── HTTPServer │ └── Request.pm │ ├── Homeserver.pm │ ├── Homeserver │ ├── Dendrite.pm │ ├── Manual.pm │ ├── ProcessManager.pm │ └── Synapse.pm │ ├── HomeserverFactory.pm │ ├── HomeserverFactory │ ├── Dendrite.pm │ ├── Manual.pm │ └── Synapse.pm │ ├── Identity │ └── Server.pm │ ├── JSONSensible.pm │ ├── MailServer.pm │ ├── MailServer │ └── Protocol.pm │ ├── Output │ ├── TAP.pm │ └── Term.pm │ ├── SSL.pm │ └── TCPProxy.pm ├── run-tests.pl ├── scripts ├── dendrite_sytest.sh ├── format_tap.pl ├── prep_sytest_for_postgres.sh ├── synapse_sytest.sh └── tap_to_gha.pl └── tests ├── 00expect_http_fail.pl ├── 01http-server.pl ├── 02as-info.pl ├── 04mail-server.pl ├── 05homeserver.pl ├── 06http-clients.pl ├── 07id-server.pl ├── 10apidoc ├── 00prepare.pl ├── 01register.pl ├── 01request-encoding.pl ├── 02login.pl ├── 03events-initial.pl ├── 04version.pl ├── 09synced.pl ├── 10profile-displayname.pl ├── 11profile-avatar_url.pl ├── 12device_management.pl ├── 13ui-auth.pl ├── 20presence.pl ├── 30room-create.pl ├── 31room-state.pl ├── 32room-alias.pl ├── 33room-members.pl ├── 34room-messages.pl ├── 35room-typing.pl ├── 36room-levels.pl ├── 37room-receipts.pl ├── 38room-read-marker.pl ├── 44account_data.pl ├── 45server-capabilities.pl └── 46push.pl ├── 11register.pl ├── 12login ├── 01threepid-and-password.pl └── 02cas.pl ├── 13logout.pl ├── 14account ├── 01change-password.pl └── 02deactivate.pl ├── 21presence-events.pl ├── 30rooms ├── 01state.pl ├── 02members-local.pl ├── 03members-remote.pl ├── 04messages.pl ├── 05aliases.pl ├── 06invite.pl ├── 07ban.pl ├── 08levels.pl ├── 09eventstream.pl ├── 10redactions.pl ├── 11leaving.pl ├── 12thirdpartyinvite.pl ├── 13guestaccess.pl ├── 14override-per-room.pl ├── 15kick.pl ├── 20typing.pl ├── 21receipts.pl ├── 22profile.pl ├── 30history-visibility.pl ├── 32erasure.pl ├── 40joinedapis.pl ├── 50context.pl ├── 51event.pl ├── 60version_upgrade.pl └── 70publicroomslist.pl ├── 31sync ├── 01filter.pl ├── 02sync.pl ├── 03joined.pl ├── 04timeline.pl ├── 05presence.pl ├── 06state.pl ├── 07invited.pl ├── 08polling.pl ├── 09archived.pl ├── 10archived-ban.pl ├── 11typing.pl ├── 12receipts.pl ├── 13filtered_sync.pl ├── 14read-markers.pl ├── 15lazy-members.pl ├── 16room-summary.pl └── 17peeking.pl ├── 32room-versions.pl ├── 40presence.pl ├── 41end-to-end-keys ├── 01-upload-key.pl ├── 03-one-time-keys.pl ├── 04-query-key-federation.pl ├── 05-one-time-key-federation.pl ├── 06-device-lists.pl ├── 07-backup.pl └── 08-cross-signing.pl ├── 42tags.pl ├── 44account_data.pl ├── 45openid.pl ├── 46direct ├── 01directmessage.pl ├── 02reliability.pl ├── 03polling.pl ├── 04federation.pl └── 05wildcard.pl ├── 48admin.pl ├── 49ignore.pl ├── 50federation ├── 00prepare.pl ├── 01keys.pl ├── 02server-names.pl ├── 10query-profile.pl ├── 11query-directory.pl ├── 30room-join.pl ├── 31room-send.pl ├── 32room-getevent.pl ├── 33room-get-missing-events.pl ├── 34room-backfill.pl ├── 35room-invite.pl ├── 36state.pl ├── 37public-rooms.pl ├── 38receipts.pl ├── 39redactions.pl ├── 40devicelists.pl ├── 40publicroomlist.pl ├── 41power-levels.pl ├── 43typing.pl ├── 44presence.pl ├── 50no-deextrem-outliers.pl ├── 50server-acl-endpoints.pl ├── 51transactions.pl └── 52soft-fail.pl ├── 51media ├── 20urlpreview.pl ├── 30config.pl ├── 48admin-quarantine.pl └── test.png ├── 52user-directory ├── 01public.pl └── 02private.pl ├── 54identity.pl ├── 60app-services ├── 00prepare.pl ├── 01as-create.pl ├── 02ghost.pl ├── 03passive.pl ├── 04asuser.pl ├── 05lookup3pe.pl ├── 06publicroomlist.pl └── 07deactivate.pl ├── 61push ├── 01message-pushed.pl ├── 02add_rules.pl ├── 03_unread_count.pl ├── 05_set_actions.pl ├── 06_get_pusher.pl ├── 07_set_enabled.pl ├── 08_rejected_pushers.pl ├── 09_notifications_api.pl └── 80torture.pl ├── 80torture ├── 03events.pl ├── 10filters.pl └── 20json.pl └── 90jira ├── SYN-205.pl ├── SYN-328.pl ├── SYN-343.pl ├── SYN-390.pl ├── SYN-516.pl ├── SYN-606.pl └── SYN-627.pl /.dir-locals.el: -------------------------------------------------------------------------------- 1 | ((cperl-mode . ((cperl-indent-level . 3) 2 | (cperl-continued-statement-offset . 3) 3 | (cperl-continued-brace-offset . -3) 4 | )) 5 | (perl-mode . ((perl-indent-level . 3) 6 | ))) 7 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | server-0/ 3 | server-1/ 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Automatically request reviews from the synapse-core team when a pull request comes in. 2 | * @matrix-org/synapse-core 3 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | # GitHub actions workflow which builds and publishes the docker images. 2 | 3 | name: Build and deploy docker images 4 | 5 | on: 6 | push: 7 | branches: [develop] 8 | schedule: 9 | - cron: 0 1 * * * # 1am, daily. 10 | workflow_dispatch: 11 | 12 | permissions: 13 | contents: read 14 | 15 | concurrency: 16 | group: ${{ github.workflow }}-${{ github.ref }} 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | build-sytest-images: 21 | name: "Build sytest:${{ matrix.tag }}" 22 | runs-on: ubuntu-latest 23 | strategy: 24 | matrix: 25 | include: 26 | - base_image: debian:bullseye 27 | tag: bullseye 28 | - base_image: debian:testing 29 | tag: testing 30 | steps: 31 | - name: Set up QEMU 32 | id: QEMU 33 | uses: docker/setup-qemu-action@v2 34 | 35 | - name: Set up Docker Buildx 36 | id: buildx 37 | uses: docker/setup-buildx-action@v2 38 | 39 | - name: Inspect builder 40 | run: docker buildx inspect 41 | 42 | - name: Log in to DockerHub 43 | uses: docker/login-action@v2 44 | with: 45 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 46 | password: ${{ secrets.DOCKER_HUB_TOKEN }} 47 | 48 | - name: Build and push 49 | uses: docker/build-push-action@v4 50 | with: 51 | pull: true 52 | push: true 53 | platforms: linux/amd64,linux/arm64 54 | labels: "gitsha1=${{ github.sha }}" 55 | file: docker/base.Dockerfile 56 | build-args: "BASE_IMAGE=${{ matrix.base_image }}" 57 | tags: matrixdotorg/sytest:${{ matrix.tag }} 58 | 59 | build-dependent-images: 60 | needs: build-sytest-images 61 | name: "Build sytest-${{ matrix.dockerfile }}:${{ matrix.sytest_image_tag }}" 62 | runs-on: ubuntu-latest 63 | strategy: 64 | matrix: 65 | include: 66 | - sytest_image_tag: bullseye 67 | dockerfile: synapse 68 | tags: "matrixdotorg/sytest-synapse:bullseye" 69 | build_args: "SYTEST_IMAGE_TAG=bullseye" 70 | - sytest_image_tag: testing 71 | dockerfile: synapse 72 | tags: "matrixdotorg/sytest-synapse:testing" 73 | build_args: | 74 | SYTEST_IMAGE_TAG=testing 75 | SYSTEM_PIP_INSTALL_SUFFIX=--break-system-packages 76 | 77 | steps: 78 | - name: Set up QEMU 79 | id: QEMU 80 | uses: docker/setup-qemu-action@v2 81 | 82 | - name: Set up Docker Buildx 83 | id: buildx 84 | uses: docker/setup-buildx-action@v2 85 | 86 | - name: Inspect builder 87 | run: docker buildx inspect 88 | 89 | - name: Log in to DockerHub 90 | uses: docker/login-action@v2 91 | with: 92 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 93 | password: ${{ secrets.DOCKER_HUB_TOKEN }} 94 | 95 | - name: Build and push 96 | uses: docker/build-push-action@v4 97 | with: 98 | pull: true 99 | push: true 100 | platforms: linux/amd64,linux/arm64 101 | labels: "gitsha1=${{ github.sha }}" 102 | file: docker/${{ matrix.dockerfile }}.Dockerfile 103 | build-args: ${{ matrix.build_args }} 104 | tags: ${{ matrix.tags }} 105 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .*.swp 3 | /.synapse-base 4 | /localhost-* 5 | /logs/ 6 | /media_store 7 | /results.tap 8 | /server-* 9 | /synapse 10 | /test-server 11 | /var 12 | *.db 13 | \#* 14 | signingkeyserver.db 15 | .vscode 16 | .coverage* 17 | .idea 18 | .DS_Store 19 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contributing code to SyTest 2 | =========================== 3 | 4 | sytest follows the same pattern as https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.md 5 | -------------------------------------------------------------------------------- /cpanfile: -------------------------------------------------------------------------------- 1 | # vim:ft=perl 2 | 3 | requires 'Class::Method::Modifiers'; 4 | 5 | requires 'Crypt::Ed25519'; 6 | 7 | requires 'Data::Dump'; 8 | 9 | # DBD::Pg fails to install if DBI is not already installed before we start. 10 | # (DBI goes into an architecture-dependent directory, which may not exist when 11 | # the installation process starts; by doing the install in two steps, we force 12 | # perl to rescan the library directories and add any new ones which it finds.) 13 | requires 'DBI'; 14 | 15 | requires 'DBD::Pg'; 16 | requires 'Digest::HMAC_SHA1'; 17 | requires 'Digest::SHA'; 18 | requires 'Email::Address::XS'; 19 | requires 'Email::MIME'; 20 | requires 'File::Basename'; 21 | requires 'File::Path'; 22 | requires 'File::Slurper'; 23 | 24 | # Future 0.45 allows you to return immediate values from sequence functions. 25 | requires 'Future', '>= 0.45'; 26 | requires 'Getopt::Long'; 27 | requires 'HTTP::Request'; 28 | requires 'IO::Async', '>= 0.69'; 29 | requires 'IO::Async::SSL'; 30 | requires 'IO::Socket::IP', '>= 0.04'; 31 | requires 'IO::Socket::SSL'; 32 | requires 'JSON'; 33 | 34 | # We don't have a hard dep on JSON::PP (JSON::XS would be fine), but 35 | # JSON::PP 2.274 incorrectly encodes JSON::Number(0) as "0", so we don't 36 | # want to end up using that by accident 37 | requires 'JSON::PP', '>= 2.91'; 38 | 39 | requires 'List::Util', '>= 1.45'; 40 | requires 'List::UtilsBy', '>= 0.10'; 41 | requires 'MIME::Base64'; 42 | requires 'Module::Pluggable'; 43 | requires 'Net::Async::HTTP', '>= 0.39'; 44 | requires 'Net::Async::HTTP::Server', '>= 0.09'; 45 | requires 'Net::SSLeay', '>= 1.59'; 46 | requires 'Struct::Dumb', '>= 0.04'; 47 | requires 'URI::Escape'; 48 | requires 'YAML::XS'; 49 | -------------------------------------------------------------------------------- /docker/base.Dockerfile: -------------------------------------------------------------------------------- 1 | ARG BASE_IMAGE=debian:bullseye 2 | 3 | FROM ${BASE_IMAGE} 4 | 5 | ENV DEBIAN_FRONTEND noninteractive 6 | 7 | # Install base dependencies that Python or Go would require 8 | RUN apt-get -qq update && apt-get -qq install -y \ 9 | apt-utils \ 10 | build-essential \ 11 | eatmydata \ 12 | git \ 13 | haproxy \ 14 | jq \ 15 | libffi-dev \ 16 | libjpeg-dev \ 17 | libpq-dev \ 18 | libssl-dev \ 19 | libxslt1-dev \ 20 | libz-dev \ 21 | locales \ 22 | perl \ 23 | postgresql \ 24 | rsync \ 25 | sqlite3 \ 26 | wget \ 27 | libicu-dev \ 28 | pkg-config \ 29 | && rm -rf /var/lib/apt/lists/* 30 | 31 | # Set up the locales, as the default Debian image only has C, and PostgreSQL needs the correct locales to make a UTF-8 database 32 | RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \ 33 | dpkg-reconfigure --frontend=noninteractive locales && \ 34 | update-locale LANG=en_US.UTF-8 35 | 36 | # Set the locales in the environment 37 | ENV LANG en_US.UTF-8 38 | ENV LANGUAGE en_US:en 39 | ENV LC_ALL en_US.UTF-8 40 | 41 | # Copy in the sytest dependencies and install them 42 | # (we expect the docker build context be the sytest repo root, rather than the `docker` folder) 43 | ADD install-deps.pl ./install-deps.pl 44 | ADD cpanfile ./cpanfile 45 | RUN perl ./install-deps.pl -T 46 | RUN rm cpanfile install-deps.pl 47 | 48 | # /logs is where we should expect logs to end up 49 | RUN mkdir /logs 50 | 51 | # Add the bootstrap file. 52 | ADD docker/bootstrap.sh /bootstrap.sh 53 | 54 | # PostgreSQL setup 55 | ENV PGHOST=/var/run/postgresql 56 | ENV PGDATA=$PGHOST/data 57 | ENV PGUSER=postgres 58 | 59 | RUN for ver in `ls /usr/lib/postgresql | head -n 1`; do \ 60 | su postgres -c '/usr/lib/postgresql/'$ver'/bin/initdb -E "UTF-8" --lc-collate="C" --lc-ctype="C" --username=postgres'; \ 61 | done 62 | 63 | # configure it not to try to listen on IPv6 (it won't work and will cause warnings) 64 | RUN echo "listen_addresses = '127.0.0.1'" >> "$PGDATA/postgresql.conf" 65 | -------------------------------------------------------------------------------- /docker/bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Fetch sytest, and then run the sytest running script. 4 | 5 | set -ex 6 | 7 | export SYTEST_TARGET="$1" 8 | export SYTEST_DEFAULT_BRANCH="${SYTEST_DEFAULT_BRANCH:-develop}" 9 | shift 10 | 11 | if [ -d "/sytest" ]; then 12 | # If the user has mounted in a SyTest checkout, use that. 13 | # This is the case for sytest's GitHub Actions. 14 | echo "Using local sytests" 15 | else 16 | echo "--- Trying to get same-named sytest branch..." 17 | # Check if we're running in CI. If so it can tell us what 18 | # Synapse/Dendrite branch we're running 19 | if [ -n "$SYTEST_BRANCH" ]; then 20 | branch_name="$SYTEST_BRANCH" 21 | else 22 | # Otherwise, try and find the branch that the Synapse/Dendrite checkout 23 | # is using. Fall back to the default branch if unknown. 24 | branch_name="$(git --git-dir=/src/.git symbolic-ref HEAD 2>/dev/null)" || branch_name="${SYTEST_DEFAULT_BRANCH}" 25 | fi 26 | 27 | # Try and fetch the branch 28 | wget -q https://github.com/matrix-org/sytest/archive/$branch_name.tar.gz -O sytest.tar.gz || { 29 | # Probably a 404, fall back to develop 30 | echo "Using ${SYTEST_DEFAULT_BRANCH} instead..." 31 | wget -q https://github.com/matrix-org/sytest/archive/${SYTEST_DEFAULT_BRANCH}.tar.gz -O sytest.tar.gz 32 | } 33 | 34 | mkdir -p /sytest 35 | tar -C /sytest --strip-components=1 -xf sytest.tar.gz 36 | 37 | if [ -n "$PLUGINS" ]; then 38 | mkdir /sytest/plugins 39 | echo "--- Downloading plugins for sytest" 40 | IFS=' '; for plugin in $PLUGINS; do 41 | plugindir=$(mktemp -d --tmpdir=/sytest/plugins) 42 | wget -q $plugin -O plugin.tar.gz || { 43 | echo "Failed to download plugin: $plugin" >&2 44 | exit 1 45 | } 46 | tar -C $plugindir --strip-components=1 -xf plugin.tar.gz 47 | done 48 | fi 49 | fi 50 | 51 | echo "--- Preparing sytest for ${SYTEST_TARGET}" 52 | 53 | export SYTEST_LIB="/sytest/lib" 54 | 55 | if [ -x "/sytest/scripts/${SYTEST_TARGET}_sytest.sh" ]; then 56 | exec "/sytest/scripts/${SYTEST_TARGET}_sytest.sh" "$@" 57 | 58 | elif [ -x "/sytest/docker/${SYTEST_TARGET}_sytest.sh" ]; then 59 | # old branches of sytest used to put the sytest running script in the "/docker" directory 60 | exec "/sytest/docker/${SYTEST_TARGET}_sytest.sh" "$@" 61 | 62 | else 63 | PLUGIN_RUNNER=$(find /sytest/plugins/ -type f -name "${SYTEST_TARGET}_sytest.sh" -print) 64 | if [ -n PLUGIN_RUNNER ]; then 65 | exec ${PLUGIN_RUNNER} "$@" 66 | else 67 | echo "sytest runner script for ${SYTEST_TARGET} not found" >&2 68 | exit 1 69 | fi 70 | fi 71 | -------------------------------------------------------------------------------- /docker/dendrite.Dockerfile: -------------------------------------------------------------------------------- 1 | ARG SYTEST_IMAGE_TAG=bullseye 2 | FROM matrixdotorg/sytest:${SYTEST_IMAGE_TAG} 3 | 4 | ARG GO_VERSION=1.19.1 5 | ARG TARGETARCH 6 | ENV GO_DOWNLOAD https://dl.google.com/go/go${GO_VERSION}.linux-${TARGETARCH}.tar.gz 7 | 8 | RUN mkdir -p /goroot /gopath 9 | RUN wget -q $GO_DOWNLOAD -O go.tar.gz 10 | RUN tar xf go.tar.gz -C /goroot --strip-components=1 11 | ENV GOROOT=/goroot 12 | ENV GOPATH=/gopath 13 | ENV PATH="/goroot/bin:${PATH}" 14 | # This is used in bootstrap.sh to pull in a dendrite specific Sytest branch 15 | ENV SYTEST_DEFAULT_BRANCH dendrite 16 | 17 | # This is where we expect Dendrite to be binded to from the host 18 | RUN mkdir -p /src 19 | 20 | ENTRYPOINT [ "/bin/bash", "/bootstrap.sh", "dendrite" ] 21 | -------------------------------------------------------------------------------- /docker/synapse.Dockerfile: -------------------------------------------------------------------------------- 1 | ARG SYTEST_IMAGE_TAG=bullseye 2 | 3 | FROM matrixdotorg/sytest:${SYTEST_IMAGE_TAG} 4 | 5 | ARG PYTHON_VERSION=python3 6 | ARG SYSTEM_PIP_INSTALL_SUFFIX="" 7 | 8 | ENV DEBIAN_FRONTEND noninteractive 9 | 10 | # Ensure we die correctly when using pipes in RUN. 11 | SHELL ["/bin/bash", "-o", "pipefail", "-c"] 12 | 13 | RUN apt-get -qq update && apt-get -qq install -y \ 14 | apt-utils ${PYTHON_VERSION} ${PYTHON_VERSION}-dev ${PYTHON_VERSION}-venv \ 15 | python3-pip eatmydata redis-server curl 16 | 17 | ENV RUSTUP_HOME=/rust 18 | ENV CARGO_HOME=/cargo 19 | ENV PATH=/cargo/bin:/rust/bin:$PATH 20 | RUN mkdir /rust /cargo 21 | 22 | RUN curl -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path --default-toolchain stable --profile minimal 23 | 24 | # For now, we need to tell Debian we don't care that we're editing the system python 25 | # installation. 26 | # Some context in https://github.com/pypa/pip/issues/11381#issuecomment-1399263627 27 | RUN ${PYTHON_VERSION} -m pip install -q --no-cache-dir poetry==1.3.2 ${SYSTEM_PIP_INSTALL_SUFFIX} 28 | 29 | # As part of the Docker build, we attempt to pre-install Synapse's dependencies 30 | # in the hope that it speeds up the real install of Synapse. To make this work, 31 | # we have to reuse the same virtual env both times. There are three ways to do 32 | # this with poetry: 33 | # 1. Ensure that the Synapse source directory lives in the same path both 34 | # times. Poetry creates and reuses a virtual env based off the package name 35 | # ("matrix-synapse") and directory path. 36 | # 2. Configure `virtualenvs.in-project` to `true`. This makes poetry create or 37 | # use a virtual env at `./.venv` in the source directory. 38 | # 3. Run poetry with a virtual env already active. Poetry will use the active 39 | # virtual env, if there is one. 40 | # We use the second option and make `.venv` a symlink. 41 | ENV POETRY_VIRTUALENVS_IN_PROJECT true 42 | 43 | # /src is where we expect the Synapse source directory to be mounted 44 | RUN mkdir /src 45 | 46 | # Download a cache of build dependencies to support offline mode. 47 | # These version numbers are arbitrary and were the latest at the time. 48 | RUN ${PYTHON_VERSION} -m pip download --dest /pypi-offline-cache \ 49 | poetry-core==1.1.0 setuptools==65.3.0 wheel==0.37.1 \ 50 | setuptools-rust==1.5.1 51 | 52 | # Create the virtual env upfront so we don't need to keep reinstalling 53 | # dependencies. 54 | RUN wget -q https://github.com/element-hq/synapse/archive/develop.tar.gz \ 55 | -O /synapse.tar.gz && \ 56 | mkdir /synapse && \ 57 | tar -C /synapse --strip-components=1 -xf synapse.tar.gz && \ 58 | ln -s -T /venv /synapse/.venv && \ 59 | cd /synapse && \ 60 | poetry install -q --no-root --extras all && \ 61 | # Finally clean up the poetry cache and the copy of Synapse. 62 | # This must be done in the same RUN command, otherwise intermediate layers 63 | # of the Docker image will contain all the unwanted files we think we've 64 | # deleted. 65 | rm -rf `poetry config cache-dir` && \ 66 | rm -rf /synapse && \ 67 | rm /synapse.tar.gz 68 | 69 | # Pre-install test dependencies installed by `scripts/synapse_sytest.sh`. 70 | RUN /venv/bin/pip install -q --no-cache-dir \ 71 | coverage codecov tap.py coverage_enable_subprocess 72 | 73 | # Poetry runs multiple pip operations in parallel. Unfortunately this results 74 | # in race conditions when a dependency is installed while another dependency 75 | # is being up/downgraded in `scripts/synapse_sytest.sh`. Configure poetry to 76 | # serialize `pip` operations by setting `experimental.new-installer` to a falsy 77 | # value. 78 | # See https://github.com/matrix-org/synapse/issues/12419 79 | # TODO: Once poetry 1.2.0 has been released, use the `installer.max-workers` 80 | # or `installer.parallel` config option instead. 81 | # poetry has a bug where this environment variable is not converted to a 82 | # boolean, so we choose a falsy string value for it. It's fixed in 1.2.0, 83 | # where we'll be wanting to use `installer.max-workers` anyway. 84 | ENV POETRY_EXPERIMENTAL_NEW_INSTALLER "" 85 | 86 | ENTRYPOINT [ "/bin/bash", "/bootstrap.sh", "synapse" ] 87 | -------------------------------------------------------------------------------- /docs/dendrite-setup.md: -------------------------------------------------------------------------------- 1 | # How to set up SyTest for Dendrite 2 | 3 | See [dendrite's developer documentation](https://github.com/element-hq/dendrite/blob/main/docs/development/sytest.md#using-the-sytest-docker-image) 4 | 5 | ([permalink](https://github.com/element-hq/dendrite/blob/3f727485d6e21a603e4df1cb31c3795cc1023caa/docs/development/sytest.md#using-the-sytest-docker-image)). 6 | -------------------------------------------------------------------------------- /install-deps.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Getopt::Long; 7 | 8 | use CPAN; 9 | 10 | GetOptions( 11 | 'T|notest' => \my $NOTEST, 12 | 'n|dryrun' => \my $DRYRUN, 13 | ) or exit 1; 14 | 15 | sub check_installed 16 | { 17 | my ( $mod, $want_ver, %opts ) = @_; 18 | 19 | # we do the import via a subprocess. The main reason for this is that, as 20 | # things get installed, the number of directories to be scanned increases 21 | # (for example, we may add architecture-dependent directories), and perl 22 | # only checks for these to add to @INC at startup. 23 | # 24 | # There are other benefits in doing so: 25 | # - we don't pollute the installation process with lots of random modules 26 | # - we ensure that each module really is installable in its own right. 27 | 28 | my $res = `$^X -M$mod -e 1 2>&1`; 29 | if( $? != 0 ) { 30 | die "unable to import $mod: $res"; 31 | } 32 | 33 | defined $want_ver or return 1; 34 | my $inst_ver = `$^X -M$mod -e 'print \$${mod}::VERSION'`; 35 | 36 | if( $want_ver =~ s/^>=\s*// ) { 37 | if( $inst_ver lt $want_ver ) { 38 | die "$mod: got $inst_ver, want >=$want_ver\n"; 39 | } 40 | } elsif( $want_ver =~ s/^<\s*// ) { 41 | if( $inst_ver ge $want_ver ) { 42 | die "$mod: got $inst_ver, want <$want_ver\n"; 43 | } 44 | } else { 45 | print STDERR "TODO: can only perform '<' and '>=' version checks: cannot support $want_ver\n"; 46 | return 1; 47 | } 48 | 49 | return 1; 50 | } 51 | 52 | sub requires 53 | { 54 | my ( $mod, $ver, $dist_path ) = @_; 55 | 56 | eval { check_installed( $mod, $ver ) } and return; 57 | 58 | $dist_path //= $mod; 59 | 60 | # TODO: check that some location is user-writable in @INC, and that it appears 61 | # somehow in PERL_{MB,MM}_OPT 62 | 63 | if( !$DRYRUN ) { 64 | print STDERR "\n\n**** install-deps.pl: Installing $mod ****\n"; 65 | 66 | if( $NOTEST ) { 67 | CPAN::Shell->notest('install', $dist_path); 68 | } else { 69 | CPAN::Shell->install($dist_path); 70 | } 71 | 72 | if( not eval { check_installed( $mod, $ver ) } ) { 73 | print STDERR "Failed to import $mod even after installing: $@\n"; 74 | exit 1; 75 | } 76 | } else { 77 | print qq($^X -MCPAN -e 'install "$dist_path"'\n); 78 | } 79 | } 80 | 81 | # $CPAN::DEBUG=2047; 82 | 83 | # load the config before we override things 84 | CPAN::HandleConfig->load; 85 | 86 | # tell CPAN to halt on first failure, to avoid hiding the error with errors 87 | # from things that are now certain to fail 88 | $CPAN::Config->{halt_on_failure} = 1; 89 | 90 | 91 | # Alien::Sodium will think it is building for javascript if the EMSCRIPTEN env 92 | # var is set. 93 | delete $ENV{EMSCRIPTEN}; 94 | 95 | do "./cpanfile"; 96 | -------------------------------------------------------------------------------- /keys/ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDZDCCAkygAwIBAgIJAKccGMVr5gLXMA0GCSqGSIb3DQEBCwUAMEcxCzAJBgNV 3 | BAYTAkdCMQ8wDQYDVQQIDAZMb25kb24xEzARBgNVBAoMCm1hdHJpeC5vcmcxEjAQ 4 | BgNVBAMMCXN5dGVzdCBDQTAeFw0xOTA2MDUxNTI2MDlaFw0yOTA2MDIxNTI2MDla 5 | MEcxCzAJBgNVBAYTAkdCMQ8wDQYDVQQIDAZMb25kb24xEzARBgNVBAoMCm1hdHJp 6 | eC5vcmcxEjAQBgNVBAMMCXN5dGVzdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP 7 | ADCCAQoCggEBALbZ+4WFsg4j/O+sFvL2vznFf6WvJEOd905KN9vwsz3Jp0d5BF6z 8 | WqzakQ2o2KXbfvF7ElkEQPH0GOvjCmsoyDOeRHj8nE5rms5bZimi76kakr0RrY2P 9 | OCg0+Oayan7RHb6QcFllbL9aJckJ9qD9UqRiIvUbd6ZT0x3CYObWGsiUWNsBiK72 10 | AkVtnlq6qseC90lFvx3YGOevetxJWyyCYPO5+hvYPNSa8TjjaQtVwAQumcTevS+K 11 | G6zvFdoxEhL1e+5RJyeNIN7hrG+Ht3G5qmn0cEId6E/CSEBe/m9o1hqd3jGY4Boh 12 | J8GPsEfdfK3r68u4cgna+0WjZQhlwxGwWYcCAwEAAaNTMFEwHQYDVR0OBBYEFEzx 13 | D0cBAWPq+0Lr0bbIDmThuR11MB8GA1UdIwQYMBaAFEzxD0cBAWPq+0Lr0bbIDmTh 14 | uR11MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAHh1w9FRwLGE 15 | rqPd8IhEHaq5oZXXp08uhV6dhnsOcU3LoD8dZyllBIEFSSF5Lwi6tNcefS16QZzV 16 | phzJ595bnfNlIltss/LsWqVKoXPOmyD7u0fOYJGAAVII0U2nS/8nNlvyoPxCcorW 17 | xmUSQjB9RKdlm6TdcDj6FwRTRwmmGhBnTcC2PWrqslXFo9hslYKFDiL/4RL+NKIC 18 | V4mIhTALOnNxU0L3yxdUzUiQmepDjUffPPk7rYbMJe8q38jm1EWlERuMKz6bY4rf 19 | QK9vBctMWiEdwnG+RfO4VP73dSc7UFodKSzBBt7TwLgtqntOUzfbIPlKjlVmnK0X 20 | Kz5XuJVplBQ= 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /keys/ca.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEAttn7hYWyDiP876wW8va/OcV/pa8kQ533Tko32/CzPcmnR3kE 3 | XrNarNqRDajYpdt+8XsSWQRA8fQY6+MKayjIM55EePycTmuazltmKaLvqRqSvRGt 4 | jY84KDT45rJqftEdvpBwWWVsv1olyQn2oP1SpGIi9Rt3plPTHcJg5tYayJRY2wGI 5 | rvYCRW2eWrqqx4L3SUW/HdgY56963ElbLIJg87n6G9g81JrxOONpC1XABC6ZxN69 6 | L4obrO8V2jESEvV77lEnJ40g3uGsb4e3cbmqafRwQh3oT8JIQF7+b2jWGp3eMZjg 7 | GiEnwY+wR918revry7hyCdr7RaNlCGXDEbBZhwIDAQABAoIBAGwdS0jRmkweH0of 8 | OJqEJuEj06vFeO26EyXpYEndcj3QY+YwudK8vZqCyU2ITkETHWXu3RRhHX1yVOH0 9 | po5h2K4coGPhCRKdMTVeeXOY8ZfNLII6V6Hh0tSDLcBKMgm1355zjNpuy/QAe2L5 10 | Tyg1YI3tsLm4efCQk71+1wjmA4QgotU/V7Cc8VrfqICiw3oXJcdmxw94NYxQJQVr 11 | oWpuLhqU1Mf3blryMg1X8J5TuFRnZCEf1nhRvryLa9XYPIGBtB/WUVVJroYNTIdx 12 | uBhA05ANkNDg4aByNg2xaQAt2Llri/rFaZXlDtifjtMIoNgmKfWCzAH+bHymo7D0 13 | hD0Rq6ECgYEA3WmiFWAJ68kDTdA2Y7YzNA7O7jIx+UB1E6lpyCyt8I+rrRHBEF2Q 14 | W5DIuwUtYro+A8X4wmJo0mPm5bso4ct1wnT2sxtkjjJEUlfSo6cJQq80cYwvCnYZ 15 | IDtidLp4JJv4IOA62Pxc3WwpQ0J82HSy2F0wQDHIscvb1Se9RB7QXikCgYEA02pG 16 | v9MS0KgyN3zLGRgjdeqCWRCLTtVDkGZBBu3PY18AmnY11rX1IipsUpNmwFu9Sjde 17 | 2qB4D8fjVa1ZAnVpkaPh+8lIAffjxdolkzQ25oqHVRE5F+Yig3d0EgpNnRyvZvFn 18 | 72P2cRAOXZI7DTVHYn/7M/dRLWDClBhcxy+4kC8CgYEAj/VlmEZITRD2X/qX0n8d 19 | jaRvMPpb+bbKKI2HJMrAEWAofC/F+pELEi3yBX9ZQg7b0XI/yotXoiuobgghjaXP 20 | HC8WU9/konvWZj+JyjQJ1ly6WXWPBFtC/Oz/l+vBv/PVAfMo7/otmx3/OicZq1c9 21 | DWaRv7texRNKDK545bivO/kCgYEApnS237HAzqifYTDQeCGZSe4qUxXDmX4whDD+ 22 | YgY7k3Hpd7Q7D6KULyJXx2xnKm0QzK5r8JcH8OThCURDILxxMkpmU2hXWbVjkRQB 23 | IbWqxDmt9DxrR3XbFsemi82w7lL3h4Xq34FFOB/8L5BDDlM0sUky7+d58tCMYy4L 24 | XokkN+ECgYEA1n/bRkP5IDFKeK/bc6j4juCdgQR1+yXJb/C8t+KmPJ1H87w4aYOQ 25 | 3RPkfBwWOHgJPlgoXOyS1ld2hyGqIurfSUrUpTU4aZnn1Uh22nGq7P7O8UKVNYGs 26 | OCOLYzSR5S2LimawXM8rYnihf9C4K9ev1vCQ418zR3iTk0M7X6tPLNY= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /keys/tls-selfsigned.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDfTCCAmWgAwIBAgIJAIkSb+v4fDtmMA0GCSqGSIb3DQEBCwUAMFQxCzAJBgNV 3 | BAYTAlVLMQ8wDQYDVQQIDAZMb25kb24xDzANBgNVBAcMBkxvbmRvbjEPMA0GA1UE 4 | CgwGU3l0ZXN0MRIwEAYDVQQDDAlsb2NhbGhvc3QwIBcNMTUwOTA5MTAwNjUzWhgP 5 | NDc1MzA4MDUxMDA2NTNaMFQxCzAJBgNVBAYTAlVLMQ8wDQYDVQQIDAZMb25kb24x 6 | DzANBgNVBAcMBkxvbmRvbjEPMA0GA1UECgwGU3l0ZXN0MRIwEAYDVQQDDAlsb2Nh 7 | bGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGPxg2Fo/uQ+Z3 8 | HAkgCFsBIQZ0+HBmoQDfelFyhSPE7Pbf3qWOzHTh2U0+xNIWwkvatAbJkr9Ijcbx 9 | OlUburYu11hLiUhokmQutiZJpjq7at6jxd+tX9+579tTVWJrlViyQt5x63NfywYi 10 | XF7/kanEOEBnLY+gZxDNX9s/ihjbDW6wQFufSrF8EQGACFGZlUi7H7msuy/O63E8 11 | hg77bFolt/vDxmIBtYx/qiSXKYXJtwjpvEmD0RqD/yytQ7lz7KwHElFwPLKGU3TJ 12 | lMWCOY4+1/Yd/RMhgV5jg3YJj0kZdiu4qlPnOXKTpUAifJiPuECsX9iC85lfTBsO 13 | TW1AhwCXAgMBAAGjUDBOMB0GA1UdDgQWBBTzmCknkdS112jenh5ojvUOzRBAojAf 14 | BgNVHSMEGDAWgBTzmCknkdS112jenh5ojvUOzRBAojAMBgNVHRMEBTADAQH/MA0G 15 | CSqGSIb3DQEBCwUAA4IBAQBUjkWtJcN0L8Aznjx4EC6kQiuvi1JNMv6JSy+ASAkY 16 | prT+aAptGLLWtWYLNEFjmDXE9LHHNdy5HRH4omtzHSxG52skLQSdgk/UUVz5SH00 17 | iUOrfMkmcA37Rn0QuzXt51Nzla2xY7ocpaCKGjXjnYJtuiSvIL+42BZ3Ljl4XwrP 18 | VGAtQb1z143YhQkEE9i2SUX8XruA8/iDikphmqQbgeCCXKe3twC4slqsP/jJ+26r 19 | puFZqq4M+eIbPrgiIcSI/8uFcrFtA+FhdOGaDefb9RBpDrfpakQuf0h/Bdc/TQq6 20 | 5j+4kdAXzr+34bMG4XV3lTwjImETuO3VqNFz1Vbgi+pl 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /keys/tls-selfsigned.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDGPxg2Fo/uQ+Z3 3 | HAkgCFsBIQZ0+HBmoQDfelFyhSPE7Pbf3qWOzHTh2U0+xNIWwkvatAbJkr9Ijcbx 4 | OlUburYu11hLiUhokmQutiZJpjq7at6jxd+tX9+579tTVWJrlViyQt5x63NfywYi 5 | XF7/kanEOEBnLY+gZxDNX9s/ihjbDW6wQFufSrF8EQGACFGZlUi7H7msuy/O63E8 6 | hg77bFolt/vDxmIBtYx/qiSXKYXJtwjpvEmD0RqD/yytQ7lz7KwHElFwPLKGU3TJ 7 | lMWCOY4+1/Yd/RMhgV5jg3YJj0kZdiu4qlPnOXKTpUAifJiPuECsX9iC85lfTBsO 8 | TW1AhwCXAgMBAAECggEAdbsynax4fX9FdqbnpGZR5TG/q+d+mPQ0mIMDq4b13xT6 9 | 56hJmaxCHX33KbepLGrmsVMIVN0ZS/UmmEuUD4uqddbdUL6OicumQzdZNZPKfVLS 10 | NWBlnYi6TKntDmg+srVWn0am4/B37AOd+PnyUMQolabyWFPuX8vVZqHuiqVwbWVZ 11 | IBjTg3KoSLIOmt/zFSmbXDh3u/CZhVtFhWzugn2VOosSDD2cSj7RPYYprPrbeFIR 12 | t/gJ+ytqhoVE9Pe540jrQ+FNezcer6RQ/M3StTBnCmyfy0fFz4kcr04NZFDhKa6p 13 | UWiYrN42+uj+f+HENtKA/ctsbpMAtraYcakzGcayQQKBgQDlb/Tyc5nl0FDaxEqP 14 | U5TcO1J+wvD91M3F/XMK+2HF/kaNi0eKB/022rYDxZg39Vxw+VgWix+0FdOWmPkG 15 | ZUHP5GFwwQdKI1vv2IF/WvMCvKDDqb3ek4xPtX4u3FmBxSJlC6Nah9zNlB+4P2dk 16 | lOfogjjEJbL+9u3B0GTs+N5FtQKBgQDdMrSg66nFwVzBWnR4uoaac5GIPJzaUwNx 17 | 4pp8+qWnaZXd1XcoVAypprGsZpzFuzej0WLe6j1IALht3L7/tLhXIZvEjiHs8kk8 18 | NmJ4s8CmYRfSDbEfP2bLsNgZOR0dt0FkRkyGaa7QFk5678M+3k1Ebg6Yo8SUqSY1 19 | MeZdbOccmwKBgQCaCtX5ZlYAbyGqD7pDiolaZ9XWV8n3kkXp1WXFDMwzY6o45DdO 20 | 3FhM2QD1fVfa0jiTfUWANT4+6Zya8u/XNGrASoSFcIQVabITUVh4ija4Mq75T2C0 21 | LHo7Fg63JahOsW2LTAoMAUXlHLtDOAaSCEu/1paIUwtflahBxUnNtLV/NQKBgQC5 22 | S+SseT46/wQopRG0oOxpLi7XpY7wBWJ5YI5x67YhS/3TWyL6kY4aAB4OgdcKfQ7b 23 | GGhuvGKhkpjsz2Lg6g8SToEiYr5gs4ZN/nD3E/1qYyOFPxsFRiNitr4QQxv07LLG 24 | wW0RnC5o+oa7zWYNoEOx81Ae5fGjZx8uqKKF2NixyQKBgGc+jcB6aya9DIttEtwQ 25 | DSJD6KdUQWd1qnvM6UWJI68fZ32gRTpnO0dLke1jm6tsWM4l3GGZv0B03zg0STIr 26 | N2Hfq1/9lixJbnYe1b7sdhFwBtpaiMAicjqgYDlxWrpmxwOkR1qU02y68YwFLviV 27 | ndP6RVYxlMogQcpx2r0oFXR9 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /keys/tls.dh: -------------------------------------------------------------------------------- 1 | -----BEGIN DH PARAMETERS----- 2 | MIIBCAKCAQEApgV+eHn+sejAJ8FIcc4diNmPmUxOYX2RDjAkFX7CzsGgYpqR+yh0 3 | XCnhprrgUVs4WO1o3fTNA5nQwatGOog+4SibdwJ3YixcjExdlz3Qk3gOzSUoxH12 4 | pbLxxE+hLl9ASfhZoAElZZyRoY+hSnoihqpfbL1cevfPb+ctvCaWh8DRtI3EEiXy 5 | nrc/ZnubERdnznARRkjsKClOi5fAgJXDwWihrjH6TGtNNKtOn6kF2EfVZzk17fFJ 6 | vBzo1oGMt4NRvCPNfv4qinBJhsd2o00GCv9Ovcm2QEPag0qX4j2zs937egZbQcsy 7 | qB+me4i6LIoCASH1GE8/WNGHgn+u0yDBSwIBAg== 8 | -----END DH PARAMETERS----- 9 | -------------------------------------------------------------------------------- /lib/Protocol/Matrix/HTTP/Federation.pm: -------------------------------------------------------------------------------- 1 | # You may distribute under the terms of either the GNU General Public License 2 | # or the Artistic License (the same terms as Perl itself) 3 | # 4 | # (C) Paul Evans, 2015 -- leonerd@leonerd.org.uk 5 | 6 | package Protocol::Matrix::HTTP::Federation; 7 | 8 | use strict; 9 | use warnings; 10 | 11 | our $VERSION = '0.02'; 12 | 13 | use HTTP::Request; 14 | 15 | =head1 NAME 16 | 17 | C - helpers for HTTP messages relating to Matrix federation 18 | 19 | =cut 20 | 21 | sub new 22 | { 23 | bless {}, shift; 24 | } 25 | 26 | =head1 METHODS 27 | 28 | =cut 29 | 30 | =head2 make_key_v1_request 31 | 32 | $req = $fed->make_key_v1_request( server_name => $name ) 33 | 34 | =cut 35 | 36 | sub make_key_v1_request 37 | { 38 | shift; 39 | my %params = @_; 40 | 41 | return HTTP::Request->new( 42 | GET => "/_matrix/key/v1", 43 | [ 44 | Host => $params{server_name}, 45 | ], 46 | ); 47 | } 48 | 49 | =head2 make_key_v2_server_request 50 | 51 | $req = $fed->make_key_v2_server_request( server_name => $name, key_id => $id ) 52 | 53 | =cut 54 | 55 | sub make_key_v2_server_request 56 | { 57 | shift; 58 | my %params = @_; 59 | 60 | return HTTP::Request->new( 61 | GET => "/_matrix/key/v2/server/$params{key_id}", 62 | [ 63 | Host => $params{server_name}, 64 | ], 65 | ); 66 | } 67 | 68 | 0x55AA; 69 | -------------------------------------------------------------------------------- /lib/SyTest/ApplicationService.pm: -------------------------------------------------------------------------------- 1 | package SyTest::ApplicationService; 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Future::Utils qw( repeat ); 7 | 8 | =head1 NAME 9 | 10 | C - abstraction of a single application service 11 | 12 | =head1 DESCRIPTION 13 | 14 | An instance of this class represents an abstracted application service for the 15 | homeserver to talk to. It provides the test scripts a way to receive inbound 16 | HTTP requests and respond to them, and allows access to the user information 17 | allowing a test script to send API requests. 18 | 19 | =cut 20 | 21 | =head1 CONSTRUCTOR 22 | 23 | =cut 24 | 25 | =head2 new 26 | 27 | $appserv = SyTest::ApplicationService->new( $info, $await_http ) 28 | 29 | Returns a newly constructed instance that uses the given C structure 30 | and the C function. 31 | 32 | =cut 33 | 34 | sub new 35 | { 36 | my $class = shift; 37 | my ( $info, $await_http ) = @_; 38 | 39 | my $path = $info->path; 40 | 41 | # Map event types to ARRAYs of Futures 42 | my %futures_by_type; 43 | 44 | my $f = repeat { 45 | $await_http->( qr{^\Q$path\E/_matrix/app/v1/transactions/\d+$}, sub { 1 }, 46 | timeout => 0, 47 | )->then( sub { 48 | my ( $request ) = @_; 49 | 50 | # Respond immediately to AS 51 | $request->respond_json( {} ); 52 | 53 | foreach my $event ( @{ $request->body_from_json->{events} } ) { 54 | my $type = $event->{type}; 55 | 56 | my $futures = $futures_by_type{$type}; 57 | 58 | # Ignore any cancelled ones 59 | while( $futures and @$futures and $futures->[0]->is_cancelled ) { 60 | shift @$futures; 61 | } 62 | 63 | if( $futures and my $next_f = shift @$futures ) { 64 | $next_f->done( $event, $request ); 65 | } 66 | else { 67 | warn "Ignoring incoming AS event of type $type\n"; 68 | } 69 | } 70 | 71 | Future->done; 72 | }) 73 | } while => sub { not $_[0]->failure }; 74 | 75 | $f->on_fail( sub { die $_[0] } ); 76 | 77 | return bless { 78 | info => $info, 79 | await_http => $await_http, 80 | 81 | futures_by_type => \%futures_by_type, 82 | await_loop_f => $f, 83 | }, $class; 84 | } 85 | 86 | =head1 METHODS 87 | 88 | =cut 89 | 90 | =head2 info 91 | 92 | $info = $appserv->info 93 | 94 | Returns the C structure instance this object was constructed with. 95 | 96 | =cut 97 | 98 | sub info 99 | { 100 | my $self = shift; 101 | return $self->{info}; 102 | } 103 | 104 | =head2 await_http_request 105 | 106 | $f = $appserv->await_http_request( $path, @args ) 107 | 108 | A wrapper around the C function the instance was 109 | constructed with, that prepends this server's path prefix onto the C<$path> 110 | argument for convenience. 111 | 112 | =cut 113 | 114 | sub await_http_request 115 | { 116 | my $self = shift; 117 | my ( $path, @args ) = @_; 118 | 119 | $self->{await_http}->( $self->info->path . $path, @args ); 120 | } 121 | 122 | =head2 await_event 123 | 124 | $f = $appserv->await_event( $type ) 125 | 126 | Returns a L that will succeed with the next event of the given 127 | C<$type> that the homeserver pushes to the application service. 128 | 129 | =cut 130 | 131 | sub await_event 132 | { 133 | my $self = shift; 134 | my ( $type ) = @_; 135 | 136 | my $failmsg = SyTest::CarpByFile::shortmess( 137 | "Timed out waiting for an AS event of type $type" 138 | ); 139 | 140 | push @{ $self->{futures_by_type}{$type} }, my $f = Future->new; 141 | 142 | return Future->wait_any( 143 | $f, 144 | 145 | main::delay( 10 ) 146 | ->then_fail( $failmsg ), 147 | ); 148 | } 149 | 150 | 1; 151 | -------------------------------------------------------------------------------- /lib/SyTest/CarpByFile.pm: -------------------------------------------------------------------------------- 1 | package SyTest::CarpByFile; 2 | 3 | # A variant of Carp:: that hunts for the first call from a different /file/, 4 | # regardless of package name. This is because most calls appear to come from the 5 | # "main::" package, on account of the way we slurp+eval() the test bodies. 6 | 7 | use strict; 8 | use warnings; 9 | 10 | use Exporter qw( import ); 11 | our @EXPORT_OK = qw( shortmess ); 12 | our @EXPORT = qw( carp croak ); 13 | 14 | our $CarpLevel = 0; 15 | 16 | sub shortmess 17 | { 18 | my ( $str ) = @_; 19 | 20 | my $callerfile = ( caller( $CarpLevel ) )[1]; 21 | my $level = $CarpLevel + 1; 22 | 23 | $level++ while ( caller( $level ) )[1] eq $callerfile; 24 | 25 | my ( undef, $file, $line ) = caller( $level ); 26 | 27 | return sprintf "%s at %s line %d.\n", $str, ( caller( $level ) )[1,2]; 28 | } 29 | 30 | sub croak 31 | { 32 | local $CarpLevel = $CarpLevel + 1; 33 | die shortmess( $_[0] ); 34 | } 35 | 36 | sub carp 37 | { 38 | local $CarpLevel = $CarpLevel + 1; 39 | warn shortmess( $_[0] ); 40 | } 41 | 42 | 1; 43 | -------------------------------------------------------------------------------- /lib/SyTest/Crypto.pm: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021 The Matrix.org Foundation C.I.C. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # crypto-related utility functions for SyTest 17 | 18 | 19 | package SyTest::Crypto; 20 | 21 | use Crypt::Ed25519; 22 | 23 | use Exporter qw( import ); 24 | our @EXPORT_OK = qw( ed25519_nacl_keypair ); 25 | 26 | =head2 ed25519_nacl_keypair 27 | 28 | ( $public_key, $secret_key ) = ed25519_nacl_keypair( [ $seed ] ); 29 | 30 | A drop in replacement for Crypt::NaCl::Sodium->sign->keypair. 31 | 32 | Generate a new Ed25519 keypair, in a format compatible with the NaCl API. 33 | 34 | If the optional seed is given, that is used to determiniatically derive the 35 | public and secret key. 36 | 37 | NaCl (http://nacl.cr.yp.to/) uses "secret keys" which are actually 64-byte 38 | tuples of (seed, public key) (whereas most other libraries either use just the 39 | seed, or a "preprocessed" 64-byte private key, which is deterministically 40 | derived from the seed. SyTest includes a bunch of code which relies on the NaCl 41 | format, so for now we have this shim to create them. 42 | 43 | Deprecated: it's better just to use the `Crypt::Ed25519::eddsa_*` APIs directly. 44 | 45 | =cut 46 | 47 | sub ed25519_nacl_keypair { 48 | my ( $seed ) = @_; 49 | $seed //= Crypt::Ed25519::eddsa_secret_key(); 50 | my $public_key = Crypt::Ed25519::eddsa_public_key($seed); 51 | return ( $public_key, $seed.$public_key ); 52 | } 53 | 54 | -------------------------------------------------------------------------------- /lib/SyTest/Federation/AuthChecks.pm: -------------------------------------------------------------------------------- 1 | package SyTest::Federation::AuthChecks; 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use List::Util qw( first ); 7 | 8 | # A mixin containing the actual event authentication check logic 9 | 10 | sub _find_auth_event_by_type 11 | { 12 | my ( $accepted_events, $auth_events, $type ) = @_; 13 | 14 | return first { $_ and $_->{type} eq $type } 15 | map { $accepted_events->{$_} } 16 | map { $_->[0] } @$auth_events; 17 | } 18 | 19 | sub auth_check_event 20 | { 21 | my $self = shift; 22 | my ( $event, $accepted_events ) = @_; 23 | 24 | # No event can be accepted if any of its own auth_events remains unaccepted 25 | $accepted_events->{$_} or return 0 26 | for map { $_->[0] } @{ $event->{auth_events} }; 27 | 28 | my $type = $event->{type}; 29 | 30 | # A couple of types of event are special 31 | if( $type eq "m.room.create" ) { 32 | return $self->auth_check_event_m_room_create( $event ); 33 | } 34 | elsif( $type eq "m.room.member" ) { 35 | return $self->auth_check_event_m_room_member( $event, $accepted_events ); 36 | } 37 | 38 | # Generic fallthrough which checks m.room.power_levels 39 | 40 | my $power_levels_event = _find_auth_event_by_type( 41 | $accepted_events, $event->{auth_events}, "m.room.power_levels" 42 | ); 43 | 44 | # If no m.room.power_levels event exists (e.g. because of bootstrapping) 45 | # synthesize a default one 46 | my $content = $power_levels_event ? $power_levels_event->{content} : do { 47 | my $create_event = _find_auth_event_by_type( 48 | $accepted_events, $event->{auth_events}, "m.room.create" 49 | ); 50 | 51 | { 52 | users => { 53 | $create_event->{content}{creator} => 100, 54 | }, 55 | users_default => 0, 56 | 57 | events => { 58 | "m.room.avatar" => 50, 59 | "m.room.canonical_alias" => 50, 60 | "m.room.history_visibility" => 100, 61 | "m.room.name" => 50, 62 | "m.room.power_levels" => 100, 63 | }, 64 | events_default => 0, 65 | state_default => 50, 66 | 67 | ban => 50, 68 | invite => 0, 69 | kick => 50, 70 | redact => 50, 71 | } 72 | }; 73 | 74 | my $requires_level = $content->{events}{"m.room.power_levels"}; 75 | $requires_level //= $content->{ 76 | defined $event->{state_key} ? "state_default" : "event_default" 77 | }; 78 | 79 | my $user_level = $content->{users}{ $event->{sender} }; 80 | $user_level //= $content->{users_default}; 81 | 82 | return 0 if $user_level < $requires_level; 83 | 84 | # TODO: Check special other rules for type 85 | 86 | return 1; 87 | } 88 | 89 | sub auth_check_event_m_room_create 90 | { 91 | my $self = shift; 92 | my ( $event ) = @_; 93 | 94 | # m.room.create really should not have any auth_events of its own 95 | @{ $event->{auth_events} } == 0 or 96 | return 0; 97 | 98 | # Any m.room.create event is acceptable, provided that the creator matches 99 | return $event->{sender} eq $event->{content}{creator}; 100 | } 101 | 102 | sub auth_check_event_m_room_member 103 | { 104 | my $self = shift; 105 | my ( $event, $accepted_events ) = @_; 106 | 107 | $event->{content}{membership} eq "join" or 108 | die "TODO: This check can only test join events"; 109 | 110 | # Users may only join themselves 111 | $event->{state_key} eq $event->{sender} or 112 | return 0; 113 | 114 | # For post-create bootstrapping, the room creator is always allowed to join 115 | # TODO(paul): Is this right? 116 | my $create_event = _find_auth_event_by_type( 117 | $accepted_events, $event->{auth_events}, "m.room.create" 118 | ); 119 | 120 | if( $create_event and $event->{state_key} eq $create_event->{content}{creator} ) { 121 | return 1; 122 | } 123 | 124 | die "TODO: non-creator join"; 125 | } 126 | 127 | 1; 128 | -------------------------------------------------------------------------------- /lib/SyTest/Federation/Protocol.pm: -------------------------------------------------------------------------------- 1 | # Copyright 2019 The Matrix.org Foundation C.I.C 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | package SyTest::Federation::Protocol; 16 | 17 | use strict; 18 | use warnings; 19 | 20 | use Digest::SHA qw( sha256 ); 21 | use MIME::Base64 qw( encode_base64url ); 22 | 23 | use Protocol::Matrix qw( 24 | redacted_event 25 | encode_json_for_signing 26 | encode_base64_unpadded 27 | ); 28 | 29 | use Carp; 30 | 31 | use Exporter 'import'; 32 | our @EXPORT_OK = qw( 33 | hash_event 34 | id_for_event 35 | ); 36 | 37 | =head2 hash_event 38 | 39 | $hash = hash_event( $event ); 40 | 41 | Calculates the reference hash of an event. 42 | 43 | =cut 44 | 45 | sub hash_event 46 | { 47 | my ( $event ) = @_; 48 | croak "Require an event" unless ref $event eq 'HASH'; 49 | my $redacted = redacted_event( $event ); 50 | delete $redacted->{signatures}; 51 | delete $redacted->{age_ts}; 52 | delete $redacted->{unsigned}; 53 | 54 | my $bytes_to_hash = encode_json_for_signing( $redacted ); 55 | return sha256( $bytes_to_hash ); 56 | } 57 | 58 | 59 | =head2 id_for_event 60 | 61 | $event_id = id_for_event( $event, $room_version ); 62 | 63 | Fetches or calculates the event_id for the given event 64 | 65 | =cut 66 | 67 | sub id_for_event 68 | { 69 | my ( $event, $room_version ) = @_; 70 | 71 | $room_version //= 1; 72 | 73 | if( $room_version eq '1' || $room_version eq '2' ) { 74 | my $event_id = $event->{event_id}; 75 | die "event with no event_id" if not $event_id; 76 | return $event_id; 77 | } 78 | 79 | my $event_hash = hash_event( $event ); 80 | 81 | # room v3 uses the unpadded-base64-encoded hash 82 | if( $room_version eq '3' ) { 83 | return '$' . encode_base64_unpadded( $event_hash ); 84 | } 85 | 86 | # other rooms use the url-safe unpadded-base64-encoded hash 87 | return '$' . encode_base64url( $event_hash ); 88 | } 89 | -------------------------------------------------------------------------------- /lib/SyTest/Federation/_Base.pm: -------------------------------------------------------------------------------- 1 | package SyTest::Federation::_Base; 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use mro 'c3'; 7 | use Protocol::Matrix qw( sign_json encode_base64_unpadded ); 8 | 9 | use Time::HiRes qw( time ); 10 | 11 | sub configure 12 | { 13 | my $self = shift; 14 | my %params = @_; 15 | 16 | foreach (qw( datastore )) { 17 | $self->{$_} = delete $params{$_} if exists $params{$_}; 18 | } 19 | 20 | $self->next::method( %params ); 21 | } 22 | 23 | sub datastore 24 | { 25 | my $self = shift; 26 | return $self->{datastore}; 27 | } 28 | 29 | sub server_name 30 | { 31 | my $self = shift; 32 | return $self->{datastore}->server_name; 33 | } 34 | 35 | sub key_id 36 | { 37 | my $self = shift; 38 | return $self->{datastore}->key_id; 39 | } 40 | 41 | # mutates the data 42 | sub sign_data 43 | { 44 | my $self = shift; 45 | my ( $data ) = @_; 46 | 47 | my $store = $self->{datastore}; 48 | 49 | sign_json( $data, 50 | secret_key => $store->secret_key, 51 | origin => $store->server_name, 52 | key_id => $store->key_id, 53 | ); 54 | } 55 | 56 | # returns a signed copy of the data 57 | sub signed_data 58 | { 59 | my $self = shift; 60 | my ( $orig ) = @_; 61 | 62 | $self->sign_data( my $copy = { %$orig } ); 63 | 64 | return $copy; 65 | } 66 | 67 | sub get_key 68 | { 69 | my $self = shift; 70 | my %params = @_; 71 | 72 | if( my $key = $self->{datastore}->get_key( %params ) ) { 73 | return Future->done( $key ); 74 | } 75 | 76 | $self->_fetch_key( $params{server_name}, $params{key_id} ) 77 | ->on_done( sub { 78 | my ( $key ) = @_; 79 | $self->{datastore}->put_key( %params, key => $key ); 80 | }); 81 | } 82 | 83 | sub time_ms 84 | { 85 | return int( time() * 1000 ); 86 | } 87 | 88 | 1; 89 | -------------------------------------------------------------------------------- /lib/SyTest/HTTPServer/Request.pm: -------------------------------------------------------------------------------- 1 | package SyTest::HTTPServer::Request; 2 | use 5.014; # ${^GLOBAL_PHASE} 3 | use base qw( Net::Async::HTTP::Server::Request ); 4 | 5 | use HTTP::Response; 6 | use JSON; 7 | my $json = JSON->new->convert_blessed; 8 | 9 | use constant JSON_MIME_TYPE => "application/json"; 10 | 11 | use SyTest::CarpByFile; 12 | 13 | use SyTest::HTTPClient; 14 | 15 | sub DESTROY 16 | { 17 | return if ${^GLOBAL_PHASE} eq "DESTRUCT"; 18 | my $self = shift or return; 19 | return if $self->{__responded}; 20 | carp "Destroying unresponded HTTP request to ${\$self->path}"; 21 | } 22 | 23 | sub respond 24 | { 25 | my $self = shift; 26 | $self->{__responded}++; 27 | $self->SUPER::respond( @_ ); 28 | } 29 | 30 | sub body_from_json 31 | { 32 | my $self = shift; 33 | 34 | if( ( my $type = $self->header( "Content-Type" ) // "" ) ne JSON_MIME_TYPE ) { 35 | croak "Cannot ->body_from_json with Content-Type: $type"; 36 | } 37 | 38 | my $decoded = $json->decode( $self->body ); 39 | 40 | # wrap numeric values with JSON::number to stop them getting turned into 41 | # strings when they are printed. 42 | $decoded = SyTest::HTTPClient::wrap_numbers( $decoded ); 43 | 44 | return $decoded; 45 | } 46 | 47 | sub respond_json 48 | { 49 | my $self = shift; 50 | my ( $body, %opts ) = @_; 51 | 52 | my $response = HTTP::Response->new( $opts{code} // 200 ); 53 | $response->add_content( $json->encode( $body )); 54 | $response->content_type( JSON_MIME_TYPE ); 55 | $response->content_length( length $response->content ); 56 | 57 | $self->respond( $response ); 58 | } 59 | 60 | sub body_from_form 61 | { 62 | my $self = shift; 63 | 64 | if( ( my $type = $self->header( "Content-Type" ) // "" ) ne "application/x-www-form-urlencoded" ) { 65 | croak "Cannot ->body_from_form with Content-Type: $type"; 66 | } 67 | 68 | # TODO: Surely there's a neater way than this?? 69 | return { URI->new( "http://?" . $self->body )->query_form }; 70 | } 71 | 72 | 1; 73 | -------------------------------------------------------------------------------- /lib/SyTest/Homeserver/Manual.pm: -------------------------------------------------------------------------------- 1 | # Copyright 2017 New Vector Ltd 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | use strict; 16 | use warnings; 17 | 18 | package SyTest::Homeserver::Manual; 19 | use base qw( SyTest::Homeserver ); 20 | 21 | use Carp; 22 | 23 | sub configure 24 | { 25 | my $self = shift; 26 | my %params = @_; 27 | 28 | foreach (qw ( 29 | host 30 | secure_port 31 | unsecure_port 32 | server_name 33 | )) { 34 | $self->{$_} = delete $params{$_} if exists $params{$_}; 35 | } 36 | 37 | foreach (qw ( host server_name )) { 38 | defined $self->{$_} or croak "Need a $_"; 39 | } 40 | 41 | if ( ! defined $self->{secure_port} && ! defined $self->{unsecure_port} ) { 42 | croak "Need either a secure_port or an unsecure_port"; 43 | } 44 | 45 | $self->SUPER::configure( %params ); 46 | } 47 | 48 | sub server_name 49 | { 50 | my $self = shift; 51 | 52 | return $self->{server_name}; 53 | } 54 | 55 | sub federation_host 56 | { 57 | my $self = shift; 58 | return $self->{host}; 59 | } 60 | 61 | sub federation_port 62 | { 63 | my $self = shift; 64 | return $self->{secure_port}; 65 | } 66 | 67 | sub public_baseurl 68 | { 69 | my $self = shift; 70 | 71 | return defined $self->{secure_port} ? 72 | "https://$self->{host}:" . $self->{secure_port} : 73 | "http://$self->{host}:" . $self->{unsecure_port}; 74 | } 75 | 76 | sub start 77 | { 78 | my $self = shift; 79 | 80 | return Future->done; 81 | } 82 | 83 | sub pid 84 | { 85 | return 0; 86 | } 87 | 88 | 89 | 1; 90 | -------------------------------------------------------------------------------- /lib/SyTest/HomeserverFactory.pm: -------------------------------------------------------------------------------- 1 | # Copyright 2017 New Vector Ltd 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | package SyTest::HomeserverFactory; 16 | 17 | # get the name of this implementation, by which it can be referenced with -I. 18 | # 19 | # Intented to be called as a class method: $FACTORY_CLASS->name 20 | sub name 21 | { 22 | my $cls = shift; 23 | $cls =~ s/^SyTest::HomeserverFactory:://; 24 | return $cls; 25 | } 26 | 27 | sub new 28 | { 29 | my $class = shift; 30 | my %params = @_; 31 | 32 | my $self = bless {}, $class; 33 | 34 | $self->_init( \%params ); 35 | 36 | return $self; 37 | } 38 | 39 | sub _init {} 40 | 41 | # Returns the generic simple name of the implementation, e.g. "synapse", 42 | # "dendrite" etc. This is used when determining whether to run any 43 | # implementation-specific tests. 44 | sub implementation_name 45 | { 46 | return ""; 47 | } 48 | 49 | # returns a list of (name => action) pairs suitable for 50 | # inclusion in the GetOptions argument list 51 | sub get_options 52 | { 53 | return (); 54 | } 55 | 56 | # writes a help string to STDERR 57 | sub print_usage 58 | { 59 | print STDERR " (no options)\n"; 60 | } 61 | 62 | 1; 63 | -------------------------------------------------------------------------------- /lib/SyTest/HomeserverFactory/Dendrite.pm: -------------------------------------------------------------------------------- 1 | # Copyright 2017 New Vector Ltd 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | use strict; 16 | use warnings; 17 | 18 | require SyTest::Homeserver::Dendrite; 19 | 20 | package SyTest::HomeserverFactory::Dendrite; 21 | use base qw( SyTest::HomeserverFactory ); 22 | 23 | sub _init 24 | { 25 | my $self = shift; 26 | 27 | $self->{args} = { 28 | bindir => "../dendrite/bin", 29 | }; 30 | 31 | $self->SUPER::_init( @_ ); 32 | } 33 | 34 | sub implementation_name 35 | { 36 | return "dendrite"; 37 | } 38 | 39 | sub get_options 40 | { 41 | my $self = shift; 42 | 43 | return ( 44 | 'd|dendrite-binary-directory=s' => \$self->{args}{bindir}, 45 | $self->SUPER::get_options(), 46 | ); 47 | } 48 | 49 | sub print_usage 50 | { 51 | print STDERR <{impl} = "SyTest::Homeserver::Dendrite::Monolith"; 69 | 70 | $self->SUPER::_init( @_ ); 71 | } 72 | 73 | sub create_server 74 | { 75 | my $self = shift; 76 | my %params = ( @_, %{ $self->{args}} ); 77 | 78 | return $self->{impl}->new( %params ); 79 | } 80 | 81 | 1; 82 | -------------------------------------------------------------------------------- /lib/SyTest/HomeserverFactory/Manual.pm: -------------------------------------------------------------------------------- 1 | # Copyright 2017 New Vector Ltd 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | use strict; 16 | use warnings; 17 | 18 | require SyTest::Homeserver::Manual; 19 | 20 | package SyTest::HomeserverFactory::Manual; 21 | use base qw( SyTest::HomeserverFactory ); 22 | 23 | sub _init 24 | { 25 | my $self = shift; 26 | 27 | $self->{location} = 'https://localhost:8448'; 28 | $self->{server_name} = undef; 29 | $self->{created} = 0; 30 | 31 | $self->SUPER::_init( @_ ); 32 | } 33 | 34 | sub implementation_name 35 | { 36 | return "manual"; 37 | } 38 | 39 | sub get_options 40 | { 41 | my $self = shift; 42 | 43 | return ( 44 | 'L|location=s' => \$self->{location}, 45 | 'server-name=s' => \$self->{server_name}, 46 | $self->SUPER::get_options(), 47 | ); 48 | } 49 | 50 | sub print_usage 51 | { 52 | print STDERR <{created} > 0 ) { 66 | die "can only create one server with -I Manual\n"; 67 | } 68 | $self->{created} ++; 69 | 70 | my ( $https, $host, $port ) = 71 | ( $self->{location} =~ m#^http(s)?://([^:/]+)(?::([0-9]+))?$# ) or 72 | die 'unable to parse location'; 73 | 74 | if( !defined $port ) { 75 | $port = $https ? 443 : 80; 76 | } 77 | 78 | $params{host} = $host; 79 | if( $https ) { 80 | $params{secure_port} = $port; 81 | } else { 82 | $params{unsecure_port} = $port; 83 | } 84 | 85 | $params{server_name} = $self->{server_name} // "$host:$port"; 86 | 87 | return SyTest::Homeserver::Manual->new( %params ); 88 | } 89 | 90 | 1; 91 | -------------------------------------------------------------------------------- /lib/SyTest/HomeserverFactory/Synapse.pm: -------------------------------------------------------------------------------- 1 | # Copyright 2017 New Vector Ltd 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | use strict; 16 | use warnings; 17 | 18 | require SyTest::Homeserver::Synapse; 19 | 20 | package SyTest::HomeserverFactory::Synapse; 21 | use base qw( SyTest::HomeserverFactory ); 22 | 23 | sub _init 24 | { 25 | my $self = shift; 26 | $self->{impl} = "SyTest::Homeserver::Synapse::Direct"; 27 | 28 | $self->{args} = { 29 | synapse_dir => "../synapse", 30 | python => "python", 31 | asyncio_reactor => 0, 32 | coverage => 0, 33 | print_output => 0, 34 | filter_output => undef, 35 | }; 36 | 37 | $self->{extra_args} = []; 38 | 39 | $self->SUPER::_init( @_ ); 40 | } 41 | 42 | sub implementation_name 43 | { 44 | return "synapse"; 45 | } 46 | 47 | sub get_options 48 | { 49 | my $self = shift; 50 | 51 | return ( 52 | 'd|synapse-directory=s' => \$self->{args}{synapse_dir}, 53 | 'python=s' => \$self->{args}{python}, 54 | 'coverage+' => \$self->{args}{coverage}, 55 | 'asyncio-reactor+' => \$self->{args}{asyncio_reactor}, 56 | 57 | 'S|server-log+' => \$self->{args}{print_output}, 58 | 'server-grep=s' => \$self->{args}{filter_output}, 59 | 60 | 'E=s' => sub { # process -Eoption=value 61 | my @more = split m/=/, $_[1]; 62 | 63 | # Turn single-letter into -X but longer into --NAME 64 | $_ = ( length > 1 ? "--$_" : "-$_" ) for $more[0]; 65 | 66 | push @{ $self->{extra_args} }, @more; 67 | }, 68 | 69 | $self->SUPER::get_options(), 70 | ); 71 | } 72 | 73 | sub print_usage 74 | { 75 | print STDERR <{extra_args} }; 93 | 94 | my %params = ( 95 | @_, 96 | %{ $self->{args}}, 97 | extra_args => \@extra_args, 98 | ); 99 | return $self->{impl}->new( %params ); 100 | } 101 | 102 | 103 | package SyTest::HomeserverFactory::Synapse::ViaHaproxy; 104 | use base qw( SyTest::HomeserverFactory::Synapse ); 105 | 106 | sub _init 107 | { 108 | my $self = shift; 109 | $self->SUPER::_init( @_ ); 110 | $self->{impl} = "SyTest::Homeserver::Synapse::ViaHaproxy"; 111 | $self->{args}{torture_replication} = 0; 112 | $self->{args}{redis_host} = ""; 113 | } 114 | 115 | sub get_options 116 | { 117 | my $self = shift; 118 | 119 | return ( 120 | 'workers' => sub { $self->{args}{workers} = 1 }, 121 | 'torture-replication:50' => \$self->{args}{torture_replication}, 122 | 'redis-host=s' => \$self->{args}{redis_host}, 123 | $self->SUPER::get_options(), 124 | ); 125 | } 126 | 127 | sub print_usage 128 | { 129 | my $self = shift; 130 | 131 | $self->SUPER::print_usage(); 132 | 133 | print STDERR < sub { ${ $_[0] } }, 9 | fallback => 1; 10 | sub new { 11 | my ( $class, $value ) = @_; 12 | return bless \$value, $class; 13 | } 14 | 15 | # By this even more terrible hack we can be both a function name and a package 16 | sub JSON::number { JSON::number::->new( $_[0] ) } 17 | 18 | sub TO_JSON { 0 + ${ $_[0] } } 19 | 20 | Data::Dump::Filtered::add_dump_filter( sub { 21 | ( ref($_[1]) // '' ) eq __PACKAGE__ 22 | ? { dump => "JSON::number(${ $_[1] })" } 23 | : undef; 24 | }); 25 | } 26 | 27 | use constant JSON_BOOLEAN_CLASS => ref( JSON::true ); 28 | 29 | Data::Dump::Filtered::add_dump_filter( sub { 30 | ( ref($_[1]) // '' ) eq JSON_BOOLEAN_CLASS 31 | ? { dump => $_[1] ? "JSON::true" : "JSON::false" } 32 | : undef; 33 | }); 34 | 35 | 1; 36 | -------------------------------------------------------------------------------- /lib/SyTest/MailServer.pm: -------------------------------------------------------------------------------- 1 | package SyTest::MailServer; 2 | 3 | use strict; 4 | use warnings; 5 | use Carp; 6 | 7 | use SyTest::MailServer::Protocol; 8 | 9 | use base qw( IO::Async::Listener ); 10 | 11 | =head1 NAME 12 | 13 | C - serve SMTP with C 14 | 15 | =head1 SYNOPSIS 16 | 17 | use SyTest::MailServer; 18 | use IO::Async::Loop; 19 | 20 | my $loop = IO::Async::Loop->new(); 21 | 22 | my $mailserver = Net::Async::HTTP::Server->new( 23 | on_mail => sub { 24 | my $self = shift; 25 | my ( $to, $from, $data ) = @_; 26 | }, 27 | ); 28 | 29 | $loop->add( $mailserver ); 30 | 31 | $mailserver->listen( 32 | addr => { family => "inet6", socktype => "stream", port => 2525 }, 33 | )->get 34 | 35 | $loop->run; 36 | 37 | =head1 DESCRIPTION 38 | 39 | This module allows a program to respond asynchronously to SMTP requests, as 40 | part of a program based on L. An object in this class listens on a 41 | single port and invokes the C callback or subclass method whenever 42 | a mail is received over SMTP. 43 | 44 | =cut 45 | 46 | =head1 EVENTS 47 | 48 | =head2 on_mail( $from, $to, $data ) 49 | 50 | Invoked when a new mail is received. 51 | 52 | =head1 METHODS 53 | 54 | As a small subclass of L, this class does not provide many 55 | new methods of its own. The superclass provides useful methods to control the 56 | basic operation of this server. 57 | 58 | Specifically, see the L method on how to actually 59 | bind the server to a listening socket to make it accept requests. 60 | 61 | =cut 62 | 63 | sub _init 64 | { 65 | my $self = shift; 66 | my ( $args ) = @_; 67 | 68 | $args->{handle_class} = "SyTest::MailServer::Protocol"; 69 | 70 | $self->SUPER::_init( $args ); 71 | } 72 | 73 | sub configure 74 | { 75 | my $self = shift; 76 | my %params = @_; 77 | 78 | foreach ( qw( on_mail ) ) { 79 | $self->{$_} = delete $params{$_} if $params{$_}; 80 | } 81 | 82 | $self->SUPER::configure( %params ); 83 | } 84 | 85 | sub _add_to_loop 86 | { 87 | my $self = shift; 88 | 89 | $self->can_event( "on_mail" ) or croak "Expected either a on_mail callback or an ->on_mail method"; 90 | 91 | $self->SUPER::_add_to_loop( @_ ); 92 | } 93 | 94 | sub on_accept 95 | { 96 | my $self = shift; 97 | my ( $conn ) = @_; 98 | 99 | $conn->configure( 100 | on_closed => sub { 101 | my $conn = shift; 102 | $conn->on_closed(); 103 | 104 | $conn->remove_from_parent; 105 | }, 106 | ); 107 | 108 | $self->add_child( $conn ); 109 | 110 | $conn->send_reply( 220, "Sytest test server" ); 111 | 112 | return $conn; 113 | } 114 | 115 | sub _received_mail 116 | { 117 | my $self = shift; 118 | my ( $from, $to, $data ) = @_; 119 | 120 | $self->invoke_event( 'on_mail', $from, $to, $data ); 121 | } 122 | 123 | 1; 124 | 125 | -------------------------------------------------------------------------------- /lib/SyTest/MailServer/Protocol.pm: -------------------------------------------------------------------------------- 1 | package SyTest::MailServer::Protocol; 2 | 3 | use strict; 4 | use warnings; 5 | use base qw( IO::Async::Stream ); 6 | 7 | my $CRLF = "\x0d\x0a"; 8 | 9 | sub on_read 10 | { 11 | my $self = shift; 12 | my ( $buffref, $eof ) = @_; 13 | 14 | return 0 if $eof; 15 | return 0 unless $$buffref =~ s/^(.*?$CRLF)//s; 16 | 17 | my ( $verb, $params ) = $self->tokenize_command( $1 ); 18 | return $self->process_command( $verb, $params ); 19 | } 20 | 21 | sub on_closed 22 | { 23 | my $self = shift; 24 | } 25 | 26 | sub tokenize_command { 27 | my ( $self, $line ) = @_; 28 | $line =~ s/\r?\n$//s; 29 | $line =~ s/^\s+|\s+$//g; 30 | my ( $verb, $params ) = split ' ', $line, 2; 31 | $verb = uc($verb) if defined($verb); 32 | return ( $verb, $params ); 33 | } 34 | 35 | sub process_command 36 | { 37 | my $self = shift; 38 | my ( $verb, $params ) = @_; 39 | 40 | $self->debug_printf( "COMMAND %s %s", $verb, $params ); 41 | 42 | if( my $code = $self->can( "on_" . $verb )) { 43 | return $code->( $self, $params ) // 1; 44 | } else { 45 | $self->send_reply( 500, 'Syntax error: unrecognized command' ); 46 | return 1; 47 | } 48 | } 49 | 50 | sub send_reply 51 | { 52 | my ( $self ) = shift; 53 | my ( $code, $msg ) = @_; 54 | 55 | $self->write( "$code $msg\r\n" ); 56 | } 57 | 58 | sub on_HELO 59 | { 60 | my ( $self ) = shift; 61 | my ( $params ) = @_; 62 | 63 | $self->send_reply( 250, "hi" ); 64 | } 65 | 66 | sub on_MAIL 67 | { 68 | my ( $self ) = shift; 69 | my ( $params ) = @_; 70 | 71 | if( defined $self->{mail_from} ) { 72 | $self->send_reply( 503, 'Bad sequence of commands' ); 73 | return; 74 | } 75 | 76 | unless ( $params =~ s/^from:\s*//i ) { 77 | $self->send_reply( 501, 'Syntax error in parameters or arguments' ); 78 | return; 79 | } 80 | 81 | $self->{mail_from} = $params; 82 | $self->send_reply( 250, "ok" ); 83 | } 84 | 85 | sub on_RCPT 86 | { 87 | my ( $self ) = shift; 88 | my ( $params ) = @_; 89 | 90 | if( defined $self->{rcpt_to} ) { 91 | $self->send_reply( 503, 'Bad sequence of commands' ); 92 | return; 93 | } 94 | 95 | unless ( $params =~ s/^to:\s*//i ) { 96 | $self->send_reply( 501, 'Syntax error in parameters or arguments' ); 97 | return; 98 | } 99 | 100 | $self->{rcpt_to} = $params; 101 | $self->send_reply( 250, "ok" ); 102 | } 103 | 104 | sub on_DATA 105 | { 106 | my ( $self ) = shift; 107 | my ( $params ) = @_; 108 | 109 | if( not defined $self->{rcpt_to} or not defined $self->{mail_from} ) { 110 | $self->send_reply( 503, 'Bad sequence of commands' ); 111 | return; 112 | } 113 | 114 | if ( $params ) { 115 | $self->send_reply( 501, 'Syntax error in parameters or arguments' ); 116 | return; 117 | } 118 | 119 | $self->send_reply( 354, "send message" ); 120 | 121 | return sub { 122 | my ( undef, $buffref, $eof ) = @_; 123 | return 0 unless $$buffref =~ s/(^.*$CRLF)\.$CRLF//s; 124 | 125 | $self->parent->_received_mail( $self->{mail_from}, $self->{rcpt_to}, $1 ); 126 | $self->send_reply( 250, "ok" ); 127 | $self->{rcpt_to} = undef; 128 | $self->{mail_from} = undef; 129 | return undef; 130 | } 131 | } 132 | 133 | 1; 134 | 135 | -------------------------------------------------------------------------------- /lib/SyTest/SSL.pm: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2019 The Matrix.org Foundation C.I.C. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | package SyTest::SSL; 17 | 18 | use Exporter 'import'; 19 | our @EXPORT_OK = qw( 20 | ensure_ssl_key 21 | create_ssl_cert 22 | ); 23 | 24 | =head2 ensure_ssl_key 25 | 26 | ensure_ssl_key( $key_file ); 27 | 28 | Create an SSL key file, if it doesn't exist. 29 | 30 | =cut 31 | 32 | sub ensure_ssl_key 33 | { 34 | my ( $key_file ) = @_; 35 | 36 | if ( ! -e $key_file ) { 37 | # todo: we can do this in pure perl 38 | system("openssl", "genrsa", "-out", $key_file, "2048") == 0 39 | or die "openssl genrsa failed $?"; 40 | } 41 | } 42 | 43 | =head2 create_ssl_cert 44 | 45 | create_ssl_cert( $cert_file, $key_file, $server_name ); 46 | 47 | Create a new SSL certificate file. The certificate will be signed by the test CA. 48 | 49 | =cut 50 | 51 | sub create_ssl_cert 52 | { 53 | my ( $cert_file, $key_file, $server_name ) = @_; 54 | 55 | # generate a CSR 56 | my $csr_file = "$cert_file.csr"; 57 | system( 58 | "openssl", "req", "-new", "-key", $key_file, "-out", $csr_file, 59 | "-subj", "/CN=$server_name", 60 | ) == 0 or die "openssl req failed $?"; 61 | 62 | # Create extension file 63 | my $ext_file = "$cert_file.ext"; 64 | open(my $fh, '>', $ext_file) or die "Could not open file '$ext_file': $!"; 65 | if ( $server_name =~ m/^[\d\.:]+$/ ) { 66 | # We assume that a server name that is purely numeric (plus ':' and '.') 67 | # is an IP. 68 | print $fh "subjectAltName=IP:$server_name\n"; 69 | } else { 70 | print $fh "subjectAltName=DNS:$server_name\n"; 71 | } 72 | close $fh; 73 | 74 | # sign it with the CA 75 | system( 76 | "openssl", "x509", "-req", "-in", $csr_file, 77 | "-CA", "keys/ca.crt", "-CAkey", "keys/ca.key", "-set_serial", 1, 78 | "-out", $cert_file, "-extfile", $ext_file, 79 | ) == 0 or die "openssl x509 failed $?"; 80 | } 81 | -------------------------------------------------------------------------------- /lib/SyTest/TCPProxy.pm: -------------------------------------------------------------------------------- 1 | package SyTest::TCPProxy; 2 | 3 | use strict; 4 | use warnings; 5 | use Carp; 6 | 7 | # A subclass of IO:Async::Listener that forwards all of its connections 8 | # to another TCP socket 9 | 10 | use base qw( IO::Async::Listener ); 11 | 12 | sub _init 13 | { 14 | my $self = shift; 15 | my ( $args ) = @_; 16 | 17 | $self->{$_} = delete $args->{$_} for qw( 18 | output 19 | ); 20 | 21 | $self->SUPER::_init( $args ); 22 | } 23 | 24 | sub configure 25 | { 26 | my $self = shift; 27 | my %params = @_; 28 | 29 | foreach ( qw( host port ) ) { 30 | $self->{$_} = delete $params{$_} if $params{$_}; 31 | } 32 | 33 | $self->SUPER::configure( %params ); 34 | } 35 | 36 | sub on_stream 37 | { 38 | my $self = shift; 39 | 40 | my ( $incoming ) = @_; 41 | 42 | my $socket1 = $incoming->read_handle; 43 | my $peeraddr = $socket1->peerhost . ":" . $socket1->peerport; 44 | 45 | $self->{output}->diag( "connection to proxy server from $peeraddr" ); 46 | 47 | my ( $host, $port ) = ( $self->{'host'}, $self->{'port'} ); 48 | 49 | my $fut = $self->loop->connect( 50 | host => $host, 51 | service => $port, 52 | socktype => "stream", 53 | 54 | on_stream => sub { 55 | my ( $outgoing ) = @_; 56 | 57 | $self->{output}->diag("connected to $host:$port"); 58 | 59 | $outgoing->configure( 60 | on_read => sub { 61 | my ( $self, $buffref, $eof ) = @_; 62 | $incoming->write( $$buffref ); 63 | $$buffref = ""; 64 | return 0; 65 | }, 66 | on_closed => sub { 67 | $incoming->close_when_empty; 68 | }, 69 | ); 70 | 71 | $incoming->configure( 72 | on_read => sub { 73 | my ( $self, $buffref, $eof ) = @_; 74 | $outgoing->write( $$buffref ); 75 | $$buffref = ""; 76 | return 0; 77 | }, 78 | on_closed => sub { 79 | $outgoing->close_when_empty; 80 | }, 81 | ); 82 | 83 | $self->add_child( $incoming ); 84 | $self->add_child( $outgoing ); 85 | }, 86 | ); 87 | 88 | $self->adopt_future( $fut ); 89 | } 90 | 91 | 1; 92 | -------------------------------------------------------------------------------- /scripts/dendrite_sytest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This script is run by the bootstrap.sh script in the docker image. 4 | # 5 | # It expects to find a built dendrite in /src/bin. It sets up the 6 | # postgres database and runs sytest against dendrite. 7 | 8 | set -ex 9 | 10 | cd /sytest 11 | 12 | mkdir -p /work 13 | 14 | # Make sure all Perl deps are installed -- this is done in the docker build so will only install packages added since the last Docker build 15 | ./install-deps.pl 16 | 17 | # Start the database 18 | su -c 'eatmydata /usr/lib/postgresql/*/bin/pg_ctl -w -D $PGDATA start' postgres 19 | 20 | # Create required databases 21 | su -c 'for i in pg1 pg2 sytest_template; do psql -c "CREATE DATABASE $i;"; done' postgres 22 | 23 | export PGUSER=postgres 24 | export POSTGRES_DB_1=pg1 25 | export POSTGRES_DB_2=pg2 26 | export GOBIN=/tmp/bin 27 | 28 | # Write out the configuration for a PostgreSQL Dendrite 29 | # Note: Dendrite can run entirely within a single database as all of the tables have 30 | # component prefixes 31 | ./scripts/prep_sytest_for_postgres.sh 32 | 33 | # Build dendrite 34 | echo >&2 "--- Building dendrite from source" 35 | cd /src 36 | mkdir -p $GOBIN 37 | 38 | if [[ -z ${COVER} || ${COVER} -eq 0 ]]; then 39 | go install -buildvcs=false -race=${RACE_DETECTION:-0} -tags vw -v ./cmd/dendrite 40 | else 41 | go test -c -cover -covermode=atomic -race=${RACE_DETECTION:-0} -buildvcs=false -tags vw -o $GOBIN/dendrite -coverpkg "github.com/matrix-org/..." ./cmd/dendrite 42 | fi 43 | 44 | go install -buildvcs=false -race=${RACE_DETECTION:-0} -tags vw -v ./cmd/generate-keys 45 | go install -buildvcs=false -race=${RACE_DETECTION:-0} -tags vw -v ./cmd/generate-config 46 | cd - 47 | 48 | # Run the tests 49 | echo >&2 "+++ Running tests" 50 | 51 | TEST_STATUS=0 52 | mkdir -p /logs 53 | ./run-tests.pl -I Dendrite::Monolith -d $GOBIN -W /src/sytest-whitelist -B /src/sytest-blacklist -O tap --all \ 54 | --work-directory="/work" --exclude-deprecated \ 55 | "$@" > /logs/results.tap & 56 | pid=$! 57 | 58 | # make sure that we kill the test runner on SIGTERM, SIGINT, etc 59 | trap 'kill $pid' TERM INT 60 | wait $pid || TEST_STATUS=$? 61 | trap - TERM INT 62 | 63 | if [ $TEST_STATUS -ne 0 ]; then 64 | echo >&2 -e "run-tests \e[31mFAILED\e[0m: exit code $TEST_STATUS" 65 | else 66 | echo >&2 -e "run-tests \e[32mPASSED\e[0m" 67 | fi 68 | 69 | # Check for new tests to be added to the test whitelist 70 | /src/show-expected-fail-tests.sh /logs/results.tap /src/sytest-whitelist \ 71 | /src/sytest-blacklist | tee /work/show_expected_fail_tests_output.txt || TEST_STATUS=$? 72 | 73 | echo >&2 "--- Copying assets" 74 | 75 | # Copy out the logs 76 | rsync -r --ignore-missing-args --min-size=1B -av /work/server-0 /work/server-1 /logs --include "*/" --include="*.log.*" --include="*.log" --exclude="*" 77 | find /logs | xargs -r chmod go+rX 78 | 79 | # Generate annotate.md. This is Buildkite-specific. 80 | if [ -n "$BUILDKITE_LABEL" ] && [ $TEST_STATUS -ne 0 ]; then 81 | # Build the annotation 82 | perl /sytest/scripts/format_tap.pl /logs/results.tap "$BUILDKITE_LABEL" >/logs/annotate.md 83 | # If show-expected-fail-tests logged something, put it into the annotation 84 | # Annotations from a failed build show at the top of buildkite, alerting 85 | # developers quickly as to what needs to change in the black/whitelist. 86 | cat /work/show_expected_fail_tests_output.txt >> /logs/annotate.md 87 | fi 88 | 89 | echo >&2 "--- Sytest compliance report" 90 | (cd /src && ./are-we-synapse-yet.py /logs/results.tap) || true 91 | 92 | exit $TEST_STATUS 93 | -------------------------------------------------------------------------------- /scripts/format_tap.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | # 3 | # Write a summary of a TAP file in a format suitable for Buildkite Annotations 4 | 5 | use strict; 6 | use warnings FATAL => 'all'; 7 | 8 | use TAP::Parser; 9 | 10 | # Get tap results filename and CI build name from argv 11 | my $tap_file = $ARGV[0]; 12 | my $build_name = $ARGV[1]; 13 | 14 | my $parser = TAP::Parser->new( { source => $tap_file } ); 15 | my $in_error = 0; 16 | my @out = ( "### TAP Output for $build_name" ); 17 | 18 | while ( my $result = $parser->next ) { 19 | if ( $result->is_test ) { 20 | # End an existing error block 21 | if ( $in_error == 1 ) { 22 | push( @out, "" ); 23 | push( @out, "" ); 24 | push( @out, "" ); 25 | push( @out, "----" ); 26 | push( @out, "" ); 27 | } 28 | 29 | $in_error = 0; 30 | 31 | # Start a new error block 32 | if ( not $result->is_ok ) { 33 | $in_error = 1; 34 | 35 | my $number = $result->number; 36 | my $description = $result->description; 37 | 38 | push(@out, "FAILURE Test #$number: ``$description``"); 39 | push(@out, ""); 40 | push(@out, "
Show log
");
41 |       }
42 |    } elsif ( $result->is_comment and $in_error == 1 ) {
43 |       # Print error contents
44 |       push( @out, $result->raw );
45 |    }
46 | }
47 | 
48 | # Print out the contents of @out, cutting off the final "----" and newlines
49 | foreach my $line ( @out[0..$#out-3] ) {
50 |    print $line . "\n";
51 | }
52 | 


--------------------------------------------------------------------------------
/scripts/prep_sytest_for_postgres.sh:
--------------------------------------------------------------------------------
 1 | #!/bin/sh
 2 | #
 3 | # Configure sytest to use postgres databases, per the env vars.  This is used
 4 | # by both the sytest builds and the synapse ones.
 5 | #
 6 | 
 7 | set -e
 8 | 
 9 | cd "/work"
10 | 
11 | if [ -z "$POSTGRES_DB_1" ]; then
12 |     echo >&2 "Variable POSTGRES_DB_1 not set"
13 |     exit 1
14 | fi
15 | 
16 | if [ -z "$POSTGRES_DB_2" ]; then
17 |     echo >&2 "Variable POSTGRES_DB_2 not set"
18 |     exit 1
19 | fi
20 | 
21 | mkdir -p "server-0"
22 | mkdir -p "server-1"
23 | 
24 | : PGUSER=${PGUSER:=$USER}
25 | 
26 | # We leave user, password, host blank to use the defaults (unix socket and
27 | # local auth)
28 | cat > "server-0/database.yaml" << EOF
29 | name: psycopg2
30 | args:
31 |     dbname: $POSTGRES_DB_1
32 |     user: $PGUSER
33 |     password: $PGPASSWORD
34 |     host: localhost
35 |     sslmode: disable
36 | EOF
37 | 
38 | cat > "server-1/database.yaml" << EOF
39 | name: psycopg2
40 | args:
41 |     dbname: $POSTGRES_DB_2
42 |     user: $PGUSER
43 |     password: $PGPASSWORD
44 |     host: localhost
45 |     sslmode: disable
46 | EOF
47 | 


--------------------------------------------------------------------------------
/scripts/tap_to_gha.pl:
--------------------------------------------------------------------------------
 1 | #!/usr/bin/perl
 2 | #
 3 | # Write a summary of a TAP file in a format suitable for Github Actions output
 4 | #
 5 | # See: https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions
 6 | 
 7 | use strict;
 8 | use warnings FATAL => 'all';
 9 | 
10 | use TAP::Parser;
11 | 
12 | my $RED = "\e[31m";
13 | my $RESET_FG = "\e[39m";
14 | 
15 | my $tap_file = $ARGV[0];
16 | 
17 | my $parser = TAP::Parser->new( { source => $tap_file } );
18 | my $in_error = 0;
19 | my $expected_fail = 0;
20 | 
21 | while ( my $result = $parser->next ) {
22 |    if ( $result->is_test ) {
23 |       # conclude any previous error block
24 |       if( $in_error ) {
25 |          print "::endgroup::\n"
26 |       }
27 | 
28 |       $in_error = 0;
29 | 
30 |       if ( not $result->is_ok ) {
31 |          $in_error = 1;
32 | 
33 |          my $number = $result->number;
34 |          my $description = $result->description;
35 | 
36 |          print "::error ::FAILURE: #$number: $description\n";
37 |          print "::group::Logs\n"
38 |       } elsif ( $result->directive and not $result->is_actual_ok ) {
39 |          $expected_fail++;
40 |       }
41 |    } elsif ( $result->is_comment and $in_error == 1 ) {
42 |       print $result->raw, "\n";
43 |    }
44 | }
45 | 
46 | if( $in_error ) {
47 |    print "::endgroup::\n"
48 | }
49 | 
50 | printf "Totals: %i passed, %i expected fail, %i failed\n", (
51 |    # actual_passed includes unexpected passes (ie expected failures which accidentally passed)
52 |    scalar $parser->actual_passed,
53 |    $expected_fail,
54 |    scalar $parser->failed,
55 | );
56 | 
57 | exit 1 if $parser->failed;
58 | 


--------------------------------------------------------------------------------
/tests/02as-info.pl:
--------------------------------------------------------------------------------
 1 | push our @EXPORT, qw( AS_INFO );
 2 | 
 3 | sub gen_token
 4 | {
 5 |    my ( $length ) = @_;
 6 |    return join "", map { chr 64 + rand 63 } 1 .. $length;
 7 | }
 8 | 
 9 | struct ASInfo => [qw( localpart user_id as2hs_token hs2as_token path id
10 |                       user_regexes alias_regexes protocols )],
11 |    named_constructor => 1;
12 | 
13 | my $n_appservers = 1;
14 | 
15 | # The actual infos
16 | my @as_info = (
17 |    # user_id will be filled in later once the homeserver is started
18 | 
19 |    ASInfo(
20 |       localpart     => "as-user-1",
21 |       user_id       => undef,
22 |       as2hs_token   => gen_token( 32 ),
23 |       hs2as_token   => gen_token( 32 ),
24 |       path          => "/appservs/1",
25 |       id            => "AS-1",
26 |       user_regexes  => [ '@astest-.*' ],
27 |       alias_regexes => [ '#astest-.*' ],
28 |       protocols     => [ 'ymca' ],
29 |    ),
30 | 
31 |    ASInfo(
32 |       localpart     => "as-user-2",
33 |       user_id       => undef,
34 |       as2hs_token   => gen_token( 32 ),
35 |       hs2as_token   => gen_token( 32 ),
36 |       path          => "/appservs/2",
37 |       id            => "AS-2",
38 |       user_regexes  => [],
39 |       alias_regexes => [],
40 |       protocols     => [ 'ymca' ],
41 |    ),
42 | );
43 | 
44 | our @AS_INFO = map {
45 |    my $idx = $_;
46 | 
47 |    fixture(
48 |       setup => sub { Future->done( $as_info[$idx] ) },
49 |    );
50 | } 0 .. $#as_info;
51 | 


--------------------------------------------------------------------------------
/tests/04mail-server.pl:
--------------------------------------------------------------------------------
  1 | use SyTest::MailServer;
  2 | use Email::Address::XS;
  3 | use Email::MIME;
  4 | use List::UtilsBy qw( extract_first_by );
  5 | 
  6 | 
  7 | =head2 MAIL_SERVER_INFO
  8 | 
  9 | A fixture which starts a test SMTP server.
 10 | 
 11 | The result is a hash with the following members:
 12 | 
 13 | =over
 14 | 
 15 | =item host
 16 | 
 17 | hostname where this server can be reached
 18 | 
 19 | =item port
 20 | 
 21 | port where this server can be reached
 22 | 
 23 | =back
 24 | 
 25 | =cut
 26 | 
 27 | our $MAIL_SERVER_INFO = fixture(
 28 |    requires => [],
 29 |    setup => sub {
 30 |       my $mail_server = SyTest::MailServer->new(
 31 |          on_mail => \&_on_mail,
 32 |       );
 33 |       $loop->add( $mail_server );
 34 | 
 35 |       $mail_server->listen(
 36 |          host     => $BIND_HOST,
 37 |          service  => 0,
 38 |          socktype => 'stream',
 39 |       )->then( sub {
 40 |          my ( $listener ) = @_;
 41 |          my $sockport = $listener->read_handle->sockport;
 42 |          my $sockname = "$BIND_HOST:$sockport";
 43 | 
 44 |          $OUTPUT->diag( "Started test SMTP Server at $sockname" );
 45 |          Future->done({
 46 |             host => $BIND_HOST,
 47 |             # +0 because otherwise this comes back as a string, and perl is
 48 |             # awful
 49 |             port => $sockport + 0,
 50 |          });
 51 |       });
 52 |    },
 53 | );
 54 | 
 55 | push our @EXPORT, qw( MAIL_SERVER_INFO );
 56 | 
 57 | struct MailAwaiter => [qw( future rcpt_match )];
 58 | 
 59 | my @pending_awaiters;
 60 | 
 61 | sub _on_mail {
 62 |    my ( undef, $from, $to, $data ) = @_;
 63 | 
 64 |    if( $CLIENT_LOG ) {
 65 |       my $green = -t STDOUT ? "\e[1;32m" : "";
 66 |       my $reset = -t STDOUT ? "\e[m" : "";
 67 |       print "${green}Received mail${reset} from $from to $to:\n";
 68 |       print "  $_\n" for split m/\n/, $data;
 69 |       print "-- \n";
 70 |    }
 71 | 
 72 |    $to = Email::Address::XS->parse( $to )->address;
 73 |    $from = Email::Address::XS->parse( $from )->address;
 74 |    my $email = Email::MIME->new( $data );
 75 | 
 76 |    my $awaiter = extract_first_by {
 77 |       return $to eq $_->rcpt_match;
 78 |    } @pending_awaiters;
 79 | 
 80 |    if( $awaiter ) {
 81 |       $awaiter->future->done( $from, $email );
 82 |    } else {
 83 |       warn "Received spurious email from $from to $to\n";
 84 |    }
 85 | }
 86 | 
 87 | =head2 await_email_to
 88 | 
 89 |    await_email( $rcpt )->then( sub {
 90 |        my ( $from, $email ) = @_;
 91 |        print $email->body;
 92 |    });
 93 | 
 94 | <$email> is an C instance.
 95 | 
 96 | =cut
 97 | 
 98 | sub await_email_to {
 99 |    my ( $rcpt, %args ) = @_;
100 |    my $timeout = $args{timeout} // 10;
101 | 
102 |    my $f = $loop->new_future;
103 |    my $awaiter = MailAwaiter( $f, $rcpt );
104 |    push @pending_awaiters, $awaiter;
105 | 
106 |    $f->on_cancel( sub {
107 |       extract_first_by { $_ == $awaiter } @pending_awaiters;
108 |    });
109 | 
110 |    return Future->wait_any(
111 |       $f,
112 |       delay( $timeout )->then_fail( "Timed out waiting for email" ),
113 |    );
114 | }
115 | 
116 | push @EXPORT, qw( await_email_to );
117 | 


--------------------------------------------------------------------------------
/tests/06http-clients.pl:
--------------------------------------------------------------------------------
 1 | use SyTest::HTTPClient;
 2 | 
 3 | push our @EXPORT, qw( HTTP_CLIENT API_CLIENTS );
 4 | 
 5 | our $HTTP_CLIENT = fixture(
 6 |    setup => sub {
 7 |       # Generic NaHTTP client, with SSL verification turned off, in case tests
 8 |       # need to speak plain HTTP(S) to an endpoint
 9 | 
10 |       my $http_client = SyTest::HTTPClient->new;
11 | 
12 |       $loop->add( $http_client );
13 | 
14 |       Future->done( $http_client );
15 |    },
16 | );
17 | 
18 | our @API_CLIENTS = map {
19 |    my $info_fixture = $_;
20 | 
21 |    fixture(
22 |       requires => [ $info_fixture ],
23 | 
24 |       setup => sub {
25 |          my ( $info ) = @_;
26 | 
27 |          my $location = $info->client_location;
28 | 
29 |          my $client = SyTest::HTTPClient->new(
30 |             max_connections_per_host => 3,
31 |             uri_base => "$location/_matrix/client",
32 |             server_name => $info->server_name,
33 |          );
34 |          $loop->add( $client );
35 | 
36 |          Future->done( $client );
37 |       },
38 |    );
39 | } @main::HOMESERVER_INFO;
40 | 


--------------------------------------------------------------------------------
/tests/07id-server.pl:
--------------------------------------------------------------------------------
 1 | use IO::Async::SSL;
 2 | use IO::Async::Listener 0.69;  # for ->configure( handle => undef )
 3 | use SyTest::Identity::Server;
 4 | use File::Basename qw( dirname );
 5 | 
 6 | my $DIR = dirname( __FILE__ );
 7 | 
 8 | push our @EXPORT, qw( id_server_fixture );
 9 | 
10 | sub id_server_fixture
11 | {
12 |    return fixture(
13 |       name => 'id_server_fixture',
14 | 
15 |       setup => sub {
16 |          my $id_server = SyTest::Identity::Server->new;
17 |          $loop->add( $id_server );
18 | 
19 |          $id_server->listen(
20 |             host          => $BIND_HOST,
21 |             service       => "",
22 |             extensions    => [qw( SSL )],
23 |             # Synapse currently only talks IPv4
24 |             family        => "inet",
25 | 
26 |             SSL_cert_file => "$DIR/../keys/tls-selfsigned.crt",
27 |             SSL_key_file  => "$DIR/../keys/tls-selfsigned.key",
28 |          )->then_done( $id_server );
29 |       },
30 | 
31 |       teardown => sub {
32 |          my ( $id_server ) = @_;
33 |          $loop->remove( $id_server );
34 | 
35 |          Future->done;
36 |       },
37 |    );
38 | }
39 | 


--------------------------------------------------------------------------------
/tests/10apidoc/00prepare.pl:
--------------------------------------------------------------------------------
 1 | our @EXPORT = qw( User is_User do_request_json_for new_User );
 2 | 
 3 | my @KEYS = qw(
 4 |    http user_id server_name device_id password access_token eventstream_token
 5 |    sync_next_batch saved_events pending_get_events device_message_next_batch
 6 | );
 7 | 
 8 | # A handy little structure for other scripts to find in 'user' and 'more_users'
 9 | struct User => [ @KEYS ], predicate => 'is_User';
10 | 
11 | sub do_request_json_for
12 | {
13 |    my ( $user, %args ) = @_;
14 |    is_User( $user ) or croak "Expected a User";
15 | 
16 |    croak "must give a method" unless $args{method};
17 | 
18 |    my $user_id = $user->user_id;
19 |    my $uri = delete $args{uri};
20 |    if( $uri ) {
21 |       $uri =~ s/:user_id/$user_id/g;
22 |    }
23 | 
24 |    my %params = (
25 |       access_token => $user->access_token,
26 |       %{ delete $args{params} || {} },
27 |    );
28 | 
29 |    $user->http->do_request_json(
30 |       uri          => $uri,
31 |       params       => \%params,
32 |       request_user => $user_id,
33 |       %args,
34 |    );
35 | }
36 | 
37 | 
38 | sub new_User
39 | {
40 |    my ( %params ) = @_;
41 |    $params{server_name} //= $params{http}->server_name;
42 | 
43 |    my $user = User( delete @params{ @KEYS } );
44 | 
45 |    if ( %params ) {
46 |       die "Unexpected parameter to new_User";
47 |    }
48 | 
49 |    return $user;
50 | }
51 | 


--------------------------------------------------------------------------------
/tests/10apidoc/01request-encoding.pl:
--------------------------------------------------------------------------------
 1 | use JSON qw( decode_json );
 2 | 
 3 | test "POST rejects invalid utf-8 in JSON",
 4 |    requires => [ $main::API_CLIENTS[0] ],
 5 | 
 6 |    do => sub {
 7 |       my ( $http ) = @_;
 8 | 
 9 |       my $reqbody = '{ "username": "a' . chr(0x81) . '" }';
10 | 
11 |       $http->do_request(
12 |          method => "POST",
13 |          uri    => "/v3/register",
14 | 
15 |          content => $reqbody,
16 |          content_type =>"application/json",
17 |       )->main::expect_http_400()
18 |       ->then( sub {
19 |          my ( $response ) = @_;
20 |          my $body = decode_json( $response->content );
21 |          assert_eq( $body->{errcode}, "M_NOT_JSON", 'responsecode' );
22 |          Future->done( 1 );
23 |       });
24 |    };
25 | 


--------------------------------------------------------------------------------
/tests/10apidoc/04version.pl:
--------------------------------------------------------------------------------
 1 | test "Version responds 200 OK with valid structure",
 2 |    requires => [ $main::API_CLIENTS[0] ],
 3 | 
 4 |    check => sub {
 5 |       my ( $http ) = @_;
 6 | 
 7 |       $http->do_request_json(
 8 |          method => "GET",
 9 |          uri    => "/versions",
10 |       )->then( sub {
11 |          my ( $body ) = @_;
12 | 
13 |          assert_json_keys( $body, qw( versions ) );
14 |          assert_json_list( $body->{versions} );
15 | 
16 |          Future->done( 1 );
17 |       })
18 |    };
19 | 


--------------------------------------------------------------------------------
/tests/10apidoc/10profile-displayname.pl:
--------------------------------------------------------------------------------
 1 | my $user_fixture = local_user_fixture();
 2 | 
 3 | my $displayname = "Testing Displayname";
 4 | 
 5 | test "PUT /profile/:user_id/displayname sets my name",
 6 |    requires => [ $user_fixture ],
 7 | 
 8 |    proves => [qw( can_set_displayname )],
 9 | 
10 |    check => sub {
11 |       my ( $user ) = @_;
12 | 
13 |       do_request_json_for( $user,
14 |          method => "GET",
15 |          uri    => "/v3/profile/:user_id/displayname",
16 |       )->then( sub {
17 |          my ( $body ) = @_;
18 | 
19 |          assert_json_keys( $body, qw( displayname ));
20 | 
21 |          $body->{displayname} eq $displayname or
22 |             die "Expected displayname to be '$displayname'";
23 | 
24 |          Future->done(1);
25 |       });
26 |    },
27 | 
28 |    do => sub {
29 |       my ( $user ) = @_;
30 | 
31 |       do_request_json_for( $user,
32 |          method => "PUT",
33 |          uri    => "/v3/profile/:user_id/displayname",
34 | 
35 |          content => {
36 |             displayname => $displayname,
37 |          },
38 |       );
39 |    };
40 | 
41 | test "GET /profile/:user_id/displayname publicly accessible",
42 |    requires => [ $main::API_CLIENTS[0], $user_fixture,
43 |                  qw( can_set_displayname )],
44 | 
45 |    proves => [qw( can_get_displayname )],
46 | 
47 |    check => sub {
48 |       my ( $http, $user ) = @_;
49 |       my $user_id = $user->user_id;
50 | 
51 |       $http->do_request_json(
52 |          method => "GET",
53 |          uri    => "/v3/profile/$user_id/displayname",
54 |          # no access_token
55 |       )->then( sub {
56 |          my ( $body ) = @_;
57 | 
58 |          assert_json_keys( $body, qw( displayname ));
59 | 
60 |          $body->{displayname} eq $displayname or
61 |             die "Expected displayname to be '$displayname'";
62 | 
63 |          Future->done(1);
64 |       });
65 |    };
66 | 
67 | 
68 | push our @EXPORT, qw( matrix_set_displayname );
69 | 
70 | 
71 | sub matrix_set_displayname
72 | {
73 |    my ( $user, $displayname ) = @_;
74 | 
75 |    do_request_json_for( $user,
76 |       method => "PUT",
77 |       uri    => "/v3/profile/:user_id/displayname",
78 | 
79 |       content => { displayname => $displayname },
80 |    );
81 | }
82 | 


--------------------------------------------------------------------------------
/tests/10apidoc/11profile-avatar_url.pl:
--------------------------------------------------------------------------------
 1 | my $user_fixture = local_user_fixture();
 2 | 
 3 | my $avatar_url = "mxc://example.com/SEsfnsuifSDFSSEF";
 4 | 
 5 | test "PUT /profile/:user_id/avatar_url sets my avatar",
 6 |    requires => [ $user_fixture ],
 7 | 
 8 |    proves => [qw( can_set_avatar_url )],
 9 | 
10 |    check => sub {
11 |       my ( $user ) = @_;
12 | 
13 |       do_request_json_for( $user,
14 |          method => "GET",
15 |          uri    => "/v3/profile/:user_id/avatar_url",
16 |       )->then( sub {
17 |          my ( $body ) = @_;
18 | 
19 |          assert_json_keys( $body, qw( avatar_url ));
20 | 
21 |          $body->{avatar_url} eq $avatar_url or
22 |             die "Expected avatar_url to be '$avatar_url'";
23 | 
24 |          Future->done(1);
25 |       });
26 |    },
27 | 
28 |    do => sub {
29 |       my ( $user ) = @_;
30 | 
31 |       do_request_json_for( $user,
32 |          method => "PUT",
33 |          uri    => "/v3/profile/:user_id/avatar_url",
34 | 
35 |          content => {
36 |             avatar_url => $avatar_url,
37 |          },
38 |       );
39 |    };
40 | 
41 | test "GET /profile/:user_id/avatar_url publicly accessible",
42 |    requires => [ $main::API_CLIENTS[0], $user_fixture,
43 |                  qw( can_set_avatar_url )],
44 | 
45 |    proves => [qw( can_get_avatar_url )],
46 | 
47 |    check => sub {
48 |       my ( $http, $user ) = @_;
49 |       my $user_id = $user->user_id;
50 | 
51 |       $http->do_request_json(
52 |          method => "GET",
53 |          uri    => "/v3/profile/$user_id/avatar_url",
54 |          # no access_token
55 |       )->then( sub {
56 |          my ( $body ) = @_;
57 | 
58 |          assert_json_keys( $body, qw( avatar_url ));
59 | 
60 |          $body->{avatar_url} eq $avatar_url or
61 |             die "Expected avatar_url to be '$avatar_url'";
62 | 
63 |          Future->done(1);
64 |       });
65 |    };
66 | 


--------------------------------------------------------------------------------
/tests/10apidoc/20presence.pl:
--------------------------------------------------------------------------------
 1 | my $fixture = local_user_fixture();
 2 | 
 3 | push our @EXPORT, qw( matrix_get_presence_status matrix_set_presence_status );
 4 | 
 5 | =head2 matrix_get_presence_status
 6 | 
 7 |    $status = matrix_get_presence_status( $user )
 8 | 
 9 | Returns a HASH reference containing the user's presence status. This will
10 | contain a C field, and optionally a C field as well if
11 | the user has one set.
12 | 
13 | =cut
14 | 
15 | sub matrix_get_presence_status
16 | {
17 |    my ( $user ) = @_;
18 | 
19 |    do_request_json_for( $user,
20 |       method => "GET",
21 |       uri    => "/v3/presence/:user_id/status",
22 |    );
23 | }
24 | 
25 | =head2 matrix_set_presence_status
26 | 
27 |    matrix_set_presence_status( $user, $presence, %params )
28 | 
29 | Sets the presence status of the given C<$user> to C<$presence>, with optional
30 | additional parameters (such as C) given in C<%params>.
31 | 
32 | =cut
33 | 
34 | sub matrix_set_presence_status
35 | {
36 |    my ( $user, $presence, %params ) = @_;
37 | 
38 |    do_request_json_for( $user,
39 |       method => "PUT",
40 |       uri    => "/v3/presence/:user_id/status",
41 | 
42 |       content => { presence => $presence, %params }
43 |    )->then_done();
44 | }
45 | 
46 | test "GET /presence/:user_id/status fetches initial status",
47 |    requires => [ $fixture ],
48 | 
49 |    check => sub {
50 |       my ( $user ) = @_;
51 | 
52 |       matrix_get_presence_status( $user )->then( sub {
53 |          my ( $body ) = @_;
54 | 
55 |          assert_json_keys( $body, qw( presence ));
56 | 
57 |          # TODO(paul): Newly-registered users might not yet have a
58 |          #   last_active_ago
59 |          # assert_json_number( $body->{last_active_ago} );
60 |          # $body->{last_active_ago} >= 0 or
61 |          #    die "Expected last_active_ago non-negative";
62 | 
63 |          Future->done(1);
64 |       });
65 |    };
66 | 
67 | my $status_msg = "Testing something";
68 | 
69 | test "PUT /presence/:user_id/status updates my presence",
70 |    requires => [ $fixture ],
71 | 
72 |    proves => [qw( can_set_presence )],
73 | 
74 |    check => sub {
75 |       my ( $user ) = @_;
76 | 
77 |       matrix_set_presence_status( $user, "online",
78 |          status_msg => $status_msg,
79 |       )->then( sub {
80 |          # If presence is on a different worker it may take a while for it to
81 |          # propagate.
82 |          retry_until_success {
83 |             matrix_get_presence_status( $user )->then( sub {
84 |                my ( $body ) = @_;
85 | 
86 |                ( $body->{status_msg} // "" ) eq $status_msg or
87 |                   die "Incorrect status_msg";
88 | 
89 |                Future->done(1);
90 |             });
91 |          }
92 |       })
93 |    };
94 | 


--------------------------------------------------------------------------------
/tests/10apidoc/32room-alias.pl:
--------------------------------------------------------------------------------
  1 | my $user_fixture = local_user_fixture();
  2 | 
  3 | my $room_fixture = fixture(
  4 |    requires => [ $user_fixture ],
  5 | 
  6 |    setup => sub {
  7 |       my ( $user ) = @_;
  8 | 
  9 |       matrix_create_room_synced( $user )->then( sub {
 10 |          my ( $room_id, undef ) = @_;
 11 |          Future->done( $room_id );  # Don't return the alias
 12 |       });
 13 |    },
 14 | );
 15 | 
 16 | test "PUT /directory/room/:room_alias creates alias",
 17 |    requires => [ $user_fixture, $room_fixture, room_alias_fixture() ],
 18 | 
 19 |    proves => [qw( can_create_room_alias can_lookup_room_alias )],
 20 | 
 21 |    do => sub {
 22 |       my ( $user, $room_id, $room_alias ) = @_;
 23 | 
 24 |       do_request_json_for( $user,
 25 |          method => "PUT",
 26 |          uri    => "/v3/directory/room/$room_alias",
 27 | 
 28 |          content => {
 29 |             room_id => $room_id,
 30 |          },
 31 |       );
 32 |    },
 33 | 
 34 |    check => sub {
 35 |       my ( $user, $room_id, $room_alias ) = @_;
 36 | 
 37 |       do_request_json_for( $user,
 38 |          method => "GET",
 39 |          uri    => "/v3/directory/room/$room_alias",
 40 |       )->then( sub {
 41 |          my ( $body ) = @_;
 42 | 
 43 |          assert_json_keys( $body, qw( room_id servers ));
 44 |          assert_json_list( $body->{servers} );
 45 | 
 46 |          $body->{room_id} eq $room_id or die "Expected room_id";
 47 | 
 48 |          Future->done(1);
 49 |       });
 50 |    };
 51 | 
 52 | test "GET /rooms/:room_id/aliases lists aliases",
 53 |    requires => [ $user_fixture, room_alias_fixture(), qw( can_create_room_alias )],
 54 | 
 55 |    do => sub {
 56 |       my ( $user, $room_alias ) = @_;
 57 |       my ( $room_id );
 58 | 
 59 |       matrix_create_room_synced( $user )->then( sub {
 60 |          ( $room_id ) = @_;
 61 | 
 62 |          # the list should be empty initially
 63 |          do_request_json_for(
 64 |             $user,
 65 |             method => "GET",
 66 |             uri => "/v3/rooms/$room_id/aliases",
 67 |          );
 68 |       })->then( sub {
 69 |          my ( $res ) = @_;
 70 |          log_if_fail "response from /aliases before change:", $res;
 71 | 
 72 |          assert_json_keys( $res, qw( aliases ));
 73 |          assert_json_empty_list( $res->{aliases} );
 74 | 
 75 |          # now add an alias
 76 |          do_request_json_for(
 77 |             $user,
 78 |             method => "PUT",
 79 |             uri    => "/v3/directory/room/$room_alias",
 80 |             content => { room_id => $room_id },
 81 |          );
 82 |       })->then( sub {
 83 |          my ( $res ) = @_;
 84 |          log_if_fail "response from PUT /directory:", $res;
 85 | 
 86 |          # ... and recheck. Might need to try this a few times while the caches
 87 |          # get flushed.
 88 |          retry_until_success {
 89 |             my ( $iter ) = @_;
 90 |             return do_request_json_for(
 91 |                $user,
 92 |                method => "GET",
 93 |                uri => "/v3/rooms/$room_id/aliases",
 94 |             )->then( sub {
 95 |                my ( $res ) = @_;
 96 |                log_if_fail "$iter: response from /aliases", $res;
 97 |                assert_json_keys( $res, qw( aliases ));
 98 |                assert_deeply_eq($res->{aliases}, [ $room_alias ]);
 99 |                Future->done;
100 |             });
101 |          }
102 |       });
103 |    };
104 | 


--------------------------------------------------------------------------------
/tests/10apidoc/35room-typing.pl:
--------------------------------------------------------------------------------
 1 | test "PUT /rooms/:room_id/typing/:user_id sets typing notification",
 2 |    requires => [ local_user_and_room_fixtures() ],
 3 | 
 4 |    proves => [qw( can_set_room_typing )],
 5 | 
 6 |    do => sub {
 7 |       my ( $user, $room_id ) = @_;
 8 | 
 9 |       do_request_json_for( $user,
10 |          method => "PUT",
11 |          uri    => "/v3/rooms/$room_id/typing/:user_id",
12 | 
13 |          content => { typing => JSON::true },
14 |       )->then( sub {
15 |          my ( $body ) = @_;
16 | 
17 |          # Body is empty
18 | 
19 |          Future->done(1);
20 |       });
21 |    };
22 | 


--------------------------------------------------------------------------------
/tests/10apidoc/36room-levels.pl:
--------------------------------------------------------------------------------
 1 | # Tests migrated to Complement as of https://github.com/matrix-org/complement/pull/545
 2 | # However this helper function is used in other tests.
 3 | 
 4 | push our @EXPORT, qw( matrix_change_room_power_levels );
 5 | 
 6 | sub matrix_change_room_power_levels
 7 | {
 8 |    my ( $user, $room_id, $func ) = @_;
 9 |    is_User( $user ) or croak "Expected a User; got $user";
10 | 
11 |    matrix_get_room_state( $user, $room_id, type => "m.room.power_levels" )
12 |    ->then( sub {
13 |       my ( $levels ) = @_;
14 |       $func->( $levels );
15 | 
16 |       matrix_put_room_state_synced( $user, $room_id, type => "m.room.power_levels",
17 |          content => $levels,
18 |       );
19 |    });
20 | }
21 | 


--------------------------------------------------------------------------------
/tests/10apidoc/37room-receipts.pl:
--------------------------------------------------------------------------------
 1 | use URI::Escape qw( uri_escape );
 2 | 
 3 | test "POST /rooms/:room_id/receipt can create receipts",
 4 |    requires => [ local_user_and_room_fixtures() ],
 5 | 
 6 |    proves => [qw( can_post_room_receipts )],
 7 | 
 8 |    do => sub {
 9 |       my ( $user, $room_id ) = @_;
10 | 
11 |       # We need an event ID in the room. The ID of our own member event seems
12 |       # reasonable. Lets fetch it.
13 |       matrix_get_my_member_event( $user, $room_id )->then( sub {
14 |          my ( $member_event ) = @_;
15 |          my $event_id = uri_escape( $member_event->{event_id} );
16 | 
17 |          do_request_json_for( $user,
18 |             method => "POST",
19 |             uri    => "/v3/rooms/$room_id/receipt/m.read/$event_id",
20 | 
21 |             content => {},
22 |          );
23 |       })->then_done(1);
24 |    };
25 | 
26 | push our @EXPORT, qw( matrix_advance_room_receipt matrix_advance_room_receipt_synced );
27 | 
28 | sub matrix_advance_room_receipt
29 | {
30 |    my ( $user, $room_id, $type, $event_id ) = @_;
31 | 
32 |    $event_id = uri_escape( $event_id );
33 | 
34 |    do_request_json_for( $user,
35 |       method => "POST",
36 |       uri    => "/v3/rooms/$room_id/receipt/$type/$event_id",
37 | 
38 |       content => {},
39 |    )->then_done();
40 | }
41 | 
42 | sub matrix_advance_room_receipt_synced
43 | {
44 |    my ( $user, $room_id, $type, $event_id ) = @_;
45 | 
46 |    matrix_do_and_wait_for_sync( $user,
47 |       do => sub {
48 |           matrix_advance_room_receipt( $user, $room_id, $type, $event_id );
49 |       },
50 |       check => sub {
51 |          sync_room_contains( $_[0], $room_id, "ephemeral", sub {
52 |             my ( $receipt ) = @_;
53 | 
54 |             log_if_fail "Receipt", $receipt;
55 |             $receipt->{type} eq "m.receipt" and
56 |                defined $receipt->{content}{$event_id}{$type}{ $user->user_id };
57 |          });
58 |       },
59 |    );
60 | }
61 | 


--------------------------------------------------------------------------------
/tests/10apidoc/38room-read-marker.pl:
--------------------------------------------------------------------------------
 1 | test "POST /rooms/:room_id/read_markers can create read marker",
 2 |    requires => [ local_user_and_room_fixtures() ],
 3 | 
 4 |    proves => [qw( can_post_room_markers )],
 5 | 
 6 |    do => sub {
 7 |       my ( $user, $room_id ) = @_;
 8 | 
 9 |       # We need an event ID in the room. The ID of our own member event seems
10 |       # reasonable. Lets fetch it.
11 |       matrix_get_my_member_event( $user, $room_id )->then( sub {
12 |          my ( $member_event ) = @_;
13 |          my $event_id = $member_event->{event_id};
14 | 
15 |          do_request_json_for( $user,
16 |             method => "POST",
17 |             uri    => "/v3/rooms/$room_id/read_markers",
18 | 
19 |             content => {
20 |                "m.fully_read" => $event_id,
21 |             },
22 |          );
23 |       })->then_done(1);
24 |    };
25 | 
26 | 
27 | push our @EXPORT, qw( matrix_advance_room_read_marker matrix_advance_room_read_marker_synced );
28 | 
29 | sub matrix_advance_room_read_marker
30 | {
31 |    my ( $user, $room_id, $event_id ) = @_;
32 | 
33 |    do_request_json_for( $user,
34 |       method => "POST",
35 |       uri    => "/v3/rooms/$room_id/read_markers",
36 | 
37 |       content => {
38 |          "m.fully_read" => $event_id,
39 |       },
40 |    )->then_done();
41 | }
42 | 
43 | sub matrix_advance_room_read_marker_synced
44 | {
45 |    my ( $user, $room_id, $event_id ) = @_;
46 | 
47 |    matrix_do_and_wait_for_sync( $user,
48 |       do => sub {
49 |           matrix_advance_room_read_marker( $user, $room_id, $event_id );
50 |       },
51 |       check => sub {
52 |          sync_room_contains( $_[0], $room_id, "account_data", sub {
53 |             my ( $read_marker ) = @_;
54 | 
55 |             log_if_fail "Read marker", $read_marker;
56 |             $read_marker->{type} eq "m.fully_read" and
57 |                $read_marker->{content}{event_id} eq $event_id;
58 |          });
59 |       },
60 |    );
61 | }
62 | 


--------------------------------------------------------------------------------
/tests/10apidoc/44account_data.pl:
--------------------------------------------------------------------------------
  1 | push our @EXPORT, qw(
  2 |    matrix_get_account_data matrix_get_room_account_data
  3 |    matrix_add_account_data matrix_add_room_account_data
  4 |    matrix_add_filler_account_data_synced
  5 | );
  6 | 
  7 | =head2 matrix_add_account_data
  8 | 
  9 |    matrix_add_account_data( $user, $type, $content )->get;
 10 | 
 11 | Add account data for the user.
 12 | 
 13 | =cut
 14 | 
 15 | sub matrix_add_account_data
 16 | {
 17 |    my ( $user, $type, $content ) = @_;
 18 | 
 19 |    do_request_json_for( $user,
 20 |       method  => "PUT",
 21 |       uri     => "/v3/user/:user_id/account_data/$type",
 22 |       content => $content
 23 |    );
 24 | }
 25 | 
 26 | =head2 matrix_add_room_account_data
 27 | 
 28 |     matrix_add_account_data( $user, $room_id, $type, $content )->get;
 29 | 
 30 | Add account data for the user for a room.
 31 | 
 32 | =cut
 33 | 
 34 | sub matrix_add_room_account_data
 35 | {
 36 |    my ( $user, $room_id, $type, $content ) = @_;
 37 | 
 38 |    do_request_json_for( $user,
 39 |       method  => "PUT",
 40 |       uri     => "/v3/user/:user_id/rooms/$room_id/account_data/$type",
 41 |       content => $content
 42 |    );
 43 | }
 44 | 
 45 | =head2 matrix_get_account_data
 46 | 
 47 |    matrix_get_account_data( $user, $type, $content )->get;
 48 | 
 49 | Get account data for the user.
 50 | 
 51 | =cut
 52 | 
 53 | sub matrix_get_account_data
 54 | {
 55 |    my ( $user, $type ) = @_;
 56 | 
 57 |    do_request_json_for( $user,
 58 |       method  => "GET",
 59 |       uri     => "/v3/user/:user_id/account_data/$type",
 60 |    );
 61 | }
 62 | 
 63 | =head2 matrix_get_room_account_data
 64 | 
 65 |     matrix_get_account_data( $user, $room_id, $type )->get;
 66 | 
 67 | Get account data for the user for a room.
 68 | 
 69 | =cut
 70 | 
 71 | sub matrix_get_room_account_data
 72 | {
 73 |    my ( $user, $room_id, $type ) = @_;
 74 | 
 75 |    do_request_json_for( $user,
 76 |       method  => "GET",
 77 |       uri     => "/v3/user/:user_id/rooms/$room_id/account_data/$type",
 78 |    );
 79 | }
 80 | 
 81 | sub matrix_add_filler_account_data_synced
 82 | {
 83 |    my ( $user ) = @_;
 84 | 
 85 |    my $random_id = join "", map { chr 64 + rand 63 } 1 .. 20;
 86 |    my $type = "a.made.up.filler.type";
 87 | 
 88 |    matrix_do_and_wait_for_sync( $user,
 89 |       do => sub {
 90 |          matrix_add_account_data( $user, $type, {
 91 |             "id" => $random_id,
 92 |          } );
 93 |       },
 94 |       check => sub {
 95 |          my ( $sync_body ) = @_;
 96 | 
 97 |          my $global_account_data =  $sync_body->{account_data}{events};
 98 | 
 99 |          return any {
100 |             $_->{type} eq $type && $_->{content}{id} eq $random_id
101 |          } @{ $global_account_data };
102 |       },
103 |    );
104 | }
105 | 


--------------------------------------------------------------------------------
/tests/10apidoc/45server-capabilities.pl:
--------------------------------------------------------------------------------
 1 | my $user_fixture = local_user_fixture();
 2 | test "GET /capabilities is present and well formed for registered user",
 3 |    requires => [ $main::API_CLIENTS[0], $user_fixture],
 4 |    do => sub {
 5 |          my ( $http, $user ) = @_;
 6 | 
 7 |          do_request_json_for( $user,
 8 |             method => "GET",
 9 |             uri    => "/v3/capabilities",
10 |          )->then( sub {
11 |             my ( $body ) = @_;
12 |             assert_json_keys( $body->{capabilities}, qw( m.room_versions m.change_password ));
13 |             Future->done(1);
14 |          });
15 |       };
16 | 
17 | 
18 | test "GET /v3/capabilities is not public",
19 |    requires => [ $main::API_CLIENTS[0] ],
20 | 
21 |    do => sub {
22 |       my ( $http ) = @_;
23 | 
24 |       $http->do_request_json(
25 |          method => "GET",
26 |          uri    => "/v3/capabilities",
27 |       )->main::expect_http_401->then( sub {
28 |          Future->done( 1 );
29 |       })
30 |    };
31 | 


--------------------------------------------------------------------------------
/tests/10apidoc/46push.pl:
--------------------------------------------------------------------------------
  1 | use utf8;
  2 | 
  3 | push our @EXPORT, qw(
  4 |    matrix_add_push_rule matrix_delete_push_rule
  5 |    matrix_get_push_rule matrix_get_push_rules
  6 |    matrix_set_push_rule_enabled
  7 |    matrix_set_push_rule_actions
  8 | );
  9 | 
 10 | =head2 matrix_add_push_rule
 11 | 
 12 |    matrix_add_push_rule( $user, $scope, $kind, $rule_id, $rule, %params )->get
 13 | 
 14 | scope: Either "global" or "device/"
 15 | kind: Either "override", "underride", "sender", "room", or "content"
 16 | rule_id: String id for the rule.
 17 | rule: Hash reference for the body.
 18 | params: Extra query params for the request. E.g. "before" or "after".
 19 | 
 20 | =cut
 21 | 
 22 | sub matrix_add_push_rule
 23 | {
 24 |    my ( $user, $scope, $kind, $rule_id, $rule_body, %params ) = @_;
 25 | 
 26 |    do_request_json_for( $user,
 27 |       method  => "PUT",
 28 |       uri     => "/v3/pushrules/$scope/$kind/$rule_id",
 29 |       params  => \%params,
 30 |       content => $rule_body,
 31 |    );
 32 | }
 33 | 
 34 | =head2 matrix_delete_push_rule
 35 | 
 36 |    matrix_delete_push_rule( $user, $scope, $kind, $rule_id )->get
 37 | 
 38 | scope: Either "global" or "device/"
 39 | kind: Either "override", "underride", "sender", "room", or "content"
 40 | rule_id: String id for the rule.
 41 | 
 42 | =cut
 43 | 
 44 | sub matrix_delete_push_rule
 45 | {
 46 |    my ( $user, $scope, $kind, $rule_id ) = @_;
 47 | 
 48 |    do_request_json_for( $user,
 49 |       method  => "DELETE",
 50 |       uri     => "/v3/pushrules/$scope/$kind/$rule_id",
 51 |    );
 52 | }
 53 | 
 54 | =head2 matrix_set_push_rule_enabled
 55 | 
 56 |    matrix_set_push_rule_enabled( $user, $scope, $kind, $rule_id, $enabled )->get
 57 | 
 58 | scope: Either "global" or "device/"
 59 | kind: Either "override", "underride", "sender", "room", or "content"
 60 | rule_id: String id for the rule.
 61 | enabled: JSON::true or JSON::false
 62 | 
 63 | =cut
 64 | 
 65 | sub matrix_set_push_rule_enabled
 66 | {
 67 |    my ( $user, $scope, $kind, $rule_id, $enabled ) = @_;
 68 | 
 69 |    do_request_json_for( $user,
 70 |       method  => "PUT",
 71 |       uri     => "/v3/pushrules/$scope/$kind/$rule_id/enabled",
 72 |       content => { enabled => $enabled },
 73 |    );
 74 | }
 75 | 
 76 | =head2 matrix_set_push_rule_actions
 77 | 
 78 |    matrix_set_push_rule_actions( $user, $scope, $kind, $rule_id, $actions )->get
 79 | 
 80 | scope: Either "global" or "device/"
 81 | kind: Either "override", "underride", "sender", "room", or "content"
 82 | rule_id: String id for the rule.
 83 | enabled: array of actions.
 84 | 
 85 | =cut
 86 | 
 87 | sub matrix_set_push_rule_actions
 88 | {
 89 |    my ( $user, $scope, $kind, $rule_id, $actions ) = @_;
 90 | 
 91 |    do_request_json_for( $user,
 92 |       method  => "PUT",
 93 |       uri     => "/v3/pushrules/$scope/$kind/$rule_id/actions",
 94 |       content => { actions => $actions },
 95 |    );
 96 | }
 97 | 
 98 | =head2 matrix_get_push_rule
 99 | 
100 |    my $rule = matrix_get_push_rule( $user, $scope, $kind, $rule_id )->get
101 | 
102 | scope: Either "global" or "device/"
103 | kind: Either "override", "underride", "sender", "room", or "content"
104 | rule_id: String id for the rule.
105 | 
106 | Returns a hash reference with the rule body.
107 | 
108 | =cut
109 | 
110 | sub matrix_get_push_rule
111 | {
112 |    my ( $user, $scope, $kind, $rule_id ) = @_;
113 | 
114 |    do_request_json_for( $user,
115 |       method  => "GET",
116 |       uri     => "/v3/pushrules/$scope/$kind/$rule_id",
117 |    );
118 | }
119 | 
120 | =head2 matrix_get_push_rules
121 | 
122 |    my $rules = matrix_get_push_rules( $user )->get
123 | 
124 | Returns a hash reference with all the rules for the user
125 | 
126 | =cut
127 | 
128 | sub matrix_get_push_rules
129 | {
130 |    my ( $user ) = @_;
131 | 
132 |    # Trailing slash indicates retrieving ALL push rules for this user
133 |    do_request_json_for( $user,
134 |       method  => "GET",
135 |       uri     => "/v3/pushrules/",
136 |    )->on_done( sub {
137 |       my ( $body ) = @_;
138 | 
139 |       assert_json_keys( $body, qw( global ) );
140 |    });
141 | }
142 | 


--------------------------------------------------------------------------------
/tests/12login/01threepid-and-password.pl:
--------------------------------------------------------------------------------
 1 | my $password = "my secure password";
 2 | 
 3 | test "Can login with 3pid and password using m.login.password",
 4 |    requires => [ local_user_fixture( password => $password ), id_server_fixture() ],
 5 | 
 6 |    check => sub {
 7 |       my ( $user, $id_server ) = @_;
 8 | 
 9 |       my $http = $user->http;
10 | 
11 |       my $address = 'bob@example.com';
12 | 
13 |       add_email_for_user( $user, $address, $id_server )
14 |       ->then( sub {
15 |          $http->do_request_json(
16 |             method => "POST",
17 |             uri    => "/v3/login",
18 | 
19 |             content => {
20 |                type     => "m.login.password",
21 |                medium   => 'email',
22 |                address  => $address,
23 |                password => $password,
24 |             }
25 |          );
26 |       })->then( sub {
27 |          my ( $body ) = @_;
28 | 
29 |          assert_json_keys( $body, qw( access_token ));
30 | 
31 |          if (defined $body->{home_server}) {
32 |             assert_eq( $body->{home_server}, $http->server_name,
33 |                'Response home_server' );
34 |          }
35 | 
36 |          Future->done(1);
37 |       });
38 |    };
39 | 


--------------------------------------------------------------------------------
/tests/12login/02cas.pl:
--------------------------------------------------------------------------------
 1 | use URI::Escape;
 2 | 
 3 | my $CAS_SUCCESS = generate_cas_response( 'cas_user!' );
 4 | 
 5 | test "login types include SSO",
 6 |    requires => [ $main::API_CLIENTS[0] ],
 7 | 
 8 |    check => sub {
 9 |       my ( $http ) = @_;
10 | 
11 |       $http->do_request_json(
12 |          method => "GET",
13 |          uri => "/v3/login",
14 |       )->then( sub {
15 |          my ( $body ) = @_;
16 | 
17 |          assert_json_keys( $body, qw( flows ));
18 |          assert_json_list $body->{flows};
19 | 
20 |          die "m.login.sso was not listed" unless
21 |             any { $_->{type} eq "m.login.sso" } @{ $body->{flows} };
22 | 
23 |          Future->done( 1 );
24 |       });
25 |    };
26 | 
27 | 
28 | test "/login/cas/redirect redirects if the old m.login.cas login type is listed",
29 |    requires => [
30 |       $main::TEST_SERVER_INFO, $main::API_CLIENTS[0], cas_login_fixture(),
31 |    ],
32 | 
33 |    do => sub {
34 |       my ( $test_server_info, $http ) = @_;
35 | 
36 |       my $REDIRECT_URL = "https://client?p=http%3A%2F%2Fserver";
37 | 
38 |       $http->do_request(
39 |          method => "GET",
40 |          uri    => "/v3/login/cas/redirect",
41 |          params => {
42 |             redirectUrl => $REDIRECT_URL,
43 |          },
44 |          max_redirects => 0,
45 |       )->main::expect_http_302->then( sub {
46 |          my ( $resp ) = @_;
47 |          my $loc = $resp->header( "Location" );
48 |          my $expected = $test_server_info->client_location . "/cas/login?";
49 |          die "unexpected location '$loc' (expected '$expected...')" unless
50 |             $loc =~ /^\Q$expected/;
51 |          Future->done(1);
52 |       });
53 |    };
54 | 
55 | test "Can login with new user via CAS",
56 |    requires => [
57 |       $main::API_CLIENTS[0],
58 |       $main::HOMESERVER_INFO[0],
59 |    ],
60 | 
61 |    do => sub {
62 |       my ( $http, $homeserver_info ) = @_;
63 | 
64 |       # Ensure the base login works without issue.
65 |       matrix_login_with_cas(
66 |          '@cas_user=21:' . $http->server_name,
67 |          $http,
68 |          $homeserver_info,
69 |          $CAS_SUCCESS,
70 |       );
71 |    };
72 | 


--------------------------------------------------------------------------------
/tests/14account/02deactivate.pl:
--------------------------------------------------------------------------------
  1 | use JSON qw( decode_json );
  2 | 
  3 | sub matrix_deactivate_account
  4 | {
  5 |    my ( $user, %opts ) = @_;
  6 | 
  7 |    # use the user's password unless one was given in opts
  8 |    my $password = (delete $opts{password}) // $user->password;
  9 | 
 10 |    do_request_json_for( $user,
 11 |       method  => "POST",
 12 |       uri     => "/v3/account/deactivate",
 13 |       content => {
 14 |          auth => {
 15 |             type     => "m.login.password",
 16 |             identifier => {
 17 |                type => "m.id.user",
 18 |                user => $user->user_id,
 19 |             },
 20 |             password => $password,
 21 |          },
 22 |          %opts,
 23 |       },
 24 |    );
 25 | }
 26 | push our @EXPORT, qw( matrix_deactivate_account );
 27 | 
 28 | test "Can deactivate account",
 29 |    requires => [ local_user_fixture() ],
 30 | 
 31 |    check => sub {
 32 |       my ( $user ) = @_;
 33 | 
 34 |       matrix_deactivate_account( $user );
 35 |    };
 36 | 
 37 | test "Can't deactivate account with wrong password",
 38 |    requires => [ local_user_fixture() ],
 39 | 
 40 |    check => sub {
 41 |       my ( $user ) = @_;
 42 | 
 43 |       matrix_deactivate_account( $user, password=>"wrong password" )
 44 |       ->main::expect_http_401->then( sub {
 45 |          my ( $resp ) = @_;
 46 | 
 47 |          my $body = decode_json $resp->content;
 48 | 
 49 |          assert_json_keys( $body, qw( error errcode params completed flows ));
 50 | 
 51 |          my $errcode = $body->{errcode};
 52 | 
 53 |          $errcode eq "M_FORBIDDEN" or
 54 |             die "Expected errcode to be M_FORBIDDEN but was $errcode";
 55 | 
 56 |          Future->done(1);
 57 |       });
 58 |    };
 59 | 
 60 | test "After deactivating account, can't log in with password",
 61 |    requires => [ local_user_fixture() ],
 62 | 
 63 |    check => sub {
 64 |       my ( $user ) = @_;
 65 | 
 66 |       matrix_deactivate_account( $user )
 67 |       ->then( sub {
 68 |          do_request_json_for( $user,
 69 |             method  => "POST",
 70 |             uri     => "/v3/login",
 71 |             content => {
 72 |                type     => "m.login.password",
 73 |                identifier => {
 74 |                   type => "m.id.user",
 75 |                   user => $user->user_id,
 76 |                },
 77 |                password => $user->password,
 78 |             }
 79 |          # We don't mandate the exact failure code here
 80 |          # (that should be done in the login test if
 81 |          # anywhere), any 4xx code is fine as far as
 82 |          # this test is concerned.
 83 |          )->main::expect_http_4xx;
 84 |       });
 85 |    };
 86 | 
 87 | test "After deactivating account, can't log in with an email",
 88 |    requires => [ local_user_fixture(), id_server_fixture() ],
 89 | 
 90 |    check => sub {
 91 |       my ( $user, $id_server ) = @_;
 92 | 
 93 |       add_email_for_user(
 94 |          $user, 'bob@example.com', $id_server,
 95 |       )->then( sub {
 96 |          matrix_deactivate_account( $user )
 97 |       })->then( sub {
 98 |          do_request_json_for( $user,
 99 |             method  => "POST",
100 |             uri     => "/v3/login",
101 |             content => {
102 |                identifier => {
103 |                   "type"    => "m.id.thirdparty",
104 |                   "medium"  => "email",
105 |                   "address" => 'bob@example.com',
106 |                }
107 |             }
108 |          );
109 |       })->main::expect_http_4xx;
110 |    };
111 | 


--------------------------------------------------------------------------------
/tests/21presence-events.pl:
--------------------------------------------------------------------------------
  1 | # Eventually this will be changed; see SPEC-53
  2 | my $PRESENCE_LIST_URI = "/v3/presence/list/:user_id";
  3 | 
  4 | 
  5 | test "initialSync sees my presence status",
  6 |    deprecated_endpoints => 1,
  7 |    requires => [ local_user_fixture( with_events => 1 ),
  8 |                  qw( can_initial_sync )],
  9 | 
 10 |    check => sub {
 11 |       my ( $user ) = @_;
 12 | 
 13 |       # We add a filler account data entry to ensure that replication is up to
 14 |       # date with account creation. Really this should be a synced presence
 15 |       # set
 16 |       matrix_add_filler_account_data_synced ( $user )->then( sub {
 17 |          matrix_initialsync( $user )
 18 |       })->then( sub {
 19 |          my ( $body ) = @_;
 20 | 
 21 |          assert_json_keys( $body, qw( presence ));
 22 | 
 23 |          log_if_fail "Initial sync presence", $body->{presence};
 24 | 
 25 |          my $event = first {
 26 |             ( $_->{content}{user_id} // "" ) eq $user->user_id
 27 |          } @{ $body->{presence} } or
 28 |             die "Did not find an initial presence message about myself";
 29 | 
 30 |          assert_json_object( $event, qw( type content ));
 31 |          $event->{type} eq "m.presence" or
 32 |             die "Expected type of event to be m.presence";
 33 | 
 34 |          my $content = $event->{content};
 35 |          assert_json_object( $content, qw( user_id presence last_active_ago ));
 36 | 
 37 |          Future->done(1);
 38 |       });
 39 |    };
 40 | 
 41 | my $status_msg = "A status set by 21presence-events.pl";
 42 | 
 43 | test "Presence change reports an event to myself",
 44 |    requires => [ local_user_fixture(),
 45 |                  qw( can_set_presence )],
 46 | 
 47 |    do => sub {
 48 |       my ( $user ) = @_;
 49 | 
 50 |       matrix_set_presence_status( $user, "online",
 51 |          status_msg => $status_msg,
 52 |       )->then( sub {
 53 |          await_sync_presence_contains( $user, check => sub {
 54 |             my ( $event ) = @_;
 55 | 
 56 |             log_if_fail "Event", $event;
 57 | 
 58 |             return 0 unless $event->{sender} eq $user->user_id;
 59 | 
 60 |             my $content = $event->{content};
 61 |             assert_eq( $content->{status_msg} // "", $status_msg);
 62 | 
 63 |             return 1;
 64 |          });
 65 |       });
 66 |    };
 67 | 
 68 | my $friend_status = "Status of a Friend";
 69 | 
 70 | test "Friends presence changes reports events",
 71 |    deprecated_endpoints => 1,
 72 |    requires => [ local_user_fixture(), local_user_fixture(),
 73 |                  qw( can_set_presence can_invite_presence )],
 74 | 
 75 |    do => sub {
 76 |       my ( $user, $friend ) = @_;
 77 | 
 78 |       do_request_json_for( $user,
 79 |          method => "POST",
 80 |          uri    => $PRESENCE_LIST_URI,
 81 | 
 82 |          content => {
 83 |             invite => [ $friend->user_id ],
 84 |          }
 85 |       )->then( sub {
 86 |          matrix_sync( $user )
 87 |       })->then( sub {
 88 |          matrix_set_presence_status( $friend, "online",
 89 |             status_msg => $friend_status,
 90 |          );
 91 |       })->then( sub {
 92 |          await_sync_presence_contains( $user, check => sub {
 93 |             my ( $event ) = @_;
 94 | 
 95 |             assert_json_keys( $event, qw( sender ));
 96 | 
 97 |             return unless $event->{sender} eq $friend->user_id;
 98 | 
 99 |             my $content = $event->{content};
100 | 
101 |             assert_json_keys( $content, qw( presence status_msg ));
102 |             $content->{presence} eq "online" or
103 |                die "Expected presence to be 'online'";
104 |             $content->{status_msg} eq $friend_status or
105 |                die "Expected status_msg to be '$friend_status'";
106 | 
107 |             return 1;
108 |          });
109 |       });
110 |    };
111 | 


--------------------------------------------------------------------------------
/tests/30rooms/09eventstream.pl:
--------------------------------------------------------------------------------
  1 | use Future::Utils qw( repeat try_repeat );
  2 | 
  3 | 
  4 | multi_test "Check that event streams started after a client joined a room work (SYT-1)",
  5 |    deprecated_endpoints => 1,
  6 |    requires => [ local_user_fixture(),
  7 |       qw( can_create_private_room can_send_message )
  8 |    ],
  9 | 
 10 |    do => sub {
 11 |       my ( $alice ) = @_;
 12 | 
 13 |       my $room_id;
 14 | 
 15 |       # Have Alice create a new private room
 16 |       matrix_create_room_synced( $alice,
 17 |          visibility => "private",
 18 |       )->SyTest::pass_on_done( "Created a room" )
 19 |       ->then( sub {
 20 |          ( $room_id ) = @_;
 21 |          # Now that we've joined a room, flush the event stream to get
 22 |          # a stream token from before we send a message.
 23 |          flush_events_for( $alice );
 24 |       })->then( sub {
 25 |          # Alice sends a message
 26 |          matrix_send_room_message( $alice, $room_id,
 27 |             content => {
 28 |                msgtype => "m.message",
 29 |                body    => "Room message for 90jira-SYT-1"
 30 |             },
 31 |          )
 32 |       })->then( sub {
 33 |          my ( $event_id ) = @_;
 34 | 
 35 |          # Wait for the message we just sent.
 36 |          await_event_for( $alice, filter => sub {
 37 |             my ( $event ) = @_;
 38 |             return unless $event->{type} eq "m.room.message";
 39 |             return unless $event->{event_id} eq $event_id;
 40 |             return 1;
 41 |          })->SyTest::pass_on_done( "Alice saw her message" )
 42 |       })->then_done(1);
 43 |    };
 44 | 
 45 | 
 46 | test "Event stream catches up fully after many messages",
 47 |    deprecated_endpoints => 1,
 48 |    requires => [ local_user_fixture(),
 49 |                  qw( can_send_message )],
 50 | 
 51 |    do => sub {
 52 |       my ( $user ) = @_;
 53 |       my ( $room_id, @expected_event_ids );
 54 | 
 55 |       matrix_create_room_synced( $user,
 56 |         visibility => "private",
 57 |       )
 58 |       ->then( sub {
 59 |          ( $room_id ) = @_;
 60 | 
 61 |          flush_events_for( $user )
 62 |       })->then( sub {
 63 |          repeat( sub {
 64 |             my ( $msgnum ) = @_;
 65 | 
 66 |             matrix_send_room_text_message( $user, $room_id,
 67 |                body => "Message number $msgnum"
 68 |             )->on_done( sub {
 69 |                push @expected_event_ids, @_;
 70 |             })
 71 |          }, foreach => [ 0 .. 19 ] )
 72 |       })->then( sub {
 73 |          try_repeat( sub {
 74 |             matrix_get_events( $user,
 75 |                from    => $user->eventstream_token,
 76 |                timeout => 500,
 77 |                limit   => 5,
 78 |             )->on_done( sub {
 79 |                my ( $body ) = @_;
 80 |                my ( $expected_id );
 81 |                my ( @events ) = @{ $body->{chunk} };
 82 | 
 83 |                $user->eventstream_token = $body->{end};
 84 | 
 85 |                log_if_fail "Events", @events;
 86 | 
 87 |                foreach my $event ( @events ) {
 88 |                   if ( $event->{type} eq 'm.room.message' ) {
 89 |                      $expected_id = shift @expected_event_ids;
 90 | 
 91 |                      assert_eq( $event->{event_id}, $expected_id,
 92 |                         'Unexpected or out of order event'
 93 |                      );
 94 |                   }
 95 |                }
 96 |             })
 97 |          }, foreach => [ 0 .. 10 ], while => sub {
 98 |             @expected_event_ids > 0
 99 |          })
100 |       })->then( sub {
101 |          @expected_event_ids == 0 or die "Did not see all events.";
102 | 
103 |          Future->done(1);
104 |       });
105 |    };
106 | 


--------------------------------------------------------------------------------
/tests/30rooms/14override-per-room.pl:
--------------------------------------------------------------------------------
 1 | test "Room members can override their displayname on a room-specific basis",
 2 |    requires => [ local_user_and_room_fixtures() ],
 3 | 
 4 |    do => sub {
 5 |       my ( $user, $room_id ) = @_;
 6 | 
 7 |       matrix_put_room_state( $user, $room_id,
 8 |          type      => "m.room.member",
 9 |          state_key => $user->user_id,
10 |          content   => {
11 |             membership => "join",
12 |             displayname => "Overridden",
13 |          },
14 |       )->then( sub {
15 |          matrix_get_room_state( $user, $room_id,
16 |             type      => "m.room.member",
17 |             state_key => $user->user_id,
18 |          );
19 |       })->then( sub {
20 |          my ( $state ) = @_;
21 | 
22 |          log_if_fail "State", $state;
23 | 
24 |          assert_eq( $state->{displayname}, "Overridden",
25 |             'displayname in my m.room.member event' );
26 | 
27 |          Future->done(1);
28 |       });
29 |    };
30 | 
31 | test "Room members can join a room with an overridden displayname",
32 |    requires => [ local_user_and_room_fixtures(), local_user_fixture() ],
33 | 
34 |    do => sub {
35 |       my ( $creator, $room_id, $joiner ) = @_;
36 | 
37 |       # PUT'ing my membership state should join me
38 |       matrix_put_room_state( $joiner, $room_id,
39 |          type      => "m.room.member",
40 |          state_key => $joiner->user_id,
41 |          content   => {
42 |             membership => "join",
43 |             displayname => "Overridden",
44 |          },
45 |       )->then( sub {
46 |          retry_until_success {
47 |             matrix_get_room_state( $creator, $room_id,
48 |                type      => "m.room.member",
49 |                state_key => $joiner->user_id,
50 |             )->then( sub {
51 |                my ( $state ) = @_;
52 | 
53 |                log_if_fail "State", $state;
54 | 
55 |                assert_eq( $state->{displayname}, "Overridden",
56 |                   'displayname in my m.room.member event at join time' );
57 | 
58 |                Future->done(1);
59 |             })
60 |          }
61 |       });
62 |    };
63 | 


--------------------------------------------------------------------------------
/tests/30rooms/15kick.pl:
--------------------------------------------------------------------------------
 1 | my $creator_fixture = local_user_fixture();
 2 | 
 3 | test "Users cannot kick users from a room they are not in",
 4 |    requires => [ $creator_fixture,
 5 |                  magic_room_fixture( requires_users => [ $creator_fixture ] )
 6 |                ],
 7 | 
 8 |    do => sub {
 9 |       my ( $creator, $room_id ) = @_;
10 |       my $fake_user_id = '@bob:example.com';
11 | 
12 |       do_request_json_for( $creator,
13 |          method => "POST",
14 |          uri    => "/v3/rooms/$room_id/kick",
15 | 
16 |          content => { user_id => $fake_user_id, reason => "testing" },
17 |       )->main::expect_http_403; # 403 for kicking a user who isn't in the room
18 |    };
19 | 
20 | my $kicked_user_fixture = local_user_fixture();
21 | 
22 | test "Users cannot kick users who have already left a room",
23 |     requires => [ $creator_fixture, $kicked_user_fixture,
24 |                     magic_room_fixture( requires_users => [ $creator_fixture, $kicked_user_fixture ] )
25 |                 ],
26 | 
27 |     do => sub {
28 |         my ( $creator, $kicked_user, $room_id ) = @_;
29 | 
30 |         do_request_json_for( $creator,
31 |            method => "POST",
32 |            uri    => "/v3/rooms/$room_id/kick",
33 | 
34 |            content => { user_id => $kicked_user->user_id, reason => "testing" },
35 |         )->then( sub {
36 |             retry_until_success {
37 |                 matrix_get_room_state( $creator, $room_id,
38 |                     type      => "m.room.member",
39 |                     state_key => $kicked_user->user_id,
40 |                 )->then( sub {
41 |                     my ( $body ) = @_;
42 |                     $body->{membership} eq "leave" or
43 |                         die "Expected kicked user membership to be 'leave'";
44 | 
45 |                     Future->done ( 1 );
46 |                 })
47 |             }
48 |         })->then( sub {
49 |             do_request_json_for( $creator,
50 |                 method => "POST",
51 |                 uri    => "/v3/rooms/$room_id/kick",
52 | 
53 |                 content => { user_id => $kicked_user->user_id, reason => "testing" },
54 |             )->main::expect_http_403; # 403 for kicking a user who isn't in the room anymore
55 |         })
56 |    };
57 | 


--------------------------------------------------------------------------------
/tests/30rooms/22profile.pl:
--------------------------------------------------------------------------------
 1 | foreach my $datum (qw( displayname avatar_url )) {
 2 |    test "$datum updates affect room member events",
 3 |       requires => [ local_user_and_room_fixtures() ],
 4 | 
 5 |       do => sub {
 6 |          my ( $user, $room_id ) = @_;
 7 | 
 8 |          my $uri = "/v3/profile/:user_id/$datum";
 9 | 
10 |          do_request_json_for( $user,
11 |             method => "GET",
12 |             uri    => $uri,
13 |          )->then( sub {
14 |             my ( $body ) = @_;
15 | 
16 |             # N.B. nowadays we let servers specify default displayname & avatar_url
17 |             # previously we asserted that these must be undefined at this point.
18 | 
19 |             do_request_json_for( $user,
20 |                method  => "PUT",
21 |                uri     => $uri,
22 |                content => {
23 |                   $datum => "LemurLover",
24 |                },
25 |             )
26 |          })->then( sub {
27 |              await_sync_timeline_or_state_contains( $user, $room_id, check => sub {
28 |                my ( $event ) = @_;
29 | 
30 |                return unless $event->{type} eq "m.room.member";
31 |                return unless $event->{state_key} eq $user->user_id;
32 |                return unless $event->{content}->{$datum} eq "LemurLover";
33 | 
34 |                return 1;
35 |             });
36 |          })->then( sub {
37 |             do_request_json_for( $user,
38 |                method => "GET",
39 |                uri    => "/v3/rooms/$room_id/state/m.room.member/:user_id",
40 |             )
41 |          })->then( sub {
42 |             my ( $body ) = @_;
43 | 
44 |             assert_eq( $body->{$datum}, "LemurLover", "Room $datum" );
45 | 
46 |             Future->done( 1 );
47 |          });
48 |       };
49 | }
50 | 


--------------------------------------------------------------------------------
/tests/30rooms/32erasure.pl:
--------------------------------------------------------------------------------
 1 | # Copyright 2018 New Vector Ltd
 2 | #
 3 | # Licensed under the Apache License, Version 2.0 (the "License");
 4 | # you may not use this file except in compliance with the License.
 5 | # You may obtain a copy of the License at
 6 | #
 7 | #     http://www.apache.org/licenses/LICENSE-2.0
 8 | #
 9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 | 
15 | use List::Util qw( first );
16 | 
17 | test "Only original members of the room can see messages from erased users",
18 |    requires => [ local_user_and_room_fixtures(), local_user_fixture(), local_user_fixture() ],
19 | 
20 |    do => sub {
21 |       my ( $creator, $room_id, $member, $joiner ) = @_;
22 | 
23 |       my $message_id;
24 |       matrix_join_room_synced( $member, $room_id )
25 |       ->then( sub {
26 |          matrix_send_room_text_message_synced( $creator, $room_id, body => "body1" );
27 |       })->then( sub {
28 |          ( $message_id ) = @_;
29 |          matrix_join_room_synced( $joiner, $room_id );
30 |       })->then( sub {
31 |          # now both users should see the message event
32 |          matrix_sync( $joiner, limit => 4 );
33 |       })->then( sub {
34 |          my ( $body ) = @_;
35 | 
36 |          my $room = $body->{rooms}{join}{$room_id};
37 |          my $events = $room->{timeline}->{events};
38 |          log_if_fail "messages for joining user before erasure", $events;
39 | 
40 |          my $e = first { $_->{event_id} eq $message_id } @$events;
41 |          assert_eq( $e->{type}, "m.room.message", "event type" );
42 |          assert_eq( $e->{content}->{body}, "body1", "event content body" );
43 | 
44 |          matrix_deactivate_account( $creator, erase => JSON::true );
45 |       })->then( sub {
46 |          # now the original member should see the message event, but the joiner
47 |          # should see a redacted version
48 |          matrix_sync( $member );
49 |       })->then( sub {
50 |          my ( $body ) = @_;
51 | 
52 |          my $room = $body->{rooms}{join}{$room_id};
53 |          my $events = $room->{timeline}->{events};
54 |          log_if_fail "messages for original member after erasure", $events;
55 | 
56 |          my $e = first { $_->{event_id} eq $message_id } @$events;
57 |          assert_eq( $e->{type}, "m.room.message", "event type" );
58 |          assert_eq( $e->{content}->{body}, "body1", "event content body" );
59 | 
60 |          matrix_sync( $joiner );
61 |       })->then( sub {
62 |          my ( $body ) = @_;
63 | 
64 |          my $room = $body->{rooms}{join}{$room_id};
65 |          my $events = $room->{timeline}->{events};
66 |          log_if_fail "messages for joining user after erasure", $events;
67 | 
68 |          my $e = first { $_->{event_id} eq $message_id } @$events;
69 |          assert_eq( $e->{type}, "m.room.message", "event type" );
70 |          assert_deeply_eq( $e->{content}, {}, "event content" );
71 | 
72 |          Future->done(1);
73 |       });
74 |    };
75 | 


--------------------------------------------------------------------------------
/tests/30rooms/51event.pl:
--------------------------------------------------------------------------------
  1 | use URI::Escape qw( uri_escape );
  2 | 
  3 | =head2 matrix_get_event
  4 | 
  5 |    my $event = matrix_get_event( $user, $room_id, $event_id ) -> get;
  6 | 
  7 | Makes a /_matrix/client/v3/rooms/{roomId}/event/{eventId} request. Returns the event.
  8 | 
  9 | =cut
 10 | 
 11 | sub matrix_get_event
 12 | {
 13 |    my ( $user, $room_id, $event_id ) = @_;
 14 | 
 15 |    return do_request_json_for(
 16 |       $user,
 17 |       method  => "GET",
 18 |       uri     => "/v3/rooms/${ \uri_escape( $room_id ) }/event/${ \uri_escape( $event_id ) }",
 19 |    );
 20 | }
 21 | 
 22 | push our @EXPORT, qw( matrix_get_event );
 23 | 
 24 | test "/event/ on joined room works",
 25 |    requires => [ local_user_and_room_fixtures() ],
 26 | 
 27 |    check => sub {
 28 |       my ( $user, $room_id ) = @_;
 29 | 
 30 |       matrix_send_room_text_message_synced( $user, $room_id,
 31 |          body => "hello, world",
 32 |       )->then( sub {
 33 |          my ( $event_id ) = @_;
 34 | 
 35 |          do_request_json_for( $user,
 36 |             method  => "GET",
 37 |             uri     => "/v3/rooms/$room_id/event/${ \uri_escape( $event_id ) }",
 38 |          )->then( sub {
 39 |             my ( $body ) = @_;
 40 | 
 41 |             assert_json_keys( $body, qw( content type room_id sender event_id ) );
 42 |             assert_eq( $body->{event_id}, $event_id, "event id" );
 43 |             assert_eq( $body->{room_id}, $room_id, "room id" );
 44 |             assert_eq( $body->{content}->{body}, "hello, world", "body" );
 45 | 
 46 |             Future->done( 1 );
 47 |          });
 48 |       });
 49 |    };
 50 | 
 51 | test "/event/ on non world readable room does not work",
 52 |    requires => [ local_user_and_room_fixtures(), local_user_fixture() ],
 53 | 
 54 |    check => sub {
 55 |       my ( $user, $room_id, $other_user ) = @_;
 56 | 
 57 |       matrix_send_room_text_message_synced( $user, $room_id,
 58 |          body => "hello, world",
 59 |       )->then( sub {
 60 |          my ( $event_id ) = @_;
 61 | 
 62 |          do_request_json_for( $other_user,
 63 |             method  => "GET",
 64 |             uri     => "/v3/rooms/$room_id/event/${ \uri_escape( $event_id ) }",
 65 |          );
 66 |       })->main::expect_http_404;
 67 |    };
 68 | 
 69 | test "/event/ does not allow access to events before the user joined",
 70 |    requires => [
 71 |       local_user_and_room_fixtures(),
 72 |       local_user_fixture(),
 73 |    ],
 74 | 
 75 |    check => sub {
 76 |       my ( $user, $room_id, $other_user ) = @_;
 77 | 
 78 |       my ( $event_id_1, $event_id_2 );
 79 | 
 80 |       matrix_set_room_history_visibility(
 81 |          $user, $room_id, "joined",
 82 |       )->then( sub {
 83 |          matrix_invite_user_to_room_synced(
 84 |             $user, $other_user, $room_id,
 85 |          );
 86 |       })->then( sub {
 87 |          matrix_send_room_text_message_synced( $user, $room_id,
 88 |             body => "before join",
 89 |          );
 90 |       })->then( sub {
 91 |          ( $event_id_1 ) = @_;
 92 | 
 93 |          matrix_join_room_synced( $other_user, $room_id );
 94 |       })->then( sub {
 95 |          matrix_send_room_text_message_synced( $user, $room_id,
 96 |             body => "after join",
 97 |          );
 98 |       })->then( sub {
 99 |          ( $event_id_2 ) = @_;
100 | 
101 |          # we shouldn't be able to get the event before we joined.
102 |          do_request_json_for( $other_user,
103 |             method  => "GET",
104 |             uri     => "/v3/rooms/$room_id/event/${ \uri_escape( $event_id_1 ) }",
105 |          );
106 |       })->main::expect_http_404->then( sub {
107 |          # we should be able to get the event after we joined.
108 |          do_request_json_for( $other_user,
109 |             method  => "GET",
110 |             uri     => "/v3/rooms/$room_id/event/${ \uri_escape( $event_id_2 ) }",
111 |          );
112 |       })->then( sub {
113 |          my ( $body ) = @_;
114 | 
115 |          assert_json_keys( $body, qw( content type room_id sender event_id ) );
116 |          assert_eq( $body->{event_id}, $event_id_2, "event id" );
117 |          assert_eq( $body->{content}->{body}, "after join", "body" );
118 | 
119 |          Future->done(1);
120 |       });
121 |    };
122 | 


--------------------------------------------------------------------------------
/tests/31sync/01filter.pl:
--------------------------------------------------------------------------------
 1 | push our @EXPORT, qw( matrix_create_filter );
 2 | 
 3 | =head2 matrix_create_filter
 4 | 
 5 |    my ( $filter_id ) = matrix_create_filter( $user, \%filter )->get;
 6 | 
 7 | Creates a new filter for the user. Returns the filter id of the new filter.
 8 | 
 9 | =cut
10 | 
11 | sub matrix_create_filter
12 | {
13 |    my ( $user, $filter ) = @_;
14 | 
15 |    do_request_json_for( $user,
16 |       method  => "POST",
17 |       uri     => "/v3/user/:user_id/filter",
18 |       content => $filter,
19 |    )->then( sub {
20 |       my ( $body ) = @_;
21 | 
22 |       assert_json_keys( $body, "filter_id" );
23 | 
24 |       Future->done( $body->{filter_id} )
25 |    })
26 | }
27 | 
28 | 
29 | test "Can create filter",
30 |    requires => [ local_user_fixture( with_events => 0 ) ],
31 | 
32 |    proves => [qw( can_create_filter )],
33 | 
34 |    do => sub {
35 |       my ( $user ) = @_;
36 | 
37 |       matrix_create_filter( $user, {
38 |          room => { timeline => { limit => 10 } },
39 |       });
40 |    };
41 | 
42 | 
43 | test "Can download filter",
44 |    requires => [
45 |       local_user_fixture( with_events => 0 ),
46 |       qw( can_create_filter )
47 |    ],
48 | 
49 |    check => sub {
50 |       my ( $user ) = @_;
51 | 
52 |       matrix_create_filter( $user, {
53 |          room => { timeline => { limit => 10 } }
54 |       })->then( sub {
55 |          my ( $filter_id ) = @_;
56 | 
57 |          do_request_json_for( $user,
58 |             method  => "GET",
59 |             uri     => "/v3/user/:user_id/filter/$filter_id",
60 |          )
61 |       })->then( sub {
62 |          my ( $body ) = @_;
63 | 
64 |          assert_json_keys( $body, "room" );
65 |          assert_json_keys( my $room = $body->{room}, "timeline" );
66 |          assert_json_keys( my $timeline = $room->{timeline}, "limit" );
67 |          $timeline->{limit} == 10 or die "Expected timeline limit to be 10";
68 | 
69 |          Future->done(1)
70 |       })
71 |    };
72 | 


--------------------------------------------------------------------------------
/tests/31sync/02sync.pl:
--------------------------------------------------------------------------------
 1 | use Future::Utils qw( repeat );
 2 | 
 3 | 
 4 | test "Can sync",
 5 |     requires => [ local_user_fixture( with_events => 0 ) ],
 6 | 
 7 |     proves => [qw( can_sync )],
 8 | 
 9 |     do => sub {
10 |        my ( $user ) = @_;
11 | 
12 |        matrix_sync( $user, timeout => 0 ) ->then( sub {
13 |           my ( $body ) = @_;
14 | 
15 |           matrix_sync(
16 |              $user,
17 |              since => $body->{next_batch},
18 |              timeout => 0,
19 |           )
20 |        })->then_done(1);
21 |     };
22 | 


--------------------------------------------------------------------------------
/tests/31sync/08polling.pl:
--------------------------------------------------------------------------------
 1 | test "Sync can be polled for updates",
 2 |    requires => [ local_user_fixture( with_events => 0 ),
 3 |                  qw( can_sync ) ],
 4 | 
 5 |    check => sub {
 6 |       my ( $user ) = @_;
 7 | 
 8 |       my ( $filter_id, $room_id );
 9 | 
10 |       matrix_create_filter( $user, {
11 |          presence => { not_types => ["m.presence"] }
12 |       } )->then( sub {
13 |          ( $filter_id ) = @_;
14 | 
15 |          matrix_create_room_synced( $user );
16 |       })->then( sub {
17 |          ( $room_id ) = @_;
18 | 
19 |          matrix_sync( $user, filter => $filter_id );
20 |       })->then( sub {
21 |          my ( $body ) = @_;
22 | 
23 |          Future->needs_all(
24 |             matrix_sync_again( $user, filter => $filter_id, timeout => 10000 ),
25 | 
26 |             delay( 0.1 )->then( sub {
27 |                matrix_send_room_text_message(
28 |                   $user, $room_id, body => "1"
29 |                )
30 |             }),
31 |          )
32 |       })->then( sub {
33 |          my ( $body, $response, $event_id ) = @_;
34 | 
35 |          my $room = $body->{rooms}{join}{$room_id};
36 | 
37 |          my $events = $room->{timeline}{events} or
38 |             die "Expected an event timeline";
39 |          @$events == 1 or
40 |             die "Expected one timeline event";
41 | 
42 |          $room->{timeline}{events}[0]{event_id} eq $event_id
43 |             or die "Unexpected timeline event";
44 | 
45 |          Future->done(1)
46 |       })
47 |    };
48 | 
49 | test "Sync is woken up for leaves",
50 |    requires => [ local_user_fixture( with_events => 0 ),
51 |                  qw( can_sync ) ],
52 | 
53 |    check => sub {
54 |       my ( $user ) = @_;
55 | 
56 |       my ( $filter_id, $room_id );
57 | 
58 |       matrix_create_filter( $user, {
59 |          presence => { not_types => ["m.presence"] }
60 |       } )->then( sub {
61 |          ( $filter_id ) = @_;
62 | 
63 |          matrix_create_room_synced( $user );
64 |       })->then( sub {
65 |          ( $room_id ) = @_;
66 | 
67 |          matrix_sync( $user, filter => $filter_id );
68 |       })->then( sub {
69 |          Future->needs_all(
70 |             matrix_sync_again( $user, filter => $filter_id, timeout => 10000 ),
71 | 
72 |             delay( 0.1 )->then( sub {
73 |                matrix_leave_room(
74 |                   $user, $room_id
75 |                )
76 |             }),
77 |          )
78 |       })->then( sub {
79 |          my ( $body, $response, $event_id ) = @_;
80 | 
81 |          my $room = $body->{rooms}{leave}{$room_id};
82 | 
83 |          my $events = $room->{timeline}{events} or
84 |             die "Expected an event timeline";
85 |          @$events == 1 or
86 |             die "Expected one timeline event";
87 | 
88 |          Future->done(1)
89 |       })
90 |    };
91 | 


--------------------------------------------------------------------------------
/tests/31sync/12receipts.pl:
--------------------------------------------------------------------------------
  1 | test "Read receipts appear in initial v2 /sync",
  2 |    requires => [ local_user_fixture( with_events => 0 ),
  3 |                  qw( can_sync ) ],
  4 | 
  5 |    check => sub {
  6 |       my ( $user ) = @_;
  7 | 
  8 |       my ( $filter_id, $room_id, $event_id );
  9 | 
 10 |       my $filter = {
 11 |          presence => { types => [] },
 12 |          room     => {
 13 |             state     => { types => [] },
 14 |             timeline  => { types => [] },
 15 |             ephemeral => { types => [ "m.receipt" ] },
 16 |          },
 17 |       };
 18 | 
 19 |       matrix_create_filter( $user, $filter )->then( sub {
 20 |          ( $filter_id ) = @_;
 21 | 
 22 |          matrix_create_room_synced( $user );
 23 |       })->then( sub {
 24 |          ( $room_id ) = @_;
 25 | 
 26 |          matrix_send_room_text_message_synced( $user, $room_id, body => "hello" );
 27 |       })->then( sub {
 28 |          ( $event_id ) = @_;
 29 | 
 30 |          matrix_advance_room_receipt_synced( $user, $room_id, "m.read", $event_id );
 31 |       })->then( sub {
 32 |          matrix_sync( $user, filter => $filter_id );
 33 |       })->then( sub {
 34 |          my ( $body ) = @_;
 35 | 
 36 |          my $room = $body->{rooms}{join}{$room_id};
 37 | 
 38 |          my $ephemeral = $room->{ephemeral}{events};
 39 | 
 40 |          @{ $ephemeral } == 1 or die "Expected a m.receipt event";
 41 | 
 42 |          log_if_fail "Ephemeral:", $ephemeral;
 43 | 
 44 |          my $receipt = $ephemeral->[0];
 45 | 
 46 |          $receipt->{type} eq "m.receipt" or die "Unexpected event type";
 47 |          defined $receipt->{content}{$event_id}{"m.read"}{ $user->user_id }
 48 |             or die "Expected to see a receipt for ${\ $user->user_id }";
 49 | 
 50 |          Future->done(1);
 51 |       });
 52 |    };
 53 | 
 54 | 
 55 | test "New read receipts appear in incremental v2 /sync",
 56 |    requires => [ local_user_fixture( with_events => 0 ),
 57 |                  qw( can_sync ) ],
 58 | 
 59 |    check => sub {
 60 |       my ( $user ) = @_;
 61 | 
 62 |       my ( $filter_id, $room_id, $event_id );
 63 | 
 64 |       my $filter = {
 65 |          presence => { types => [] },
 66 |          room     => {
 67 |             state     => { types => [] },
 68 |             timeline  => { types => [] },
 69 |             ephemeral => { types => [ "m.receipt" ] },
 70 |          },
 71 |       };
 72 | 
 73 |       matrix_create_filter( $user, $filter )->then( sub {
 74 |          ( $filter_id ) = @_;
 75 | 
 76 |          matrix_create_room_synced( $user );
 77 |       })->then( sub {
 78 |          ( $room_id ) = @_;
 79 | 
 80 |          matrix_send_room_text_message_synced( $user, $room_id, body => "hello" );
 81 |       })->then( sub {
 82 |          ( $event_id ) = @_;
 83 | 
 84 |          matrix_sync( $user, filter => $filter_id );
 85 |       })->then( sub {
 86 |          matrix_advance_room_receipt_synced(
 87 |             $user, $room_id, "m.read", $event_id
 88 |          );
 89 |       })->then( sub {
 90 |          matrix_sync_again( $user, filter => $filter_id );
 91 |       })->then( sub {
 92 |          my ( $body ) = @_;
 93 | 
 94 |          my $room = $body->{rooms}{join}{$room_id};
 95 | 
 96 |          my $ephemeral = $room->{ephemeral}{events};
 97 | 
 98 |          @{ $ephemeral } == 1 or die "Expected a m.receipt event";
 99 | 
100 |          log_if_fail "Ephemeral:", $ephemeral;
101 | 
102 |          my $receipt = $ephemeral->[0];
103 | 
104 |          $receipt->{type} eq "m.receipt" or die "Unexpected event type";
105 |          defined $receipt->{content}{$event_id}{"m.read"}{ $user->user_id }
106 |             or die "Expected to see a receipt for ${\ $user->user_id }";
107 | 
108 |          Future->done(1);
109 |       });
110 |    };
111 | 


--------------------------------------------------------------------------------
/tests/31sync/13filtered_sync.pl:
--------------------------------------------------------------------------------
 1 | use JSON qw( encode_json );
 2 | 
 3 | test "Can pass a JSON filter as a query parameter",
 4 |    requires => [ local_user_fixture() ],
 5 | 
 6 |    check => sub {
 7 |       my ( $user ) = @_;
 8 | 
 9 |       my ( $room_id );
10 | 
11 |       matrix_create_room_synced( $user )->then( sub {
12 |          ( $room_id ) = @_;
13 | 
14 |          matrix_sync( $user, filter => encode_json( {
15 |             room => {
16 |                state => { types => [ "m.room.member" ] },
17 |                timeline => { limit => 0 },
18 |             }
19 |          }));
20 |       })->then( sub {
21 |          my ( $body ) = @_;
22 | 
23 |          my $room = $body->{rooms}{join}{$room_id};
24 | 
25 |          if (exists($room->{timeline})) {
26 |              assert_json_empty_list( $room->{timeline}{events} );
27 |          }       
28 | 
29 |          @{ $room->{state}{events} } == 1
30 |             or die "Expected a single state event because of the filter";
31 | 
32 |          $room->{state}{events}[0]{type} eq "m.room.member"
33 |             or die "Expected a single member event because of the filter";
34 | 
35 |          Future->done(1);
36 |       });
37 |    };
38 | 
39 | 
40 | test "Can request federation format via the filter",
41 |    requires => [ local_user_fixture( with_events => 0 ),
42 |                  qw( can_sync ) ],
43 | 
44 |    check => sub {
45 |       my ( $user ) = @_;
46 | 
47 |       my ( $filter_id, $room_id, $event_id_1 );
48 | 
49 |       my $filter = {
50 |          event_format => 'federation',
51 |          room => { timeline => { limit => 1 } },
52 |       };
53 | 
54 |       matrix_create_filter( $user, $filter )->then( sub {
55 |          ( $filter_id ) = @_;
56 | 
57 |          matrix_create_room_synced( $user )
58 |       })->then( sub {
59 |          ( $room_id ) = @_;
60 | 
61 |          matrix_send_room_text_message_synced( $user, $room_id,
62 |             body => "Test message",
63 |          );
64 |       })->then( sub {
65 |          ( $event_id_1 ) = @_;
66 | 
67 |          matrix_sync( $user, filter => $filter_id );
68 |       })->then( sub {
69 |          my ( $body ) = @_;
70 | 
71 |          my $room = $body->{rooms}{join}{$room_id};
72 | 
73 |          log_if_fail "sync room result", $room;
74 | 
75 |          assert_json_keys( $room, qw( timeline state ephemeral ));
76 |          assert_json_keys( $room->{timeline}, qw( events limited prev_batch ));
77 | 
78 |          assert_eq( scalar @{ $room->{timeline}{events} }, 1, "timeline event count" );
79 | 
80 |          assert_json_keys(
81 |             $room->{timeline}{events}[0], qw(
82 |                event_id content room_id sender origin_server_ts type
83 |                prev_events auth_events depth hashes signatures
84 |             )
85 |          );
86 | 
87 |          assert_eq( $room->{timeline}{events}[0]{content}{body}, "Test message", "timeline message" );
88 |          assert_eq( $room->{timeline}{events}[0]{event_id}, $event_id_1, "timeline event id" );
89 | 
90 |          Future->done(1);
91 |       });
92 |   };
93 | 


--------------------------------------------------------------------------------
/tests/31sync/14read-markers.pl:
--------------------------------------------------------------------------------
 1 | test "Read markers appear in incremental v2 /sync",
 2 |    requires => [ local_user_fixture(), qw( can_sync ) ],
 3 | 
 4 |    check => sub {
 5 |       my ( $user ) = @_;
 6 | 
 7 |       my ( $room_id, $event_id );
 8 | 
 9 |       matrix_create_room_synced( $user )->then( sub {
10 |          ( $room_id ) = @_;
11 | 
12 |          matrix_send_room_text_message_synced( $user, $room_id, body => "hello" );
13 |       })->then( sub {
14 |          ( $event_id ) = @_;
15 | 
16 |          matrix_advance_room_read_marker_synced( $user, $room_id, $event_id );
17 |       })->then_done(1);
18 |    };
19 | 
20 | 
21 | test "Read markers appear in initial v2 /sync",
22 |    requires => [ local_user_fixture(), qw( can_sync ) ],
23 | 
24 |    check => sub {
25 |       my ( $user ) = @_;
26 | 
27 |       my ( $room_id, $event_id );
28 | 
29 |       matrix_create_room_synced( $user )->then( sub {
30 |          ( $room_id ) = @_;
31 | 
32 |          matrix_send_room_text_message_synced( $user, $room_id, body => "hello" );
33 |       })->then( sub {
34 |          ( $event_id ) = @_;
35 | 
36 |          matrix_advance_room_read_marker_synced( $user, $room_id, $event_id );
37 |       })->then( sub {
38 |          await_sync( $user, check => sub {
39 |             my ( $body ) = @_;
40 | 
41 |             return unless $body->{rooms}{join}{$room_id};
42 |             my $room = $body->{rooms}{join}{$room_id};
43 | 
44 |             return unless $room->{account_data}{events};
45 |             my $account_data = $room->{account_data}{events};
46 | 
47 |             return unless @{ $account_data } == 1;
48 |             my $read_marker = $account_data->[0];
49 | 
50 |             $read_marker->{type} eq "m.fully_read"
51 |                or die "Unexpected event type";
52 |             $read_marker->{content}{event_id} eq $event_id
53 |                or die "Expected to see a marker for $event_id";
54 | 
55 |             Future->done(1);
56 |          })
57 |       })
58 |    };
59 | 
60 | 
61 | test "Read markers can be updated",
62 |    requires => [ local_user_fixture(), qw( can_sync ) ],
63 | 
64 |    check => sub {
65 |       my ( $user ) = @_;
66 | 
67 |       my ( $room_id, $event_id );
68 | 
69 |       matrix_create_room_synced( $user )->then( sub {
70 |          ( $room_id ) = @_;
71 | 
72 |          matrix_send_room_text_message_synced( $user, $room_id, body => "hello" );
73 |       })->then( sub {
74 |          ( $event_id ) = @_;
75 | 
76 |          matrix_advance_room_read_marker_synced( $user, $room_id, $event_id );
77 |       })->then( sub {
78 |          matrix_send_room_text_message_synced( $user, $room_id, body => "hello2" );
79 |       })->then( sub {
80 |          ( $event_id ) = @_;
81 | 
82 |          matrix_advance_room_read_marker_synced( $user, $room_id, $event_id );
83 |       })->then_done(1);
84 |    };
85 | 


--------------------------------------------------------------------------------
/tests/41end-to-end-keys/03-one-time-keys.pl:
--------------------------------------------------------------------------------
 1 | multi_test "Can claim one time key using POST",
 2 |    requires => [ local_user_fixture(),
 3 |                  qw( can_upload_e2e_keys )],
 4 | 
 5 |    check => sub {
 6 |       my ( $user ) = @_;
 7 | 
 8 |       my $device_id = $user->device_id;
 9 | 
10 |       do_request_json_for( $user,
11 |          method  => "POST",
12 |          uri     => "/v3/keys/upload",
13 |          content => {
14 |             one_time_keys => {
15 |                "test_algorithm:test_id", "iDBFAwt4T0ntzaZXwEevrKfmelFUMj8KS6n6kYhBPzU"
16 |             }
17 |          }
18 |       )->SyTest::pass_on_done( "Uploaded one-time keys" )
19 |       ->then( sub {
20 |          do_request_json_for( $user,
21 |             method  => "POST",
22 |             uri     => "/v3/keys/claim",
23 |             content => {
24 |                one_time_keys => {
25 |                   $user->user_id => {
26 |                      $device_id => "test_algorithm"
27 |                   }
28 |                }
29 |             }
30 |          )
31 |       })->then( sub {
32 |          my ( $content ) = @_;
33 |          log_if_fail "POST response", $content;
34 | 
35 |          assert_json_keys( $content, "one_time_keys" );
36 | 
37 |          my $one_time_keys = $content->{one_time_keys};
38 |          assert_json_keys( $one_time_keys, $user->user_id );
39 | 
40 |          my $alice_keys = $one_time_keys->{ $user->user_id };
41 |          assert_json_keys( $alice_keys, $device_id );
42 | 
43 |          my $alice_device_keys = $alice_keys->{$device_id};
44 |          assert_json_keys( $alice_device_keys, "test_algorithm:test_id" );
45 | 
46 |          "iDBFAwt4T0ntzaZXwEevrKfmelFUMj8KS6n6kYhBPzU" eq $alice_device_keys->{"test_algorithm:test_id"} or
47 |             die "Unexpected key base64";
48 | 
49 |          pass "Took one time key";
50 | 
51 |          # a second claim should give no keys
52 |          do_request_json_for( $user,
53 |             method  => "POST",
54 |             uri     => "/v3/keys/claim",
55 |             content => {
56 |                one_time_keys => {
57 |                   $user->user_id => {
58 |                      $device_id => "test_algorithm"
59 |                   }
60 |                }
61 |             }
62 |          )
63 |       })->then( sub {
64 |          my ( $content ) = @_;
65 |          log_if_fail "Second claim response", $content;
66 | 
67 |          assert_json_keys( $content, "one_time_keys" );
68 |          assert_deeply_eq( $content->{one_time_keys}, {}, "Second claim result" );
69 | 
70 |          Future->done(1)
71 |       });
72 |    };
73 | 


--------------------------------------------------------------------------------
/tests/41end-to-end-keys/04-query-key-federation.pl:
--------------------------------------------------------------------------------
 1 | multi_test "Can query remote device keys using POST",
 2 |    requires => [ local_user_fixture(), remote_user_fixture(),
 3 |                  qw( can_upload_e2e_keys )],
 4 | 
 5 |    check => sub {
 6 |       my ( $user, $remote_user ) = @_;
 7 | 
 8 |       matrix_put_e2e_keys( $user )
 9 |          ->SyTest::pass_on_done( "Uploaded key" )
10 |       ->then( sub {
11 |          matrix_set_device_display_name( $user, $user->device_id, "test display name" ),
12 |       })->then( sub {
13 |          matrix_get_e2e_keys(
14 |             $remote_user, $user->user_id
15 |          )
16 |       })->then( sub {
17 |          my ( $content ) = @_;
18 | 
19 |          assert_json_keys( $content, "device_keys" );
20 | 
21 |          my $device_keys = $content->{device_keys};
22 |          assert_json_keys( $device_keys, $user->user_id );
23 | 
24 |          my $alice_keys = $device_keys->{ $user->user_id };
25 |          assert_json_keys( $alice_keys, $user->device_id );
26 | 
27 |          my $alice_device_keys = $alice_keys->{ $user->device_id };
28 | 
29 |          # TODO: Check that the content matches what we uploaded.
30 | 
31 |          # Device display names are optional for POST /user/keys/query responses.
32 |          # If one exists, ensure it's the one we expected.
33 |          if (exists( $alice_device_keys->{"unsigned"}->{"device_display_name"} )) {
34 |             assert_eq(
35 |                $alice_device_keys->{"unsigned"}->{"device_display_name"},
36 |                "test display name",
37 |             );
38 |          }
39 | 
40 |          Future->done(1)
41 |       });
42 |    };
43 | 


--------------------------------------------------------------------------------
/tests/41end-to-end-keys/05-one-time-key-federation.pl:
--------------------------------------------------------------------------------
 1 | multi_test "Can claim remote one time key using POST",
 2 |    requires => [ local_user_fixture(), remote_user_fixture(),
 3 |                  qw( can_upload_e2e_keys )],
 4 | 
 5 |    check => sub {
 6 |       my ( $user, $remote_user ) = @_;
 7 | 
 8 |       my $device_id = $user->device_id;
 9 | 
10 |       do_request_json_for( $user,
11 |          method  => "POST",
12 |          uri     => "/v3/keys/upload",
13 |          content => {
14 |             one_time_keys => {
15 |                "test_algorithm:test_id", "kUuAFk05Ig0RjwDimSYHOXKro8BRB14G0efxVOq73VU"
16 |             }
17 |          }
18 |       )->SyTest::pass_on_done( "Uploaded one-time keys" )
19 |       ->then( sub {
20 |          do_request_json_for( $remote_user,
21 |             method  => "POST",
22 |             uri     => "/v3/keys/claim",
23 |             content => {
24 |                one_time_keys => {
25 |                   $user->user_id => {
26 |                      $device_id => "test_algorithm"
27 |                   }
28 |                }
29 |             }
30 |          )
31 |       })->then( sub {
32 |          my ( $content ) = @_;
33 |          log_if_fail "POST response", $content;
34 | 
35 |          assert_json_keys( $content, "one_time_keys" );
36 | 
37 |          my $one_time_keys = $content->{one_time_keys};
38 |          assert_json_keys( $one_time_keys, $user->user_id );
39 | 
40 |          my $alice_keys = $one_time_keys->{ $user->user_id };
41 |          assert_json_keys( $alice_keys, $device_id );
42 | 
43 |          my $alice_device_keys = $alice_keys->{$device_id};
44 |          assert_json_keys( $alice_device_keys, "test_algorithm:test_id" );
45 | 
46 |          "kUuAFk05Ig0RjwDimSYHOXKro8BRB14G0efxVOq73VU" eq $alice_device_keys->{"test_algorithm:test_id"} or
47 |             die "Unexpected key base64";
48 | 
49 |          pass "Took one time key";
50 | 
51 |          # a second claim should give no keys
52 |          do_request_json_for( $remote_user,
53 |             method  => "POST",
54 |             uri     => "/v3/keys/claim",
55 |             content => {
56 |                one_time_keys => {
57 |                   $user->user_id => {
58 |                      $device_id => "test_algorithm"
59 |                   }
60 |                }
61 |             }
62 |          )
63 |       })->then( sub {
64 |          my ( $content ) = @_;
65 |          log_if_fail "Second claim response", $content;
66 | 
67 |          assert_json_keys( $content, "one_time_keys" );
68 |          assert_deeply_eq( $content->{one_time_keys}, {}, "Second claim result" );
69 | 
70 |          Future->done(1)
71 |       });
72 |    };
73 | 


--------------------------------------------------------------------------------
/tests/45openid.pl:
--------------------------------------------------------------------------------
 1 | test "Can generate a openid access_token that can be exchanged for information about a user",
 2 |    requires => [ local_user_fixture(), $main::HTTP_CLIENT, $main::HOMESERVER_INFO[0] ],
 3 | 
 4 |    check => sub {
 5 |       my ( $user, $http, $info ) = @_;
 6 | 
 7 |       do_request_json_for( $user,
 8 |          method  => "POST",
 9 |          uri     => "/v3/user/:user_id/openid/request_token",
10 |          content => {},
11 |       )->then( sub {
12 |          my ( $body ) = @_;
13 | 
14 |          assert_json_keys( $body, qw( access_token matrix_server_name expires_in ) );
15 |          assert_eq( $body->{matrix_server_name}, $info->server_name );
16 | 
17 |          my $token = $body->{access_token};
18 | 
19 |          $http->do_request_json(
20 |             method   => "GET",
21 |             uri => $info->client_location . "/_matrix/federation/v1/openid/userinfo",
22 |             params   => { access_token => $token },
23 |          );
24 |       })->then( sub {
25 |          my ( $body ) = @_;
26 | 
27 |          assert_json_keys( $body, qw( sub ) );
28 |          assert_eq( $body->{sub}, $user->user_id );
29 | 
30 |          Future->done(1);
31 |       });
32 |    };
33 | 
34 | test "Invalid openid access tokens are rejected",
35 |    requires => [ $main::HTTP_CLIENT, $main::HOMESERVER_INFO[0] ],
36 | 
37 |    check => sub {
38 |       my ( $http, $info ) = @_;
39 | 
40 |       $http->do_request_json(
41 |          method   => "GET",
42 |          uri => $info->client_location . "/_matrix/federation/v1/openid/userinfo",
43 |          params   => { access_token => "an/invalid/token" },
44 |       )->main::expect_http_401;
45 |    };
46 | 
47 | test "Requests to userinfo without access tokens are rejected",
48 |    requires => [ $main::HTTP_CLIENT, $main::HOMESERVER_INFO[0] ],
49 | 
50 |    check => sub {
51 |       my ( $http, $info ) = @_;
52 | 
53 |       $http->do_request_json(
54 |          method   => "GET",
55 |          uri => $info->client_location . "/_matrix/federation/v1/openid/userinfo",
56 |       )->main::expect_http_401;
57 |    };
58 | 


--------------------------------------------------------------------------------
/tests/46direct/03polling.pl:
--------------------------------------------------------------------------------
 1 | my $FILTER_ONLY_DIRECT = '{"room":{"rooms":[]},"account_data":{"types":[]},"presence":{"types":[]}}';
 2 | 
 3 | test "Device messages wake up /sync",
 4 |    requires => [ local_user_fixture( with_events => 0 ) ],
 5 | 
 6 |    check => sub {
 7 |       my ( $user ) = @_;
 8 | 
 9 |       matrix_sync( $user,
10 |          filter       => $FILTER_ONLY_DIRECT,
11 |          set_presence => "offline",
12 |       )->then( sub {
13 |          Future->needs_all(
14 |             matrix_sync_again( $user, filter => $FILTER_ONLY_DIRECT, timeout => 10000 ),
15 |             delay(0.1)->then( sub {
16 |                matrix_send_device_message( $user,
17 |                   type     => "my.test.type",
18 |                   messages => {
19 |                      $user->user_id => {
20 |                         $user->device_id => {
21 |                            my_key => "my_value",
22 |                         },
23 |                      },
24 |                   },
25 |                );
26 |             }),
27 |          );
28 |       });
29 |    };
30 | 


--------------------------------------------------------------------------------
/tests/46direct/04federation.pl:
--------------------------------------------------------------------------------
 1 | test "Can recv device messages over federation",
 2 |    requires => [ local_user_fixture(), remote_user_fixture(),
 3 |       qw( can_recv_device_message ) ],
 4 | 
 5 |    check => sub {
 6 |       my ( $local_user, $remote_user ) = @_;
 7 | 
 8 |       matrix_send_device_message( $local_user,
 9 |          type     => "my.test.type",
10 |          messages => {
11 |             $remote_user->user_id => {
12 |                $remote_user->device_id => {
13 |                   message => "first",
14 |                },
15 |             },
16 |          },
17 |       )->then( sub {
18 |          # Download the first message again and acknowledge it.
19 |          matrix_recv_and_ack_device_message( $remote_user );
20 |       })->then( sub {
21 |          my ( $messages ) = @_;
22 | 
23 |          assert_deeply_eq( $messages, [{
24 |             sender  => $local_user->user_id,
25 |             type    => "my.test.type",
26 |             content => {
27 |                message => "first",
28 |             },
29 |          }]);
30 | 
31 |          # Send another message so that we can check that the remote user
32 |          # doesn't receive the first message twice
33 |          matrix_send_device_message( $local_user,
34 |             type     => "my.test.type",
35 |             messages => {
36 |                $remote_user->user_id => {
37 |                   $remote_user->device_id => {
38 |                      message => "second",
39 |                   },
40 |                },
41 |             },
42 |          );
43 |       })->then( sub {
44 |          # Download the second message and acknowledge it.
45 |          matrix_recv_and_ack_device_message( $remote_user );
46 |       })->then( sub {
47 |          my ( $messages ) = @_;
48 | 
49 |          assert_deeply_eq( $messages, [{
50 |             sender  => $local_user->user_id,
51 |             type    => "my.test.type",
52 |             content => {
53 |                message => "second",
54 |             },
55 |          }]);
56 | 
57 |          Future->done(1);
58 |       });
59 |    };
60 | 
61 | 
62 | my $FILTER_ONLY_DIRECT = '{"room":{"rooms":[]},"account_data":{"types":[]},"presence":{"types":[]}}';
63 | 
64 | test "Device messages over federation wake up /sync",
65 |    requires => [ local_user_fixture( with_events => 0 ), remote_user_fixture() ],
66 | 
67 |    check => sub {
68 |       my ( $local_user, $remote_user ) = @_;
69 | 
70 |       matrix_sync( $local_user,
71 |          filter       => $FILTER_ONLY_DIRECT,
72 |          set_presence => "offline",
73 |       )->then( sub {
74 |          Future->needs_all(
75 |             matrix_sync_again( $local_user, filter => $FILTER_ONLY_DIRECT, timeout => 10000 ),
76 |             delay(0.1)->then( sub {
77 |                matrix_send_device_message( $remote_user,
78 |                   type     => "my.test.type",
79 |                   messages => {
80 |                      $local_user->user_id => {
81 |                         $local_user->device_id => {
82 |                            my_key => "my_value",
83 |                         },
84 |                      },
85 |                   },
86 |                );
87 |             }),
88 |          );
89 |       });
90 |    };
91 | 


--------------------------------------------------------------------------------
/tests/50federation/02server-names.pl:
--------------------------------------------------------------------------------
 1 | use SyTest::Crypto qw( ed25519_nacl_keypair );
 2 | 
 3 | # check that the rules around server_names are enforced
 4 | 
 5 | test "Non-numeric ports in server names are rejected",
 6 |    requires => [ $main::HOMESERVER_INFO[0], local_user_fixture(), ],
 7 | 
 8 |    do => sub {
 9 |       my ( $info, $user ) = @_;
10 | 
11 |       my ( $pkey, $skey ) = ed25519_nacl_keypair;
12 | 
13 |       my $datastore = SyTest::Federation::Datastore->new(
14 |          server_name => "localhost:http",
15 |          key_id      => "ed25519:1",
16 |          public_key  => $pkey,
17 |          secret_key  => $skey,
18 |       );
19 | 
20 |       my $outbound_client = SyTest::Federation::Client->new(
21 |          datastore => $datastore,
22 |          uri_base  => "/_matrix/federation/v1",
23 |         );
24 |       $loop->add( $outbound_client );
25 | 
26 |       $outbound_client->do_request_json(
27 |          method   => "GET",
28 |          hostname => $info->server_name,
29 |          uri      => "/query/profile",
30 | 
31 |          params => {
32 |             user_id => $user->user_id,
33 |             field   => "displayname",
34 |          }
35 |       )->main::expect_http_400();
36 |    };
37 | 


--------------------------------------------------------------------------------
/tests/50federation/10query-profile.pl:
--------------------------------------------------------------------------------
 1 | ## This test often fails when run via haproxy on slower machines. See
 2 | #    https://github.com/matrix-org/sytest/issues/362
 3 | #  for more information.
 4 | 
 5 | test "Outbound federation can query profile data",
 6 |    requires => [ $main::INBOUND_SERVER, $main::SPYGLASS_USER,
 7 |                 qw( can_get_displayname )],
 8 | 
 9 |    check => sub {
10 |       my ( $inbound_server, $user ) = @_;
11 | 
12 |       my $local_server_name = $inbound_server->server_name;
13 | 
14 |       require_stub $inbound_server->await_request_query_profile( "\@user:$local_server_name" )
15 |          ->on_done( sub {
16 |             my ( $req ) = @_;
17 | 
18 |             $req->respond_json( {
19 |                displayname => "The displayname of \@user:$local_server_name",
20 |                avatar_url  => "",
21 |             } );
22 |          });
23 | 
24 |       do_request_json_for( $user,
25 |          method => "GET",
26 |          uri    => "/v3/profile/\@user:$local_server_name/displayname",
27 |       )->then( sub {
28 |          my ( $body ) = @_;
29 |          log_if_fail "Query response", $body;
30 | 
31 |          assert_json_keys( $body, qw( displayname ));
32 | 
33 |          $body->{displayname} eq "The displayname of \@user:$local_server_name" or
34 |             die "Displayname not as expected";
35 | 
36 |          Future->done(1);
37 |       });
38 |    };
39 | 
40 | my $dname = "Displayname Set For Federation Test";
41 | 
42 | test "Inbound federation can query profile data",
43 |    requires => [ $main::OUTBOUND_CLIENT, local_user_fixture(),
44 |                  qw( can_set_displayname )],
45 | 
46 |    do => sub {
47 |       my ( $outbound_client, $user ) = @_;
48 | 
49 |       do_request_json_for( $user,
50 |          method => "PUT",
51 |          uri    => "/v3/profile/:user_id/displayname",
52 | 
53 |          content => {
54 |             displayname => $dname,
55 |          },
56 |       )->then( sub {
57 |          $outbound_client->do_request_json(
58 |             method   => "GET",
59 |             hostname => $user->server_name,
60 |             uri      => "/v1/query/profile",
61 | 
62 |             params => {
63 |                user_id => $user->user_id,
64 |                field   => "displayname",
65 |             }
66 |          )
67 |       })->then( sub {
68 |          my ( $body ) = @_;
69 |          log_if_fail "Query response", $body;
70 | 
71 |          assert_json_keys( $body, qw( displayname ));
72 | 
73 |          $body->{displayname} eq $dname or
74 |             die "Expected displayname to be '$dname'";
75 | 
76 |          Future->done(1);
77 |       });
78 |    };
79 | 


--------------------------------------------------------------------------------
/tests/50federation/11query-directory.pl:
--------------------------------------------------------------------------------
 1 | test "Outbound federation can query room alias directory",
 2 |    requires => [ $main::INBOUND_SERVER, $main::SPYGLASS_USER,
 3 |                 qw( can_lookup_room_alias )],
 4 | 
 5 |    check => sub {
 6 |       my ( $inbound_server, $user ) = @_;
 7 | 
 8 |       my $local_server_name = $inbound_server->server_name;
 9 |       my $room_alias = "#test:$local_server_name";
10 | 
11 |       require_stub $inbound_server->await_request_query_directory( $room_alias )
12 |          ->on_done( sub {
13 |             my ( $req ) = @_;
14 | 
15 |             $req->respond_json( {
16 |                room_id => "!the-room-id:$local_server_name",
17 |                servers => [
18 |                   $local_server_name,
19 |                ]
20 |             } );
21 |          });
22 | 
23 |       do_request_json_for( $user,
24 |          method => "GET",
25 |          uri    => "/v3/directory/room/$room_alias",
26 |       )->then( sub {
27 |          my ( $body ) = @_;
28 |          log_if_fail "Query response", $body;
29 | 
30 |          assert_json_keys( $body, qw( room_id servers ));
31 | 
32 |          $body->{room_id} eq "!the-room-id:$local_server_name" or
33 |             die "Expected room_id to be '!the-room-id:$local_server_name'";
34 | 
35 |          assert_json_nonempty_list( $body->{servers} );
36 | 
37 |          assert_json_string( $_ ) for @{ $body->{servers} };
38 | 
39 |          Future->done(1);
40 |       });
41 |    };
42 | 
43 | test "Inbound federation can query room alias directory",
44 |    # TODO(paul): technically this doesn't need local_user_fixture(), if we had
45 |    #   some user we could assert can perform media/directory/etc... operations
46 |    #   but doesn't mutate any of its own state, or join rooms, etc...
47 |    requires => [ $main::OUTBOUND_CLIENT,
48 |                  local_user_fixture(), room_alias_fixture(),
49 |                  qw( can_create_room_alias )],
50 | 
51 |    do => sub {
52 |       my ( $outbound_client, $user, $room_alias ) = @_;
53 |       my $first_home_server = $user->server_name;
54 | 
55 |       my $room_id;
56 | 
57 |       matrix_create_room_synced( $user )
58 |       ->then( sub {
59 |          ( $room_id ) = @_;
60 | 
61 |          do_request_json_for( $user,
62 |             method => "PUT",
63 |             uri    => "/v3/directory/room/$room_alias",
64 | 
65 |             content => {
66 |                room_id => $room_id,
67 |                servers => [ "example.org" ],  # TODO: Am I really allowed to do this?
68 |             },
69 |          )
70 |       })->then( sub {
71 |          $outbound_client->do_request_json(
72 |             method   => "GET",
73 |             hostname => $first_home_server,
74 |             uri      => "/v1/query/directory",
75 | 
76 |             params => {
77 |                room_alias => $room_alias,
78 |             },
79 |          )
80 |       })->then( sub {
81 |          my ( $body ) = @_;
82 |          log_if_fail "Query response", $body;
83 | 
84 |          assert_json_keys( $body, qw( room_id servers ));
85 | 
86 |          $body->{room_id} eq $room_id or
87 |             die "Expected room_id to be '$room_id'";
88 | 
89 |          assert_json_nonempty_list( $body->{servers} );
90 | 
91 |          Future->done(1);
92 |       });
93 |    };
94 | 


--------------------------------------------------------------------------------
/tests/50federation/37public-rooms.pl:
--------------------------------------------------------------------------------
 1 | test "Inbound federation can get public room list",
 2 |    requires => [ $main::OUTBOUND_CLIENT,
 3 |                  local_user_and_room_fixtures(),
 4 |                  federation_user_id_fixture() ],
 5 | 
 6 |    do => sub {
 7 |      my ( $outbound_client, $creator, $room_id, $user_id ) = @_;
 8 |      my $first_home_server = $creator->server_name;
 9 | 
10 |      my $local_server_name = $outbound_client->server_name;
11 | 
12 |      my $room;
13 | 
14 |      $outbound_client->join_room(
15 |         server_name => $first_home_server,
16 |         room_id     => $room_id,
17 |         user_id     => $user_id,
18 |      )->then( sub {
19 |         ( $room ) = @_;
20 | 
21 |         do_request_json_for( $creator,
22 |            method   => "PUT",
23 |            uri      => "/v3/directory/list/room/$room_id",
24 |            content  => {
25 |              visibility => "public",
26 |           },
27 |         );
28 |       })->then( sub {
29 |          repeat_until_true {
30 |             $outbound_client->do_request_json(
31 |                method   => "GET",
32 |                hostname => $first_home_server,
33 |                uri      => "/v1/publicRooms",
34 |             )->then( sub {
35 |                my ( $body ) = @_;
36 | 
37 |                log_if_fail "Body", $body;
38 | 
39 |                assert_json_keys( $body, qw( chunk ) );
40 | 
41 |                Future->done( any { $_->{room_id} eq $room_id } @{ $body->{chunk} } );
42 |             })
43 |          };
44 |       });
45 |    };
46 | 


--------------------------------------------------------------------------------
/tests/50federation/40publicroomlist.pl:
--------------------------------------------------------------------------------
  1 | # Copied from 30rooms/70publicroomslist.pl, modified to test the federation publicRooms API
  2 | test "Federation publicRoom Name/topic keys are correct",
  3 |    requires => [ local_user_fixture(), $main::OUTBOUND_CLIENT ],
  4 | 
  5 |    check => sub {
  6 |       my ( $user, $client ) = @_;
  7 | 
  8 |       my $server_name = $user->server_name;
  9 | 
 10 |       my %rooms = (
 11 |          publicroomalias_no_name_30_70_test => {},
 12 |          publicroomalias_with_name_30_70_test => {
 13 |             name => "name_1",
 14 |          },
 15 |          publicroomalias_with_topic_30_70_test => {
 16 |             topic => "topic_1",
 17 |          },
 18 |          publicroomalias_with_name_topic_30_70_test => {
 19 |             name => "name_2",
 20 |             topic => "topic_2",
 21 |          },
 22 |       );
 23 | 
 24 |       Future->needs_all( map {
 25 |          my $alias_local = $_;
 26 |          my $room = $rooms{$alias_local};
 27 | 
 28 |          matrix_create_room_synced( $user,
 29 |             visibility      => "public",
 30 |             room_alias_name => $alias_local,
 31 |             %$room,
 32 |          )->on_done( sub {
 33 |             my ( $room_id ) = @_;
 34 |             log_if_fail "Created room $room_id with alias $alias_local";
 35 |          });
 36 |       } keys %rooms )
 37 |       ->then( sub {
 38 |          my $iter = 0;
 39 |          retry_until_success {
 40 |             $client->do_request_json(
 41 |                 method   => "GET",
 42 |                 hostname => $server_name,
 43 |                 uri      => "/v1/publicRooms",
 44 |             )->then( sub {
 45 |                my ($body) = @_;
 46 | 
 47 |                $iter++;
 48 |                log_if_fail "Iteration $iter: publicRooms result", $body;
 49 | 
 50 |                assert_json_keys($body, qw(chunk));
 51 |                assert_json_list($body->{chunk});
 52 | 
 53 |                my %seen = map {
 54 |                   $_ => 0,
 55 |                } keys(%rooms);
 56 | 
 57 |                foreach my $room (@{$body->{chunk}}) {
 58 |                   assert_json_keys($room,
 59 |                       qw(world_readable guest_can_join num_joined_members)
 60 |                   );
 61 | 
 62 |                   my $name = $room->{name};
 63 |                   my $topic = $room->{topic};
 64 |                   my $canonical_alias = $room->{canonical_alias};
 65 | 
 66 |                   foreach my $alias_local (keys %rooms) {
 67 |                      $canonical_alias =~ m/^\Q#$alias_local:\E/ or next;
 68 | 
 69 |                      my $room_config = $rooms{$alias_local};
 70 | 
 71 |                      log_if_fail "Alias", $alias_local;
 72 |                      log_if_fail "Room", $room;
 73 | 
 74 |                      assert_eq($room->{num_joined_members}, 1, "num_joined_members");
 75 | 
 76 |                      if (defined $name) {
 77 |                         assert_eq($room_config->{name}, $name, 'room name');
 78 |                      }
 79 |                      else {
 80 |                         defined $room_config->{name} and die "Expected not to find a name";
 81 |                      }
 82 | 
 83 |                      if (defined $topic) {
 84 |                         assert_eq($room_config->{topic}, $topic, 'room topic');
 85 |                      }
 86 |                      else {
 87 |                         defined $room_config->{topic} and die "Expected not to find a topic";
 88 |                      }
 89 | 
 90 |                      $seen{$alias_local} = 1;
 91 |                      last;
 92 |                   }
 93 |                }
 94 | 
 95 |                foreach my $key (keys %seen) {
 96 |                   $seen{$key} or die "Did not find a /publicRooms result for $key";
 97 |                }
 98 | 
 99 |                Future->done(1);
100 |             });
101 |          };
102 |       })
103 |    }
104 | 


--------------------------------------------------------------------------------
/tests/50federation/43typing.pl:
--------------------------------------------------------------------------------
 1 | # note that there are happy-path tests for typing over federation in tests/30rooms/20typing.pl.
 2 | 
 3 | test "Inbound federation rejects typing notifications from wrong remote",
 4 |    requires => [
 5 |       $main::OUTBOUND_CLIENT,
 6 |       local_user_and_room_fixtures(),
 7 |       federation_user_id_fixture(),
 8 |    ],
 9 | 
10 |    do => sub {
11 |       my ( $outbound_client, $creator, $room_id, $user_id ) = @_;
12 | 
13 |       my $local_server_name = $creator->server_name;
14 | 
15 |       $outbound_client->join_room(
16 |          server_name => $local_server_name,
17 |          room_id     => $room_id,
18 |          user_id     => $user_id,
19 |       )->then( sub {
20 |          # First we send a typing notif from a user that isn't ours.
21 |          $outbound_client->send_edu(
22 |             edu_type    => "m.typing",
23 |             destination => $local_server_name,
24 |             content     => {
25 |                room_id => $room_id,
26 |                user_id => $creator->user_id,
27 |                typing  => JSON::true,
28 |             },
29 |          );
30 |       })->then( sub {
31 |          # Then we send one for a user that is ours.
32 |          $outbound_client->send_edu(
33 |             edu_type    => "m.typing",
34 |             destination => $local_server_name,
35 |             content     => {
36 |                room_id => $room_id,
37 |                user_id => $user_id,
38 |                typing  => JSON::true,
39 |             },
40 |          );
41 |       })->then( sub {
42 |          # The sync should only contain the second typing notif, since the first
43 |          # should have been dropped.
44 |          await_sync( $creator, check => sub {
45 |             my ( $body ) = @_;
46 | 
47 |             sync_room_contains( $body, $room_id, "ephemeral", sub {
48 |                my ( $edu ) = @_;
49 | 
50 |                log_if_fail "received edu", $edu;
51 | 
52 |                return unless $edu->{type} eq "m.typing";
53 | 
54 |                my @users = @{ $edu->{content}->{user_ids} };
55 | 
56 |                # Check for bad user
57 |                die "Found typing notif that should have been rejected"
58 |                   if any { $_ ne $user_id } @users;
59 | 
60 |                # Stop waiting when we find the good user (ie, we have a non-empty list)
61 |                return scalar @users;
62 |             })
63 |          })
64 |       });
65 |    };
66 | 


--------------------------------------------------------------------------------
/tests/50federation/44presence.pl:
--------------------------------------------------------------------------------
 1 | use Future::Utils qw( repeat );
 2 | 
 3 | multi_test "New federated private chats get full presence information (SYN-115)",
 4 |    requires => [ local_user_fixture(), remote_user_fixture( with_events => 1 ),
 5 |                  qw( can_create_private_room )],
 6 | 
 7 |    do => sub {
 8 |       my ( $alice, $bob ) = @_;
 9 | 
10 |       my $room_id;
11 | 
12 |       # Flush event streams for both; as a side-effect will mark presence 'online'
13 |       Future->needs_all(
14 |          flush_events_for( $alice ),
15 |          flush_events_for( $bob   ),
16 |       )->then( sub {
17 | 
18 |          # Have Alice create a new private room
19 |          matrix_create_room_synced( $alice,
20 |             visibility => "private",
21 |          )->SyTest::pass_on_done( "Created a room" )
22 |       })->then( sub {
23 |          ( $room_id ) = @_;
24 | 
25 |          # Alice invites Bob
26 |          matrix_invite_user_to_room_synced( $alice, $bob, $room_id )
27 |             ->SyTest::pass_on_done( "Sent invite" )
28 |       })->then( sub {
29 | 
30 |          # Bob should receive the invite
31 |          await_sync( $bob, check => sub {
32 |             my ( $body ) = @_;
33 | 
34 |             return 0 unless exists $body->{rooms}{invite}{$room_id};
35 |             return $body->{rooms}{invite}{$room_id};
36 |          })->SyTest::pass_on_done( "Bob received invite" ),
37 |       })->then( sub {
38 | 
39 |          # Bob accepts the invite by joining the room
40 |          matrix_join_room_synced( $bob, $room_id )
41 |             ->SyTest::pass_on_done( "Joined room" )
42 |       })->then( sub {
43 | 
44 |          # At this point, both users should see both users' presence, either
45 |          # right now via global /initialSync, or should soon receive an
46 |          # m.presence event from /events.
47 |          Future->needs_all( map {
48 |             my $user = $_;
49 | 
50 |             my %presence_by_userid;
51 | 
52 |             my $f = repeat {
53 | 
54 |                await_sync_presence_contains( $user, check => sub {
55 |                   my ( $event ) = @_;
56 |                   return unless $event->{type} eq "m.presence";
57 |                   return 1;
58 |                })->then( sub {
59 |                   my ( $body ) = @_;
60 |                   my @presence = @{ $body->{presence}{events} };
61 | 
62 |                   foreach my $event ( @presence ) {
63 |                      
64 |                      my $user_id = $event->{sender};
65 |                      pass "User ${\$user->user_id} received presence for $user_id";
66 |                      $presence_by_userid{$user_id} = $event;
67 |                   }
68 | 
69 |                   Future->done(1);
70 |                });
71 |             } until => sub { keys %presence_by_userid == 2 };
72 | 
73 |             Future->wait_any(
74 |                $f,
75 | 
76 |                delay( 2 )
77 |                   ->then_fail( "Timed out waiting for ${\$user->user_id} to receive all presence" )
78 |             );
79 |          } $alice, $bob )
80 |          ->SyTest::pass_on_done( "Both users see both users' presence" )
81 |       })->then_done(1);
82 |    };
83 | 


--------------------------------------------------------------------------------
/tests/51media/20urlpreview.pl:
--------------------------------------------------------------------------------
 1 | use File::Basename qw( dirname );
 2 | use File::Slurper qw( read_binary );
 3 | 
 4 | my $OGRAPH_TITLE = "The Rock";
 5 | my $OGRAPH_TYPE = "video.movie";
 6 | my $OGRAPH_URL = "http://www.imdb.com/title/tt0117500/";
 7 | my $OGRAPH_IMAGE = "test.png";
 8 | 
 9 | my $EXAMPLE_OPENGRAPH_HTML = <<"EOHTML";
10 | 
11 | 
12 | The Rock (1996)
13 | 
14 | 
15 | 
16 | 
17 | 
18 | 
19 | 
20 | 
21 | EOHTML
22 | 
23 | my $DIR = dirname __FILE__;
24 | 
25 | multi_test "Test URL preview",
26 |    requires => [
27 |       local_user_fixture( with_events => 0 ), $main::TEST_SERVER_INFO,
28 |    ],
29 | 
30 |    do => sub {
31 |       my ( $user, $test_server_info ) = @_;
32 | 
33 |       Future->needs_all(
34 |          # TODO(check that the HTTP poke is actually the poke we wanted)
35 |          await_http_request( "/test.html", sub {
36 |             return 1;
37 |          })->then( sub {
38 |             my ( $request ) = @_;
39 | 
40 |             my $response = HTTP::Response->new( 200 );
41 |             $response->add_content( $EXAMPLE_OPENGRAPH_HTML );
42 |             $response->content_type( "text/html" );
43 |             $response->content_length( length $response->content );
44 | 
45 |             $request->respond( $response );
46 | 
47 |             Future->done();
48 |          })->SyTest::pass_on_done( "URL was fetched" ),
49 | 
50 |          await_http_request( "/test.png", sub {
51 |             return 1;
52 |          })->then( sub {
53 |             my ( $request ) = @_;
54 | 
55 |             my $pngdata = read_binary( "$DIR/test.png" );
56 | 
57 |             my $response = HTTP::Response->new( 200 );
58 |             $response->add_content( $pngdata );
59 |             $response->content_type( "image/png" );
60 |             $response->content_length( length $response->content );
61 | 
62 |             $request->respond( $response );
63 | 
64 |             Future->done();
65 |          })->SyTest::pass_on_done( "Image was fetched" ),
66 | 
67 |          $user->http->do_request(
68 |             method   => "GET",
69 |             full_uri => "/_matrix/media/v3/preview_url",
70 |             params   => {
71 |                url          => $test_server_info->client_location . "/test.html",
72 |                access_token => $user->access_token,
73 |             },
74 |          ),
75 |       )->SyTest::pass_on_done( "Preview returned successfully" )
76 |       ->then( sub {
77 |          my ( $preview_body ) = @_;
78 | 
79 |          log_if_fail "Preview body", $preview_body;
80 | 
81 |          assert_json_keys( $preview_body, qw( og:title og:type og:url og:image matrix:image:size og:image:height og:image:width ) );
82 | 
83 |          assert_eq( $preview_body->{"og:title"}, $OGRAPH_TITLE );
84 |          assert_eq( $preview_body->{"og:type"}, $OGRAPH_TYPE );
85 |          assert_eq( $preview_body->{"og:url"}, $OGRAPH_URL );
86 |          assert_eq( $preview_body->{"matrix:image:size"}, 2239 );
87 |          assert_eq( $preview_body->{"og:image:height"}, 129 );
88 |          assert_eq( $preview_body->{"og:image:width"}, 279 );
89 | 
90 |          $preview_body->{"og:image"} =~ m/^mxc:\/\// or die "Expected mxc url for og:image";
91 | 
92 |          Future->done( 1 );
93 |       })->SyTest::pass_on_done( "Preview API returned expected values" )
94 |    };
95 | 


--------------------------------------------------------------------------------
/tests/51media/30config.pl:
--------------------------------------------------------------------------------
 1 | test "Can read configuration endpoint",
 2 |    requires => [ $main::API_CLIENTS[0], local_user_fixture() ],
 3 | 
 4 |    check => sub {
 5 |         my ( $http, $user ) = @_;
 6 |         $http->do_request_json(
 7 |             method   => "GET",
 8 |             full_uri => "/_matrix/media/v3/config",
 9 |             params => {
10 |                 access_token => $user->access_token,
11 |             }
12 |         )->then( sub {
13 |             my ( $body ) = @_;
14 | 
15 |             # TODO: Check size is correct
16 |             if ( defined $body->{"m.upload.size"} ) {
17 |                 assert_json_number( $body->{"m.upload.size"} )
18 |             }
19 | 
20 |             Future->done(1);
21 |         });
22 |    };
23 | 


--------------------------------------------------------------------------------
/tests/51media/48admin-quarantine.pl:
--------------------------------------------------------------------------------
 1 | use File::Basename qw( dirname );
 2 | use File::Slurper qw( read_binary );
 3 | 
 4 | my $dir = dirname __FILE__;
 5 | 
 6 | multi_test "Can quarantine media in rooms",
 7 |    requires => [ local_admin_fixture(), local_user_and_room_fixtures(), remote_user_fixture(),
 8 |                  qw( can_upload_media can_download_media )],
 9 | 
10 |    do => sub {
11 |       my ( $admin, $user, $room_id, $remote_user ) = @_;
12 | 
13 |       my $pngdata = read_binary( "$dir/test.png" );
14 | 
15 |       my $content_id;
16 | 
17 |       # Because we're POST'ing non-JSON
18 |       $user->http->do_request(
19 |          method => "POST",
20 |          full_uri => "/_matrix/media/v3/upload",
21 |          params => {
22 |             access_token => $user->access_token,
23 |          },
24 | 
25 |          content_type => "image/png",
26 |          content      => $pngdata,
27 |       )->then( sub {
28 |          my ( $body ) = @_;
29 | 
30 |          my $content_uri = URI->new( $body->{content_uri} );
31 | 
32 |          my $server = $content_uri->authority;
33 |          my $path = $content_uri->path;
34 | 
35 |          $content_id = "$server$path";
36 | 
37 |          matrix_send_room_message( $user, $room_id,
38 |             content => {
39 |                msgtype => "m.image",
40 |                body => "test.png",
41 |                url => "$content_uri",
42 |             },
43 |          );
44 |       })->then( sub {
45 |          do_request_json_for( $admin,
46 |             method   => "POST",
47 |             full_uri => "/_synapse/admin/v1/room/$room_id/media/quarantine",
48 |             content  => {},
49 |          );
50 |       })->SyTest::pass_on_done( "Quarantine returns success" )
51 |       ->then( sub {
52 |          $user->http->do_request(
53 |             method   => "GET",
54 |             full_uri => "/_matrix/media/v3/download/$content_id",
55 |          )->main::expect_http_404;
56 |       })->SyTest::pass_on_done( "404 on getting quarantined local media" )
57 |       ->then( sub {
58 |          $user->http->do_request(
59 |             method => "GET",
60 |             full_uri => "/_matrix/media/v3/thumbnail/$content_id",
61 |             params => {
62 |                width  => 32,
63 |                height => 32,
64 |                method => "scale",
65 |             },
66 |          )->main::expect_http_404
67 |       })->SyTest::pass_on_done( "404 on getting quarantined local thumbnails" )
68 |       ->then( sub {
69 |          $remote_user->http->do_request(
70 |             method   => "GET",
71 |             full_uri => "/_matrix/media/v3/download/$content_id",
72 |          )->main::expect_http_404;
73 |       })->SyTest::pass_on_done( "404 on getting quarantined remote media" )
74 |       ->then( sub {
75 |          $remote_user->http->do_request(
76 |             method => "GET",
77 |             full_uri => "/_matrix/media/v3/thumbnail/$content_id",
78 |             params => {
79 |                width  => 32,
80 |                height => 32,
81 |                method => "scale",
82 |             }
83 |          )->main::expect_http_404;
84 |       })->SyTest::pass_on_done( "404 on getting quarantined remote thumbnails" );
85 |    };
86 | 


--------------------------------------------------------------------------------
/tests/51media/test.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matrix-org/sytest/52c5ba88cc0765ff82e1303821933ebdcedc4433/tests/51media/test.png


--------------------------------------------------------------------------------
/tests/60app-services/00prepare.pl:
--------------------------------------------------------------------------------
 1 | use SyTest::ApplicationService;
 2 | 
 3 | push our @EXPORT, qw( AS_USER APPSERV );
 4 | 
 5 | our @AS_USER = map {
 6 |    my $AS_INFO = $_;
 7 | 
 8 |    fixture(
 9 |       requires => [ $main::API_CLIENTS[0], $AS_INFO ],
10 | 
11 |       setup => sub {
12 |          my ( $http, $as_user_info ) = @_;
13 | 
14 |          Future->done( new_User(
15 |             http         => $http,
16 |             user_id      => $as_user_info->user_id,
17 |             access_token => $as_user_info->as2hs_token,
18 |          ));
19 |       },
20 |    );
21 | } @main::AS_INFO;
22 | 
23 | our @APPSERV = map {
24 |    my $AS_INFO = $_;
25 | 
26 |    fixture(
27 |       requires => [ $AS_INFO ],
28 | 
29 |       setup => sub {
30 |          my ( $info ) = @_;
31 | 
32 |          Future->done( SyTest::ApplicationService->new(
33 |             $info, \&main::await_http_request
34 |          ) );
35 |       }
36 |    );
37 | } @main::AS_INFO;
38 | 


--------------------------------------------------------------------------------
/tests/60app-services/04asuser.pl:
--------------------------------------------------------------------------------
 1 | test "AS user (not ghost) can join room without registering",
 2 |    requires => [ $main::AS_USER[0], local_user_fixture(), $main::HOMESERVER_INFO[0] ],
 3 | 
 4 |    do => sub {
 5 |       my ( $as_user, $user, $hs_info ) = @_;
 6 | 
 7 |       my $room_id;
 8 | 
 9 |       matrix_create_room( $user )->then( sub {
10 |          ( $room_id ) = @_;
11 | 
12 |          matrix_invite_user_to_room( $user, $as_user, $room_id )
13 |       })->then( sub {
14 |          matrix_join_room( $as_user, $room_id )
15 |       });
16 |    };
17 | 
18 | # TODO: Remove this test when the in-use ASes stop passing the user_id param
19 | # (or if we end up killing non-ghost AS users)
20 | test "AS user (not ghost) can join room without registering, with user_id query param",
21 |    requires => [ $main::AS_USER[0], local_user_fixture() ],
22 | 
23 |    do => sub {
24 |       my ( $as_user, $user ) = @_;
25 | 
26 |       my $room_id;
27 | 
28 |       matrix_create_room( $user )->then( sub {
29 |          ( $room_id ) = @_;
30 | 
31 |          matrix_invite_user_to_room( $user, $as_user, $room_id )
32 |       })->then( sub {
33 |          do_request_json_for( $as_user,
34 |             method => "POST",
35 |             uri    => "/v3/join/$room_id",
36 | 
37 |             params => {
38 |                user_id => $as_user->user_id,
39 |             },
40 |             content => {},
41 |          );
42 |       });
43 |    };
44 | 


--------------------------------------------------------------------------------
/tests/60app-services/07deactivate.pl:
--------------------------------------------------------------------------------
 1 | test "AS can deactivate a user",
 2 |    requires => [ $main::AS_USER[0], as_ghost_fixture() ],
 3 | 
 4 |    do => sub {
 5 |       my ( $as_user, $ghost ) = @_;
 6 | 
 7 |       do_request_json_for(
 8 |          $as_user,
 9 |          method  => "POST",
10 |          uri     => "/v3/account/deactivate",
11 |          params  => { user_id => $ghost->user_id },
12 |          content => {},
13 |       );
14 |    };
15 | 


--------------------------------------------------------------------------------
/tests/61push/05_set_actions.pl:
--------------------------------------------------------------------------------
 1 | use Future::Utils qw( repeat );
 2 | 
 3 | sub check_change_action {
 4 |    my ( $user, $scope, $kind, $rule_id, $actions ) = @_;
 5 | 
 6 |    matrix_set_push_rule_actions( $user, $scope, $kind, $rule_id, $actions )
 7 |    ->then( sub {
 8 |       # Check that the actions match.
 9 |       do_request_json_for( $user,
10 |          method  => "GET",
11 |          uri     => "/v3/pushrules/$scope/$kind/$rule_id/actions",
12 |       )->on_done( sub {
13 |          my ( $body ) = @_;
14 | 
15 |          assert_deeply_eq( $body, { actions => $actions } );
16 |       });
17 |    });
18 | }
19 | 
20 | test "Can change the actions of default rules",
21 |    requires => [ local_user_fixture() ],
22 | 
23 |    check => sub {
24 |       my ( $user ) = @_;
25 | 
26 |       my $actions = [ "dont_notify" ];
27 | 
28 |       matrix_get_push_rules( $user )->then( sub {
29 |          my ( $body ) = @_;
30 | 
31 |          my @to_check;
32 | 
33 |          foreach my $kind ( keys %{ $body->{global} } ) {
34 |             foreach my $rule ( @{ $body->{global}{$kind} } ) {
35 |                push @to_check, [ $kind, $rule->{rule_id} ];
36 |             }
37 |          }
38 | 
39 |          repeat {
40 |             my $to_check = shift;
41 | 
42 |             my ( $kind, $rule_id ) = @$to_check;
43 | 
44 |             check_change_action( $user, "global", $kind, $rule_id, $actions );
45 |          } foreach => \@to_check;
46 |       });
47 |    };
48 | 
49 | 
50 | test "Changing the actions of an unknown default rule fails with 404",
51 |    requires => [ local_user_fixture() ],
52 | 
53 |    check => sub  {
54 |       my ( $user ) = @_;
55 |       matrix_set_push_rule_actions(
56 |          $user, "global", "override", ".not.a.default.rule", [
57 |             "notify"
58 |          ],
59 |       )->main::expect_http_404;
60 |    };
61 | 
62 | 
63 | test "Can change the actions of a user specified rule",
64 |    requires => [ local_user_fixture() ],
65 | 
66 |    check => sub {
67 |       my ( $user ) = @_;
68 | 
69 |       matrix_add_push_rule( $user, "global", "sender", '@bob:example.com', {
70 |          actions => [ "notify" ]
71 |       })->then( sub {
72 |          check_change_action( $user, "global", "sender", '@bob:example.com', [
73 |             "dont_notify",
74 |          ]);
75 |       });
76 |    };
77 | 
78 | 
79 | test "Changing the actions of an unknown rule fails with 404",
80 |    requires => [ local_user_fixture() ],
81 | 
82 |    check => sub  {
83 |       my ( $user ) = @_;
84 |       matrix_set_push_rule_actions(
85 |          $user, "global", "sender", '@bob:example.com', [ "notify" ]
86 |       )->main::expect_http_404;
87 |    };
88 | 
89 | 


--------------------------------------------------------------------------------
/tests/61push/06_get_pusher.pl:
--------------------------------------------------------------------------------
 1 | test "Can fetch a user's pushers",
 2 |    requires => [ local_user_fixture( ) ],
 3 | 
 4 |    check => sub {
 5 |       my ( $alice ) = @_;
 6 | 
 7 |       my $profile_tag = "tag";
 8 |       my $app_id = "sytest";
 9 |       my $app_display_name = "SyTest";
10 |       my $device_display_name = "A testing machine";
11 |       my $pushkey = "This is my pushkey";
12 |       my $lang = "en";
13 |       my $customdata = "This is some custom data";
14 |       my $url = "https://dummy.url/_matrix/push/v1/notify";
15 |       my $format = "id_event_only";
16 | 
17 |       # create a pusher
18 |       do_request_json_for( $alice,
19 |          method  => "POST",
20 |          uri     => "/v3/pushers/set",
21 |          content => {
22 |             profile_tag         => $profile_tag,
23 |             kind                => "http",
24 |             app_id              => $app_id,
25 |             app_display_name    => $app_display_name,
26 |             device_display_name => $device_display_name,
27 |             pushkey             => $pushkey,
28 |             lang                => $lang,
29 |             data                => {
30 |                testcustom => $customdata,
31 |                url => $url,
32 |                format => $format,
33 |             },
34 |          },
35 |       )->then( sub {
36 |          do_request_json_for( $alice,
37 |             method  => "GET",
38 |             uri     => "/v3/pushers",
39 |          );
40 |       })->then( sub {
41 |          my ( $body ) = @_;
42 | 
43 |          log_if_fail "Get pusher response body", $body;
44 | 
45 |          assert_json_keys( $body, qw(pushers) );
46 | 
47 |          assert_json_keys( my $pusher = $body->{pushers}[0], qw(
48 |             profile_tag kind app_id app_display_name device_display_name
49 |             pushkey lang data
50 |          ));
51 |          assert_eq( $pusher->{profile_tag}, $profile_tag, "profile tag");
52 |          assert_eq( $pusher->{app_id}, $app_id, "app id");
53 |          assert_eq( $pusher->{app_display_name}, $app_display_name, "app_display_name");
54 |          assert_eq( $pusher->{device_display_name}, $device_display_name, "device_display_name");
55 |          assert_eq( $pusher->{pushkey}, $pushkey, "pushkey");
56 |          assert_eq( $pusher->{lang}, $lang, "lang");
57 |          assert_eq( $pusher->{data}{testcustom}, $customdata, "custom data");
58 |          assert_eq( $pusher->{data}{url}, $url, "URL");
59 |          assert_eq( $pusher->{data}{format}, $format, "format");
60 | 
61 |          Future->done(1);
62 |       });
63 |    };
64 | 


--------------------------------------------------------------------------------
/tests/61push/07_set_enabled.pl:
--------------------------------------------------------------------------------
 1 | use Future::Utils qw( repeat );
 2 | 
 3 | sub check_change_enabled
 4 | {
 5 |    my ( $user, $scope, $kind, $rule_id, $enabled ) = @_;
 6 | 
 7 |    matrix_set_push_rule_enabled( $user, $scope, $kind, $rule_id, $enabled )
 8 |    ->then( sub {
 9 |       # Check that the actions match.
10 |       do_request_json_for( $user,
11 |          method  => "GET",
12 |          uri     => "/v3/pushrules/$scope/$kind/$rule_id/enabled",
13 |       )->on_done( sub {
14 |          my ( $body ) = @_;
15 | 
16 |          assert_deeply_eq( $body, { enabled => $enabled } );
17 |       });
18 |    });
19 | }
20 | 
21 | sub check_enable_disable_rule {
22 |    my ( $user, $scope, $kind, $rule_id ) = @_;
23 | 
24 |    check_change_enabled( $user, $scope, $kind, $rule_id, JSON::true )
25 |    ->then( sub {
26 |       check_change_enabled( $user, $scope, $kind, $rule_id, JSON::false );
27 |    })->then( sub {
28 |       check_change_enabled( $user, $scope, $kind, $rule_id, JSON::true );
29 |    });
30 | }
31 | 
32 | test "Can enable/disable default rules",
33 |    requires => [ local_user_fixture() ],
34 | 
35 |    check => sub {
36 |       my ( $user ) = @_;
37 | 
38 |       matrix_get_push_rules( $user )->then( sub {
39 |          my ( $body ) = @_;
40 | 
41 |          my @to_check;
42 | 
43 |          foreach my $kind ( keys %{ $body->{global} } ) {
44 |             foreach my $rule ( @{ $body->{global}{$kind} } ) {
45 |                push @to_check, [ $kind, $rule->{rule_id} ];
46 |             }
47 |          }
48 | 
49 |          repeat {
50 |             my $to_check = shift;
51 | 
52 |             my ( $kind, $rule_id ) = @$to_check;
53 | 
54 |             log_if_fail("testing $rule_id");
55 |             check_enable_disable_rule( $user, "global", $kind, $rule_id );
56 |          } foreach => \@to_check;
57 |       });
58 |    };
59 | 
60 | test "Enabling an unknown default rule fails with 404",
61 |    requires => [ local_user_fixture() ],
62 | 
63 |    check => sub {
64 |       my ( $user ) = @_;
65 |       matrix_set_push_rule_enabled(
66 |          $user, "global", "override", ".not.a.default.rule", JSON::true
67 |       )->main::expect_http_404;
68 |    };
69 | 


--------------------------------------------------------------------------------
/tests/61push/80torture.pl:
--------------------------------------------------------------------------------
  1 | 
  2 | my $TO_CHECK_PUT = [
  3 |    [ "no scope", "/", {} ],
  4 |    [ "invalid scope", "/not_a_scope/room/#spam:example.com", {} ],
  5 |    [ "missing template", "/global", {} ],
  6 |    [ "missing rule_id", "/global/room", {} ],
  7 |    [ "empty rule_id", "/global/room/", {} ],
  8 |    [ "invalid template", "/global/not_a_template/foo", {} ],
  9 |    [ "rule_id with slashes", "/global/room/#fo\\o:example.com", {} ],
 10 |    [ "override rule without conditions", "/global/override/my_id", {} ],
 11 |    [ "underride rule without conditions", "/global/underride/my_id", {} ],
 12 |    [ "condition without kind", "/global/underride/my_id", {
 13 |       conditions => [ {} ]
 14 |    }],
 15 |    [ "content rule without pattern", "/global/content/my_id", {} ],
 16 |    [ "no actions", "/global/room/#my_room:example.com", {} ],
 17 |    [ "invalid action", "/global/room/#my_room:example.com", {
 18 |       actions => ["not_an_action"]
 19 |    }],
 20 |    [ "invalid attr", "/global/override/.m.rule.master/not_an_attr", {
 21 |       enabled => JSON::true,
 22 |    }],
 23 |    [ "invalid value for enabled", "/global/override/.m.rule.master/enabled", {
 24 |       enabled => "not a boolean"
 25 |    }],
 26 | ];
 27 | 
 28 | foreach my $test_put ( @$TO_CHECK_PUT ) {
 29 |    my ( $name, $path, $rule ) = @$test_put;
 30 | 
 31 |    test "Trying to add push rule with $name fails with 400",
 32 |       requires => [ local_user_fixture() ],
 33 | 
 34 |       check => sub {
 35 |          my ( $user ) = @_;
 36 | 
 37 |          do_request_json_for( $user,
 38 |             method  => "PUT",
 39 |             uri     => "/v3/pushrules$path",
 40 |             content => $rule,
 41 |          )->main::expect_http_400;
 42 |       };
 43 | };
 44 | 
 45 | test "Trying to get push rules with no trailing slash fails with 404",
 46 |   requires => [ local_user_fixture() ],
 47 | 
 48 |   check => sub {
 49 |      my ( $user ) = @_;
 50 | 
 51 |      do_request_json_for( $user,
 52 |         method  => "GET",
 53 |         uri     => "/v3/pushrules",
 54 |      )->main::expect_http_404;
 55 |   };
 56 | 
 57 | my $TO_CHECK_GET_400 = [
 58 |    [ "scope without trailing slash", "/global" ],
 59 |    [ "template without tailing slash", "/global/room" ],
 60 |    [ "unknown scope", "/not_a_scope/" ],
 61 |    [ "unknown template", "/global/not_a_template/" ],
 62 |    [ "unknown attribute", "/global/override/.m.rule.master/not_an_attr"],
 63 | ];
 64 | 
 65 | foreach my $test_get ( @$TO_CHECK_GET_400 ) {
 66 |    my ( $name, $path ) = @$test_get;
 67 | 
 68 |    test "Trying to get push rules with $name fails with 400",
 69 |       requires => [ local_user_fixture() ],
 70 | 
 71 |       check => sub {
 72 |          my ( $user ) = @_;
 73 | 
 74 |          do_request_json_for( $user,
 75 |             method  => "GET",
 76 |             uri     => "/v3/pushrules$path",
 77 |          )->main::expect_http_400;
 78 |       };
 79 | };
 80 | 
 81 | my $TO_CHECK_GET_404 = [
 82 |    [ "unknown rule_id", "/global/override/not_a_rule_id" ],
 83 | ];
 84 | 
 85 | foreach my $test_get ( @$TO_CHECK_GET_404 ) {
 86 |    my ( $name, $path ) = @$test_get;
 87 | 
 88 |    test "Trying to get push rules with $name fails with 404",
 89 |       requires => [ local_user_fixture() ],
 90 | 
 91 |       check => sub {
 92 |          my ( $user ) = @_;
 93 | 
 94 |          do_request_json_for( $user,
 95 |             method  => "GET",
 96 |             uri     => "/v3/pushrules$path",
 97 |          )->main::expect_http_404;
 98 |       };
 99 | };
100 | 


--------------------------------------------------------------------------------
/tests/80torture/03events.pl:
--------------------------------------------------------------------------------
 1 | test "GET /initialSync with non-numeric 'limit'",
 2 |    deprecated_endpoints => 1,
 3 |    requires => [ $main::SPYGLASS_USER,
 4 |                  qw( can_initial_sync )],
 5 | 
 6 |    check => sub {
 7 |       my ( $user ) = @_;
 8 | 
 9 |       do_request_json_for( $user,
10 |          method => "GET",
11 |          uri    => "/v3/initialSync",
12 | 
13 |          params => { limit => "hello" },
14 |       )->main::expect_http_4xx;
15 |    };
16 | 
17 | test "GET /events with non-numeric 'limit'",
18 |    deprecated_endpoints => 1,
19 |    requires => [ $main::SPYGLASS_USER ],
20 | 
21 |    check => sub {
22 |       my ( $user ) = @_;
23 | 
24 |       do_request_json_for( $user,
25 |          method => "GET",
26 |          uri    => "/r0/events",
27 | 
28 |          params => { from => $user->eventstream_token, limit => "hello" },
29 |       )->main::expect_http_4xx;
30 |    };
31 | 
32 | test "GET /events with negative 'limit'",
33 |    deprecated_endpoints => 1,
34 |    requires => [ $main::SPYGLASS_USER ],
35 | 
36 |    check => sub {
37 |       my ( $user ) = @_;
38 | 
39 |       do_request_json_for( $user,
40 |          method => "GET",
41 |          uri    => "/r0/events",
42 | 
43 |          params => { from => $user->eventstream_token, limit => -2 },
44 |       )->main::expect_http_4xx;
45 |    };
46 | 
47 | test "GET /events with non-numeric 'timeout'",
48 |    deprecated_endpoints => 1,
49 |    requires => [ $main::SPYGLASS_USER ],
50 | 
51 |    check => sub {
52 |       my ( $user ) = @_;
53 | 
54 |       do_request_json_for( $user,
55 |          method => "GET",
56 |          uri    => "/r0/events",
57 | 
58 |          params => { from => $user->eventstream_token, timeout => "hello" },
59 |       )->main::expect_http_4xx;
60 |    };
61 | 
62 | test "Event size limits",
63 |    requires => [ local_user_and_room_fixtures() ],
64 | 
65 |    do => sub {
66 |       my ( $user, $room_id ) = @_;
67 | 
68 |       Future->needs_all(
69 |          do_request_json_for( $user,
70 |             method  => "POST",
71 |             uri     => "/v3/rooms/$room_id/send/m.room.message",
72 |             content => {
73 |                msgtype => "m.text",
74 |                body    => "A" x 70000,
75 |             },
76 |          )->followed_by( \&main::expect_http_413 ),
77 | 
78 |          do_request_json_for( $user,
79 |             method  => "PUT",
80 |             uri     => "/v3/rooms/$room_id/state/oooooooh",
81 |             content => {
82 |                key => "O" x 70000,
83 |             }
84 |          )->followed_by( \&main::expect_http_413 ),
85 |       );
86 |    };
87 | 


--------------------------------------------------------------------------------
/tests/80torture/10filters.pl:
--------------------------------------------------------------------------------
 1 | my $INVALID_FILTERS = [
 2 |    { presence => "not_an_object" },
 3 |    { room => { timeline => "not_an_object" } },
 4 |    { room => { state => "not_an_object" } },
 5 |    { room => { ephemeral => "not_an_object" } },
 6 |    { room => { account_data => "not_an_object" } },
 7 |    { room => { timeline => { "rooms" => "not_a_list" } } },
 8 |    { room => { timeline => { "not_rooms" => "not_a_list" } } },
 9 |    { room => { timeline => { "senders" => "not_a_list" } } },
10 |    { room => { timeline => { "not_senders" => "not_a_list" } } },
11 |    { room => { timeline => { "types" => "not_a_list" } } },
12 |    { room => { timeline => { "not_types" => "not_a_list"} } },
13 |    { room => { timeline => { "types" => [ 0 ] } } },
14 |    { room => { timeline => { "rooms" => [ "not_a_room_id" ] } } },
15 |    { room => { timeline => { "senders" => [ "not_a_sender_id" ] } } },
16 | ];
17 | 
18 | 
19 | test "Check creating invalid filters returns 4xx",
20 |    requires => [ local_user_fixture( with_events => 0 ) ],
21 | 
22 |    check => sub {
23 |       my ( $user ) = @_;
24 | 
25 |       Future->wait_all( map {
26 |          my $filter = $_;
27 |          matrix_create_filter( $user, $_ )
28 |             ->main::expect_http_4xx
29 |             ->on_fail( sub { log_if_fail "Filter:", $filter; });
30 |       } @{ $INVALID_FILTERS } )->then( sub {
31 |          # Wait for all the requests to finish, then check that all of them
32 |          # succeeded.
33 |          Future->needs_all( @_ );
34 |       });
35 |    };
36 | 


--------------------------------------------------------------------------------
/tests/90jira/SYN-205.pl:
--------------------------------------------------------------------------------
 1 | multi_test "Rooms can be created with an initial invite list (SYN-205)",
 2 |    requires => [ local_user_fixtures( 2, with_events => 1 ),
 3 |                 qw( can_create_private_room_with_invite )],
 4 | 
 5 |    do => sub {
 6 |       my ( $user, $invitee ) = @_;
 7 | 
 8 |       matrix_create_room( $user,
 9 |          invite => [ $invitee->user_id ],
10 |       )->SyTest::pass_on_done( "Created room" )
11 |       ->then( sub {
12 |          my ( $room_id ) = @_;
13 | 
14 |          await_sync($invitee, check => sub {
15 |             my ( $sync_body ) = @_;
16 |             log_if_fail $sync_body;
17 |             my $room =  $sync_body->{rooms}{invite}{$room_id};
18 |             return 0 unless $room;
19 | 
20 |             assert_json_keys( $room, qw( invite_state ) );
21 |             assert_json_keys( $room->{invite_state}, qw( events ) );
22 |             my $invite = first {
23 |                $_->{type} eq "m.room.member"
24 |                   and $_->{state_key} eq $invitee->user_id
25 |             } @{ $room->{invite_state}{events} };
26 | 
27 |             assert_json_keys( $invite, qw( sender content state_key type ));
28 |             $invite->{content}{membership} eq "invite"
29 |                or die "Expected an invite event";
30 |             $invite->{sender} eq $user->user_id
31 |                or die "Expected the invite to be from user A";
32 | 
33 |             return 1;
34 |          });
35 |       })->then_done(1);
36 |    };
37 | 


--------------------------------------------------------------------------------
/tests/90jira/SYN-328.pl:
--------------------------------------------------------------------------------
 1 | multi_test "Typing notifications don't leak",
 2 |    requires => [ local_user_fixtures( 3, with_events => 1 ),
 3 |                  qw( can_set_room_typing )],
 4 | 
 5 |    do => sub {
 6 |       my ( $creator, $member, $nonmember ) = @_;
 7 | 
 8 |       my $room_id;
 9 | 
10 |       matrix_create_and_join_room( [ $creator, $member ] )
11 |          ->SyTest::pass_on_done( "Created room" )
12 |       ->then( sub {
13 |          ( $room_id ) = @_;
14 | 
15 |          do_request_json_for( $creator,
16 |             method => "PUT",
17 |             uri    => "/v3/rooms/$room_id/typing/:user_id",
18 | 
19 |             content => { typing => JSON::true, timeout => 30000 * $TIMEOUT_FACTOR }, # msec
20 |          );
21 |       })->then( sub {
22 |          Future->needs_all( map {
23 |             my $recvuser = $_;
24 | 
25 |             await_sync_ephemeral_contains($recvuser, $room_id,
26 |                check => sub {
27 |                   my ( $event ) = @_;
28 |                   return unless $event->{type} eq "m.typing";
29 |                   return 1;
30 |                },
31 |             )
32 |          } $creator, $member )
33 |             ->SyTest::pass_on_done( "Members received notification" )
34 |       })->then( sub {
35 | 
36 |          # Wait on a different user to see if we get a typing notification
37 |          Future->wait_any(
38 |             delay( 2 ),
39 | 
40 |             await_sync_ephemeral_contains($nonmember, $room_id,
41 |                check => sub {
42 |                   my ( $event ) = @_;
43 |                   return unless $event->{type} eq "m.typing";
44 |                   return 1;
45 |                },
46 |             )->then_fail( "Received unexpected typing notification" ),
47 |          )->SyTest::pass_on_done( "Non-member did not receive it up to timeout" )
48 |       })->then_done(1);
49 |    };
50 | 


--------------------------------------------------------------------------------
/tests/90jira/SYN-343.pl:
--------------------------------------------------------------------------------
 1 | multi_test "Non-present room members cannot ban others",
 2 |    requires => [ local_user_fixtures( 2 ),
 3 |                  qw( can_ban_room can_change_power_levels )],
 4 | 
 5 |    do => sub {
 6 |       my ( $creator, $testuser ) = @_;
 7 | 
 8 |       my $room_id;
 9 | 
10 |       matrix_create_room_synced( $creator )
11 |          ->SyTest::pass_on_done( "Created room" )
12 |       ->then( sub {
13 |          ( $room_id ) = @_;
14 | 
15 |          matrix_change_room_power_levels( $creator, $room_id, sub {
16 |             my ( $levels ) = @_;
17 |             $levels->{users}{ $testuser->user_id } = 100;
18 |          })->SyTest::pass_on_done( "Set powerlevel" )
19 |       })->then( sub {
20 | 
21 |          do_request_json_for( $testuser,
22 |             method => "POST",
23 |             uri    => "/v3/rooms/$room_id/ban",
24 | 
25 |             content => { user_id => '@random_dude:test', reason => "testing" },
26 |          )->main::expect_http_403
27 |          ->SyTest::pass_on_done( "Attempt to ban is rejected" )
28 |       })->then_done(1);
29 |    };
30 | 


--------------------------------------------------------------------------------
/tests/90jira/SYN-390.pl:
--------------------------------------------------------------------------------
 1 | multi_test "Getting push rules doesn't corrupt the cache SYN-390",
 2 |    requires => [ local_user_fixture() ],
 3 | 
 4 |    do => sub {
 5 |       my ( $user ) = @_;
 6 | 
 7 |       do_request_json_for( $user,
 8 |          method  => "PUT",
 9 |          uri     => "/v3/pushrules/global/sender/%40a_user%3Amatrix.org",
10 |          content => { "actions" => ["dont_notify"] }
11 |       )->SyTest::pass_on_done("Set push rules for user" )
12 |       ->then( sub {
13 | 
14 |          do_request_json_for( $user,
15 |             method => "GET",
16 |             uri    => "/v3/pushrules/",
17 |          )->SyTest::pass_on_done("Got push rules the first time" )
18 |       })->then( sub {
19 | 
20 |          do_request_json_for( $user,
21 |             method => "GET",
22 |             uri    => "/v3/pushrules/",
23 |          )->SyTest::pass_on_done("Got push rules the second time" )
24 |       })->then_done(1);
25 |    }
26 | 


--------------------------------------------------------------------------------
/tests/90jira/SYN-516.pl:
--------------------------------------------------------------------------------
 1 | multi_test "Multiple calls to /sync should not cause 500 errors",
 2 |     requires => [ local_user_fixture( with_events => 0 ),
 3 |                   qw( can_sync can_send_message can_post_room_receipts )],
 4 |     check => sub {
 5 |         my ( $user ) = @_;
 6 | 
 7 |         my ( $filter_id, $room_id, $message_event_id );
 8 | 
 9 |         my $filter = {
10 |            room => {
11 |               timeline  => { types => ['m.room.message'] },
12 |               state     => { types => [] },
13 |               ephemeral => {},
14 |            },
15 |            presence => { types => [] },
16 |         };
17 | 
18 |         matrix_create_filter( $user, $filter )->then( sub {
19 |             ( $filter_id ) = @_;
20 | 
21 |             matrix_create_room_synced( $user )
22 |                 ->SyTest::pass_on_done( "User A created a room" );
23 |         })->then( sub {
24 |             ( $room_id ) = @_;
25 | 
26 |             matrix_typing( $user, $room_id, typing => JSON::true, timeout => 30000 )
27 |                 ->SyTest::pass_on_done( "Sent typing notification" );
28 |         })->then( sub {
29 |             matrix_send_room_message_synced( $user, $room_id,
30 |                                       content => { msgtype => "m.message",
31 |                                                    body => "message" })
32 |                 ->SyTest::pass_on_done( "Sent message" );
33 |         })->then( sub {
34 |             ( $message_event_id ) = @_;
35 | 
36 |             matrix_advance_room_receipt_synced( $user, $room_id,
37 |                 "m.read" => $message_event_id
38 |             )->SyTest::pass_on_done( "Updated read receipt" );
39 |         })->then( sub {
40 |             matrix_sync( $user, filter => $filter_id )
41 |                 ->SyTest::pass_on_done( "Completed first sync" );
42 |         })->then( sub {
43 |             my ( $body ) = @_;
44 | 
45 |             my $room = $body->{rooms}{join}{$room_id};
46 | 
47 |             @{ $room->{ephemeral}{events} } == 2
48 |                 or die "Expected two ephemeral events";
49 | 
50 |             @{ $room->{timeline}{events} } == 1
51 |                 or die "Expected one timeline event";
52 | 
53 |             matrix_sync( $user, filter => $filter_id )
54 |                 ->SyTest::pass_on_done( "Completed second sync" );
55 |         })->then( sub {
56 |             my ( $body ) = @_;
57 | 
58 |             my $room = $body->{rooms}{join}{$room_id};
59 | 
60 |             @{ $room->{ephemeral}{events} } == 2
61 |                 or die "Expected two ephemeral events";
62 | 
63 |             Future->done(1);
64 |         });
65 |    };
66 | 


--------------------------------------------------------------------------------
/tests/90jira/SYN-606.pl:
--------------------------------------------------------------------------------
 1 | foreach my $i (
 2 |    [ "Guest", sub { guest_user_fixture() } ],
 3 |    [ "Real", sub { local_user_fixture() } ]
 4 | ) {
 5 |    my ( $name, $fixture ) = @$i;
 6 | 
 7 |    test(
 8 |       "$name user can call /events on another world_readable room (SYN-606)",
 9 |       deprecated_endpoints => 1,
10 |       requires => [ $fixture->(),  local_user_fixture() ],
11 | 
12 |       do => sub {
13 |          my ( $nonjoined_user, $user ) = @_;
14 | 
15 |          my ( $room_id1, $room_id2 );
16 | 
17 |          Future->needs_all(
18 |             matrix_create_and_join_room( [ $user ] ),
19 |             matrix_create_and_join_room( [ $user ] ),
20 |          )->then( sub {
21 |             ( $room_id1, $room_id2 ) = @_;
22 | 
23 |             Future->needs_all(
24 |                matrix_set_room_history_visibility( $user, $room_id1, "world_readable" ),
25 |                matrix_set_room_history_visibility( $user, $room_id2, "world_readable" ),
26 |             )
27 |          })->then( sub {
28 |             matrix_initialsync_room( $nonjoined_user, $room_id1 )
29 |          })->then( sub {
30 |             my ( $body ) = @_;
31 | 
32 |             # We need to manually handle the from tokens here as the await_event*
33 |             # methods may otherwise reuse results from an /events call that did
34 |             # not include the specified room (due to the user not being joined to
35 |             # it). This could cause the event to not be found.
36 |             # I.e. there is a race where we send a message, the background /events
37 |             # stream streams past the message, and then /events stream triggered by
38 |             # await_event_* (which *does* include the room_id) starts streaming
39 |             # from *after* the message. Hence the event is neither in the cache
40 |             # nor in the live event stream.
41 |             my $from_token = $body->{messages}{end};
42 | 
43 |             Future->needs_all(
44 |                matrix_send_room_text_message_synced( $user, $room_id1, body => "moose" ),
45 |                await_event_not_history_visibility_or_presence_for( $nonjoined_user, $room_id1, [],
46 |                   from => $from_token,
47 |                ),
48 |             );
49 |          })->then( sub {
50 |             matrix_initialsync_room( $nonjoined_user, $room_id2 )
51 |          })->then( sub {
52 |             my ( $body ) = @_;
53 | 
54 |             my $from_token = $body->{messages}{end};
55 | 
56 |             Future->needs_all(
57 |                matrix_send_room_text_message_synced( $user, $room_id2, body => "mice" ),
58 |                await_event_not_history_visibility_or_presence_for( $nonjoined_user, $room_id2, [],
59 |                   from => $from_token,
60 |                )->then( sub {
61 |                   my ( $event ) = @_;
62 | 
63 |                   assert_json_keys( $event, qw( content ) );
64 |                   my $content = $event->{content};
65 |                   assert_json_keys( $content, qw( body ) );
66 |                   assert_eq( $content->{body}, "mice" );
67 | 
68 |                   Future->done( 1 );
69 |                }),
70 |             );
71 |          });
72 |       },
73 |    );
74 | }
75 | 


--------------------------------------------------------------------------------
/tests/90jira/SYN-627.pl:
--------------------------------------------------------------------------------
 1 | test "Events come down the correct room",
 2 |    requires => [ local_user_fixture( with_events => 0 ), "can_sync" ],
 3 | 
 4 |    # creating all those rooms is quite slow.
 5 |    timeout => 100,
 6 | 
 7 |    check => sub {
 8 |       my ( $user ) = @_;
 9 |       my @rooms;
10 | 
11 |       Future->needs_all( map {
12 |          matrix_create_room( $user )
13 |          ->on_done( sub {
14 |             my ( $room_id ) = @_;
15 | 
16 |             push @rooms, $room_id;
17 |          });
18 |       } 1 .. 30 )
19 |       ->then( sub {
20 |          # To ensure that all the creation events have gone through
21 |          matrix_send_room_text_message_synced( $user, $rooms[-1], body => $rooms[-1] );
22 |       })->then( sub {
23 |          matrix_sync( $user );
24 |       })->then( sub {
25 |          Future->needs_all( map {
26 |             my $room_id = $_;
27 | 
28 |             matrix_send_room_text_message( $user, $room_id, body => "$room_id" );
29 |          } @rooms );
30 |       })->then( sub {
31 |          # Wait until we see all rooms come down the sync stream.
32 |          retry_until_success {
33 |             matrix_sync(
34 |                $user, since => $user->sync_next_batch, update_next_batch => 0
35 |             )->then( sub {
36 |                my ( $body ) = @_;
37 | 
38 |                foreach my $room_id ( @rooms ) {
39 |                   my $room = $body->{rooms}{join}{$room_id};
40 | 
41 |                   assert_json_keys( $room, qw( timeline ));
42 |                   @{ $room->{timeline}{events} } == 1 or die "Expected exactly one event";
43 |                }
44 |             })
45 |          }
46 |       })->then( sub {
47 |          matrix_sync_again( $user );
48 |       })->then( sub {
49 |          my ( $body ) = @_;
50 |          my $room_id;
51 | 
52 |          log_if_fail "sync body", $body;
53 | 
54 |          foreach $room_id ( @rooms ) {
55 |             my $room = $body->{rooms}{join}{$room_id};
56 | 
57 |             assert_json_keys( $room, qw( timeline ));
58 |             @{ $room->{timeline}{events} } == 1 or die "Expected exactly one event";
59 | 
60 |             my $event = $room->{timeline}{events}[0];
61 | 
62 |             assert_eq( $event->{content}{body}, $room_id, "Event in the wrong room" );
63 |          }
64 | 
65 |          Future->done(1);
66 |       });
67 |    };
68 | 


--------------------------------------------------------------------------------