From dcbdcc3fd15041b9d2a737375cf4295272d54993 Mon Sep 17 00:00:00 2001 From: evdokimovs <49490279+evdokimovs@users.noreply.github.com> Date: Tue, 2 Mar 2021 18:10:55 +0300 Subject: [PATCH] Add Medea E2E tests runner (#178) - rename old E2E tests as integration tests - implement Cucumber tests runner for new E2E tests - make 'medea-control-api-mock' protocol structures public - impl E2E test scenarios for Member joining - upd CI pipeline to run new E2E tests in Chrome and Firefox --- .clippy.toml | 2 +- .editorconfig | 2 +- .env | 7 + .github/workflows/ci.yml | 66 +- .rustfmt.toml | 2 +- CONTRIBUTING.md | 5 + Cargo.lock | 1063 +++++++++++++++-- Cargo.toml | 25 +- Makefile | 131 +- docker-compose.coturn.yml | 3 +- docker-compose.medea.yml | 2 - mock/control-api/src/api/endpoint.rs | 18 +- mock/control-api/src/api/member.rs | 38 +- mock/control-api/src/api/mod.rs | 8 +- mock/control-api/src/api/room.rs | 8 +- mock/control-api/src/api/ws.rs | 3 + mock/control-api/src/lib.rs | 40 + mock/control-api/src/main.rs | 35 +- tests/e2e/browser/client.rs | 251 ++++ tests/e2e/browser/js.rs | 147 +++ tests/e2e/browser/mod.rs | 142 +++ tests/e2e/conf.rs | 69 ++ tests/e2e/control.rs | 62 + tests/e2e/docker-compose.host.yml | 40 + tests/e2e/docker-compose.yml | 58 + .../features/on_new_connection_fires.feature | 22 + tests/e2e/index.html | 18 + tests/e2e/main.rs | 99 +- tests/e2e/nginx.conf | 27 + tests/e2e/object/connection.rs | 2 + tests/e2e/object/connections_store.rs | 63 + tests/e2e/object/jason.rs | 39 + tests/e2e/object/mod.rs | 184 +++ tests/e2e/object/room.rs | 113 ++ tests/e2e/steps/connection.rs | 33 + tests/e2e/steps/media_state.rs | 31 + tests/e2e/steps/mod.rs | 198 +++ tests/e2e/world/member.rs | 144 +++ tests/e2e/world/mod.rs | 236 ++++ .../{e2e => integration}/callbacks/member.rs | 0 tests/{e2e => integration}/callbacks/mod.rs | 0 .../grpc_control_api/create.rs | 0 .../grpc_control_api/credentials.rs | 0 .../grpc_control_api/delete.rs | 0 .../grpc_control_api/mod.rs | 0 .../grpc_control_api/rpc_settings.rs | 0 .../grpc_control_api/signaling.rs | 0 tests/integration/main.rs | 90 ++ .../add_endpoints_synchronization.rs | 0 .../signalling/command_validation.rs | 0 .../signalling/ice_restart.rs | 0 tests/{e2e => integration}/signalling/mod.rs | 0 .../signalling/pub_sub_signallng.rs | 0 .../signalling/rpc_settings.rs | 0 .../signalling/three_pubs.rs | 0 .../signalling/track_disable.rs | 0 .../signalling/track_mute.rs | 0 57 files changed, 3235 insertions(+), 291 deletions(-) create mode 100644 mock/control-api/src/lib.rs create mode 100644 tests/e2e/browser/client.rs create mode 100644 tests/e2e/browser/js.rs create mode 100644 tests/e2e/browser/mod.rs create mode 100644 tests/e2e/conf.rs create mode 100644 tests/e2e/control.rs create mode 100644 tests/e2e/docker-compose.host.yml create mode 100644 tests/e2e/docker-compose.yml create mode 100644 tests/e2e/features/on_new_connection_fires.feature create mode 100644 tests/e2e/index.html create mode 100644 tests/e2e/nginx.conf create mode 100644 tests/e2e/object/connection.rs create mode 100644 tests/e2e/object/connections_store.rs create mode 100644 tests/e2e/object/jason.rs create mode 100644 tests/e2e/object/mod.rs create mode 100644 tests/e2e/object/room.rs create mode 100644 tests/e2e/steps/connection.rs create mode 100644 tests/e2e/steps/media_state.rs create mode 100644 tests/e2e/steps/mod.rs create mode 100644 tests/e2e/world/member.rs create mode 100644 tests/e2e/world/mod.rs rename tests/{e2e => integration}/callbacks/member.rs (100%) rename tests/{e2e => integration}/callbacks/mod.rs (100%) rename tests/{e2e => integration}/grpc_control_api/create.rs (100%) rename tests/{e2e => integration}/grpc_control_api/credentials.rs (100%) rename tests/{e2e => integration}/grpc_control_api/delete.rs (100%) rename tests/{e2e => integration}/grpc_control_api/mod.rs (100%) rename tests/{e2e => integration}/grpc_control_api/rpc_settings.rs (100%) rename tests/{e2e => integration}/grpc_control_api/signaling.rs (100%) create mode 100644 tests/integration/main.rs rename tests/{e2e => integration}/signalling/add_endpoints_synchronization.rs (100%) rename tests/{e2e => integration}/signalling/command_validation.rs (100%) rename tests/{e2e => integration}/signalling/ice_restart.rs (100%) rename tests/{e2e => integration}/signalling/mod.rs (100%) rename tests/{e2e => integration}/signalling/pub_sub_signallng.rs (100%) rename tests/{e2e => integration}/signalling/rpc_settings.rs (100%) rename tests/{e2e => integration}/signalling/three_pubs.rs (100%) rename tests/{e2e => integration}/signalling/track_disable.rs (100%) rename tests/{e2e => integration}/signalling/track_mute.rs (100%) diff --git a/.clippy.toml b/.clippy.toml index 5892ca001..c697adfa4 100644 --- a/.clippy.toml +++ b/.clippy.toml @@ -30,5 +30,5 @@ doc-valid-idents = [ "RTCStats", "RTCStatsReport", "RTCTrackEvent", "RTCVideoSenderStats", - "WebRTC", "WebSocket", + "WebDriver", "WebRTC", "WebSocket", ] diff --git a/.editorconfig b/.editorconfig index 6766d9eb7..dddf0e288 100644 --- a/.editorconfig +++ b/.editorconfig @@ -49,6 +49,6 @@ indent_size = 4 indent_style = space indent_size = 2 -[**/nginx{/*,.*}.conf] +[**/ngin{x,x/*,x.*}.conf] indent_style = space indent_size = 2 diff --git a/.env b/.env index df745d510..9cea10ab8 100644 --- a/.env +++ b/.env @@ -4,5 +4,12 @@ MEDEA_CONF=_dev/config.toml MEDEA_CONTROL__STATIC_SPECS_DIR=_dev/specs/ COMPOSE_PROJECT_NAME=medea + COMPOSE_IMAGE_NAME=instrumentisto/medea COMPOSE_IMAGE_VER=dev + +COMPOSE_CONTROL_MOCK_IMAGE_NAME=instrumentisto/medea-control-api-mock +COMPOSE_CONTROL_MOCK_IMAGE_VER=dev + +COMPOSE_WEBDRIVER_IMAGE_NAME=selenoid/chrome +COMPOSE_WEBDRIVER_IMAGE_VER=latest diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb6b6eb32..e7af178c0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -147,6 +147,7 @@ jobs: test-e2e: name: E2E tests + needs: ['docker'] if: ${{ github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/medea-') || !contains(github.event.head_commit.message, '[skip ci]') }} @@ -157,6 +158,8 @@ jobs: with: profile: minimal toolchain: stable + target: wasm32-unknown-unknown + - uses: Swatinem/rust-cache@v1 if: ${{ !contains(github.event.head_commit.message, '[fresh ci]') }} - uses: satackey/action-docker-layer-caching@v0.0.11 @@ -165,7 +168,56 @@ jobs: restore-keys: test-e2e- continue-on-error: true if: ${{ !contains(github.event.head_commit.message, '[fresh ci]') }} - - run: make test.e2e up=yes dockerized=no + + - uses: actions-rs/install@v0.1 + with: + crate: wasm-pack + use-tool-cache: true + + - uses: actions/download-artifact@v2 + with: + name: docker-medea-${{ github.run_number }} + - name: Unpack `medea` Docker image + run: make docker.untar from-file=image.tar + + - uses: actions/download-artifact@v2 + with: + name: docker-medea-control-api-mock-${{ github.run_number }} + - name: Unpack `medea-control-api-mock` Docker image + run: make docker.untar from-file=image.tar + + - uses: c-py/action-dotenv-to-setenv@v2 + + - name: Chrome + run: make test.e2e browser=chrome up=yes dockerized=yes + tag=build-${{ github.run_number }} + - name: Firefox + run: make test.e2e browser=firefox up=yes dockerized=yes + tag=build-${{ github.run_number }} + + test-integration: + name: Integration tests + if: ${{ github.ref == 'refs/heads/master' + || startsWith(github.ref, 'refs/tags/medea-') + || !contains(github.event.head_commit.message, '[skip ci]') }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + + - uses: Swatinem/rust-cache@v1 + if: ${{ !contains(github.event.head_commit.message, '[fresh ci]') }} + - uses: satackey/action-docker-layer-caching@v0.0.11 + with: + key: test-integration-{hash} + restore-keys: test-integration- + continue-on-error: true + if: ${{ !contains(github.event.head_commit.message, '[fresh ci]') }} + + - run: make test.integration up=yes dockerized=no @@ -273,18 +325,15 @@ jobs: cache: ${{ github.ref != 'refs/heads/master' && !startsWith(github.ref, 'refs/tags/medea-') && !contains(github.event.head_commit.message, '[fresh ci]') }} - export: ${{ github.ref == 'refs/heads/master' - || startsWith(github.ref, 'refs/tags/medea-') }} + export: true if: true - image: medea-control-api-mock cache: ${{ github.ref != 'refs/heads/master' && !startsWith(github.ref, 'refs/tags/medea-control-api-mock-') && !contains(github.event.head_commit.message, '[fresh ci]') }} - export: ${{ github.ref == 'refs/heads/master' - || startsWith(github.ref, 'refs/tags/medea-control-api-mock-') }} - if: ${{ !startsWith(github.ref, 'refs/tags/medea-') - || startsWith(github.ref, 'refs/tags/medea-control-api-mock-') }} + export: true + if: true - image: medea-demo cache: ${{ github.ref != 'refs/heads/master' @@ -314,6 +363,7 @@ jobs: key: docker-${{ matrix.image }}-build-{hash} restore-keys: docker-${{ matrix.image }}-build- continue-on-error: true + timeout-minutes: 10 if: ${{ matrix.if && matrix.cache }} - run: make docker.build debug=yes no-cache=no image=${{ matrix.image }} tag=build-${{ github.run_number }} @@ -338,7 +388,7 @@ jobs: release-github: name: Release on GitHub - needs: ['clippy', 'rustfmt', 'rustdoc', 'test-e2e', 'test-unit'] + needs: ['clippy', 'rustfmt', 'rustdoc', 'test-e2e', 'test-integration', 'test-unit'] if: ${{ startsWith(github.ref, 'refs/tags/medea-') }} runs-on: ubuntu-latest steps: diff --git a/.rustfmt.toml b/.rustfmt.toml index 5a7b31de5..0f9c48ca3 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -2,7 +2,7 @@ # https://github.com/rust-lang-nursery/rustfmt/blob/master/Configurations.md max_width = 80 -format_strings = true +format_strings = false imports_granularity = "Crate" normalize_comments = true wrap_comments = true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d256148b2..e7322dcc1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -93,6 +93,11 @@ $ make test.unit crate=medea $ make test.unit crate=medea-jason ``` +To run integration tests use docker-wrapped commands from [`Makefile`]: +```bash +$ make test.integration +``` + To run E2E tests use docker-wrapped commands from [`Makefile`]: ```bash $ make test.e2e diff --git a/Cargo.lock b/Cargo.lock index 1532d0170..edd29282e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,8 +29,8 @@ dependencies = [ "parking_lot", "pin-project 0.4.27", "smallvec", - "tokio", - "tokio-util", + "tokio 0.2.25", + "tokio-util 0.3.1", "trust-dns-proto", "trust-dns-resolver", ] @@ -47,8 +47,8 @@ dependencies = [ "futures-sink", "log", "pin-project 0.4.27", - "tokio", - "tokio-util", + "tokio 0.2.25", + "tokio-util 0.3.1", ] [[package]] @@ -100,7 +100,7 @@ dependencies = [ "bitflags", "brotli2", "bytes 0.5.6", - "cookie", + "cookie 0.14.3", "copyless", "derive_more", "either", @@ -110,7 +110,7 @@ dependencies = [ "futures-core", "futures-util", "fxhash", - "h2", + "h2 0.2.7", "http", "httparse", "indexmap", @@ -125,8 +125,8 @@ dependencies = [ "regex", "serde 1.0.123", "serde_json", - "serde_urlencoded", - "sha-1", + "serde_urlencoded 0.7.0", + "sha-1 0.9.4", "slab", "time 0.2.25", ] @@ -166,7 +166,7 @@ dependencies = [ "futures-channel", "futures-util", "smallvec", - "tokio", + "tokio 0.2.25", ] [[package]] @@ -182,7 +182,7 @@ dependencies = [ "futures-channel", "futures-util", "log", - "mio", + "mio 0.6.23", "mio-uds", "num_cpus", "slab", @@ -292,7 +292,7 @@ dependencies = [ "regex", "serde 1.0.123", "serde_json", - "serde_urlencoded", + "serde_urlencoded 0.7.0", "socket2", "time 0.2.25", "tinyvec", @@ -517,7 +517,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22068c0c19514942eefcfd4daf8976ef1aad84e61539f95cd200c35202f80af5" dependencies = [ - "async-stream-impl", + "async-stream-impl 0.2.1", + "futures-core", +] + +[[package]] +name = "async-stream" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3670df70cbc01729f901f94c887814b3c68db038aad1329a418bae178bc5295c" +dependencies = [ + "async-stream-impl 0.3.0", "futures-core", ] @@ -532,6 +542,17 @@ dependencies = [ "syn 1.0.60", ] +[[package]] +name = "async-stream-impl" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3548b8efc9f8e8a5a0a2808c5bd8451a9031b9e5b879a79590304ae928b0a70" +dependencies = [ + "proc-macro2 1.0.24", + "quote 1.0.9", + "syn 1.0.60", +] + [[package]] name = "async-task" version = "4.0.3" @@ -593,7 +614,7 @@ dependencies = [ "rand 0.7.3", "serde 1.0.123", "serde_json", - "serde_urlencoded", + "serde_urlencoded 0.7.0", ] [[package]] @@ -645,13 +666,34 @@ dependencies = [ "constant_time_eq", ] +[[package]] +name = "block-buffer" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" +dependencies = [ + "block-padding", + "byte-tools", + "byteorder", + "generic-array 0.12.3", +] + [[package]] name = "block-buffer" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "generic-array", + "generic-array 0.14.4", +] + +[[package]] +name = "block-padding" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" +dependencies = [ + "byte-tools", ] [[package]] @@ -688,12 +730,27 @@ dependencies = [ "libc", ] +[[package]] +name = "bstr" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a40b47ad93e1a5404e6c18dec46b628214fee441c70f4ab5d6942142cc268a3d" +dependencies = [ + "memchr", +] + [[package]] name = "bumpalo" version = "3.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe" +[[package]] +name = "byte-tools" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" + [[package]] name = "byteorder" version = "1.4.2" @@ -768,7 +825,7 @@ dependencies = [ "atty", "bitflags", "strsim 0.8.0", - "textwrap", + "textwrap 0.11.0", "unicode-width", "vec_map", ] @@ -784,7 +841,7 @@ dependencies = [ "futures-util", "memchr", "pin-project-lite 0.2.4", - "tokio", + "tokio 0.2.25", ] [[package]] @@ -834,6 +891,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "cookie" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "888604f00b3db336d2af898ec3c1d5d0ddf5e6d462220f2ededc33a87ac4bbd5" +dependencies = [ + "time 0.1.43", +] + [[package]] name = "cookie" version = "0.14.3" @@ -851,6 +917,22 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2df960f5d869b2dd8532793fde43eb5427cceb126c929747a26823ab0eeb536" +[[package]] +name = "core-foundation" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" + [[package]] name = "cpuid-bool" version = "0.1.2" @@ -930,6 +1012,54 @@ dependencies = [ "syn 1.0.60", ] +[[package]] +name = "cucumber_rust" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cc67008716666beb00d20d63debed61b55c78f1ccdf4efb629ddae3205d0f52" +dependencies = [ + "async-stream 0.3.0", + "async-trait", + "clap", + "cucumber_rust_codegen", + "cute_custom_default", + "futures 0.3.13", + "futures-timer", + "gherkin_rust", + "globwalk", + "inventory", + "pathdiff", + "regex", + "shh", + "termcolor", + "textwrap 0.12.1", + "thiserror", +] + +[[package]] +name = "cucumber_rust_codegen" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebf7a94fedf6f251d5cb208ea536496a191c552509a052976af59792199cbc8" +dependencies = [ + "inflections", + "itertools 0.9.0", + "proc-macro2 1.0.24", + "quote 1.0.9", + "regex", + "syn 1.0.60", +] + +[[package]] +name = "cute_custom_default" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed431abf442833fd62ad7cc527a3833d969155c803f0dcf7f8a68db8adddc4c5" +dependencies = [ + "quote 1.0.9", + "syn 1.0.60", +] + [[package]] name = "darling" version = "0.10.2" @@ -942,12 +1072,12 @@ dependencies = [ [[package]] name = "darling" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11947000d710ff98138229f633039982f0fef2d9a3f546c21d610fee5f8631d5" +checksum = "9bfca34df8d0d73d61f26ce03a8be6da606d6c419a10c099009a879c165d2a71" dependencies = [ - "darling_core 0.12.0", - "darling_macro 0.12.0", + "darling_core 0.12.1", + "darling_macro 0.12.1", ] [[package]] @@ -966,9 +1096,9 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae53b4d9cc89c40314ccf2bf9e6ff1eb19c31e3434542445a41893dbf041aec2" +checksum = "893119a69874649edb8ee1b912a7f367f251c5c862c45d3eed7abad2535e766b" dependencies = [ "fnv", "ident_case", @@ -991,11 +1121,11 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9cd9ac4d50d023af5e710cae1501afb063efcd917bd3fc026e8ed6493cc9755" +checksum = "86184131efd474ebc1264537cfc8bbe9710f73846d02cc0305370410b848ef0f" dependencies = [ - "darling_core 0.12.0", + "darling_core 0.12.1", "quote 1.0.9", "syn 1.0.60", ] @@ -1011,7 +1141,7 @@ dependencies = [ "crossbeam-queue", "num_cpus", "serde 1.0.123", - "tokio", + "tokio 0.2.25", ] [[package]] @@ -1023,7 +1153,7 @@ dependencies = [ "async-trait", "config", "deadpool", - "futures 0.3.12", + "futures 0.3.13", "log", "redis", "serde 1.0.123", @@ -1082,13 +1212,22 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" +[[package]] +name = "digest" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +dependencies = [ + "generic-array 0.12.3", +] + [[package]] name = "digest" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array", + "generic-array 0.14.4", ] [[package]] @@ -1191,6 +1330,33 @@ dependencies = [ "synstructure", ] +[[package]] +name = "fake-simd" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" + +[[package]] +name = "fantoccini" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b29a87bffc0ae78d88bedd8b1a9cb885ce84382ed3457429f35d480269ae17c2" +dependencies = [ + "base64 0.13.0", + "cookie 0.14.3", + "futures-core", + "futures-util", + "http", + "hyper 0.14.4", + "hyper-tls", + "mime", + "serde 1.0.123", + "serde_json", + "tokio 1.2.0", + "url", + "webdriver", +] + [[package]] name = "fastrand" version = "1.4.0" @@ -1233,6 +1399,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.0.1" @@ -1299,9 +1480,9 @@ checksum = "4c7e4c2612746b0df8fed4ce0c69156021b704c9aefa360311c04e6e9e002eed" [[package]] name = "futures" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9052a1a50244d8d5aa9bf55cbc2fb6f357c86cc52e46c62ed390a7180cf150" +checksum = "7f55667319111d593ba876406af7c409c0ebb44dc4be6132a783ccf163ea14c1" dependencies = [ "futures-channel", "futures-core", @@ -1314,9 +1495,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2d31b7ec7efab6eefc7c57233bb10b847986139d88cc2f5a02a1ae6871a1846" +checksum = "8c2dd2df839b57db9ab69c2c9d8f3e8c81984781937fe2807dc6dcf3b2ad2939" dependencies = [ "futures-core", "futures-sink", @@ -1324,15 +1505,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79e5145dde8da7d1b3892dad07a9c98fc04bc39892b1ecc9692cf53e2b780a65" +checksum = "15496a72fabf0e62bdc3df11a59a3787429221dd0710ba8ef163d6f7a9112c94" [[package]] name = "futures-executor" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9e59fdc009a4b3096bf94f740a0f2424c082521f20a9b08c5c07c48d90fd9b9" +checksum = "891a4b7b96d84d5940084b2a37632dd65deeae662c114ceaa2c879629c9c0ad1" dependencies = [ "futures-core", "futures-task", @@ -1341,9 +1522,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28be053525281ad8259d47e4de5de657b25e7bac113458555bb4b70bc6870500" +checksum = "d71c2c65c57704c32f5241c1223167c2c3294fd34ac020c807ddbe6db287ba59" [[package]] name = "futures-lite" @@ -1362,9 +1543,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c287d25add322d9f9abdcdc5927ca398917996600182178774032e9f8258fedd" +checksum = "ea405816a5139fb39af82c2beb921d52143f556038378d6db21183a5c37fbfb7" dependencies = [ "proc-macro-hack", "proc-macro2 1.0.24", @@ -1374,24 +1555,27 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf5c69029bda2e743fddd0582d1083951d65cc9539aebf8812f36c3491342d6" +checksum = "85754d98985841b7d4f5e8e6fbfa4a4ac847916893ec511a2917ccd8525b8bb3" [[package]] name = "futures-task" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13de07eb8ea81ae445aca7b69f5f7bf15d7bf4912d8ca37d6645c77ae8a58d86" -dependencies = [ - "once_cell", -] +checksum = "fa189ef211c15ee602667a6fcfe1c1fd9e07d42250d2156382820fba33c9df80" + +[[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" [[package]] name = "futures-util" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "632a8cd0f2a4b3fdea1657f08bde063848c3bd00f9bbf6e256b8be78802e624b" +checksum = "1812c7ab8aedf8d6f2701a43e1243acdbcc2b36ab26e2ad421eb99ac963d96d1" dependencies = [ "futures 0.1.30", "futures-channel", @@ -1436,6 +1620,15 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "generic-array" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec" +dependencies = [ + "typenum", +] + [[package]] name = "generic-array" version = "0.14.4" @@ -1468,12 +1661,64 @@ dependencies = [ "wasi 0.10.2+wasi-snapshot-preview1", ] +[[package]] +name = "gherkin_rust" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f70e36c2b9e5ec714060599e3a983dc7a9123f32c5c63ee638c1e024f36d5c1" +dependencies = [ + "heck", + "peg", + "quote 1.0.9", + "serde 1.0.123", + "serde_json", + "syn 1.0.60", + "textwrap 0.12.1", + "thiserror", + "typed-builder", +] + +[[package]] +name = "ghost" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5bcf1bbeab73aa4cf2fde60a846858dc036163c7c33bec309f8d17de785479" +dependencies = [ + "proc-macro2 1.0.24", + "quote 1.0.9", + "syn 1.0.60", +] + [[package]] name = "gimli" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce" +[[package]] +name = "globset" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c152169ef1e421390738366d2f796655fec62621dabbd0fd476f905934061e4a" +dependencies = [ + "aho-corasick", + "bstr", + "fnv", + "log", + "regex", +] + +[[package]] +name = "globwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +dependencies = [ + "bitflags", + "ignore", + "walkdir", +] + [[package]] name = "gloo-timers" version = "0.2.1" @@ -1501,8 +1746,28 @@ dependencies = [ "http", "indexmap", "slab", - "tokio", - "tokio-util", + "tokio 0.2.25", + "tokio-util 0.3.1", + "tracing", + "tracing-futures", +] + +[[package]] +name = "h2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b67e66362108efccd8ac053abafc8b7a8d86a37e6e48fc4f6f7485eb5e9e6a5" +dependencies = [ + "bytes 1.0.1", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio 1.2.0", + "tokio-util 0.6.3", "tracing", "tracing-futures", ] @@ -1513,6 +1778,31 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" +[[package]] +name = "headers" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62689dc57c7456e69712607ffcbd0aa1dfcccf9af73727e9b25bc1825375cac3" +dependencies = [ + "base64 0.13.0", + "bitflags", + "bytes 1.0.1", + "headers-core", + "http", + "mime", + "sha-1 0.8.2", + "time 0.1.43", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http", +] + [[package]] name = "heck" version = "0.3.2" @@ -1563,6 +1853,16 @@ dependencies = [ "http", ] +[[package]] +name = "http-body" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2861bd27ee074e5ee891e8b539837a9430012e249d7f0ca2d795650f579c1994" +dependencies = [ + "bytes 1.0.1", + "http", +] + [[package]] name = "httparse" version = "1.3.5" @@ -1601,20 +1901,57 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", + "h2 0.2.7", "http", - "http-body", + "http-body 0.3.1", "httparse", "httpdate", "itoa", "pin-project 1.0.5", "socket2", - "tokio", + "tokio 0.2.25", "tower-service", "tracing", "want", ] +[[package]] +name = "hyper" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8e946c2b1349055e0b72ae281b238baf1a3ea7307c7e9f9d64673bdd9c26ac7" +dependencies = [ + "bytes 1.0.1", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.0", + "http", + "http-body 0.4.0", + "httparse", + "httpdate", + "itoa", + "pin-project 1.0.5", + "socket2", + "tokio 1.2.0", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes 1.0.1", + "hyper 0.14.4", + "native-tls", + "tokio 1.2.0", + "tokio-native-tls", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -1632,6 +1969,24 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "ignore" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b287fb45c60bb826a0dc68ff08742b9d88a2fea13d6e0c286b3172065aaf878c" +dependencies = [ + "crossbeam-utils 0.8.2", + "globset", + "lazy_static", + "log", + "memchr", + "regex", + "same-file", + "thread_local", + "walkdir", + "winapi-util", +] + [[package]] name = "indexmap" version = "1.6.1" @@ -1642,6 +1997,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inflections" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a257582fdcde896fd96463bf2d40eefea0580021c0712a0e2b028b60b47a837a" + [[package]] name = "instant" version = "0.1.9" @@ -1651,6 +2012,28 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "inventory" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f0f7efb804ec95e33db9ad49e4252f049e37e8b0a4652e3cd61f7999f2eff7f" +dependencies = [ + "ctor", + "ghost", + "inventory-impl", +] + +[[package]] +name = "inventory-impl" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75c094e94816723ab936484666968f5b58060492e880f3c8d00489a1e244fa51" +dependencies = [ + "proc-macro2 1.0.24", + "quote 1.0.9", + "syn 1.0.60", +] + [[package]] name = "iovec" version = "0.1.4" @@ -1669,9 +2052,15 @@ dependencies = [ "socket2", "widestring", "winapi 0.3.9", - "winreg", + "winreg 0.6.2", ] +[[package]] +name = "ipnet" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47be2f14c678be2fdcab04ab1171db51b2762ce6f0a8ee87c8dd4a04ed216135" + [[package]] name = "itertools" version = "0.8.2" @@ -1681,6 +2070,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "0.4.7" @@ -1829,28 +2227,35 @@ dependencies = [ "actix-rt", "actix-web", "actix-web-actors", + "async-recursion", "async-trait", "awc", "bytes 0.5.6", "chrono", "config", + "cucumber_rust", "deadpool", "deadpool-redis", "derive_builder", "derive_more", "dotenv", "failure", + "fantoccini", "function_name", - "futures 0.3.12", + "futures 0.3.13", "humantime-serde", + "hyper 0.14.4", "lazy_static", "medea-client-api-proto 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "medea-control-api-mock", "medea-control-api-proto 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "medea-coturn-telnet-client 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "medea-macro 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "medea-macro 0.2.0", "mockall", + "once_cell", "rand 0.8.3", "redis", + "reqwest", "rust-argon2", "rust-crypto", "serde 1.0.123", @@ -1866,10 +2271,14 @@ dependencies = [ "smart-default", "subtle", "tempfile", - "tokio", + "tokio 0.2.25", + "tokio 1.2.0", + "tokio-util 0.6.3", "toml", "tonic", "url", + "uuid", + "webdriver", ] [[package]] @@ -1954,11 +2363,11 @@ dependencies = [ "bytes 0.5.6", "deadpool", "derive_more", - "futures 0.3.12", + "futures 0.3.13", "once_cell", "regex", - "tokio", - "tokio-util", + "tokio 0.2.25", + "tokio-util 0.3.1", ] [[package]] @@ -1971,11 +2380,11 @@ dependencies = [ "bytes 0.5.6", "deadpool", "derive_more", - "futures 0.3.12", + "futures 0.3.13", "once_cell", "regex", - "tokio", - "tokio-util", + "tokio 0.2.25", + "tokio-util 0.3.1", ] [[package]] @@ -1990,7 +2399,7 @@ dependencies = [ "derive_more", "downcast", "fragile", - "futures 0.3.12", + "futures 0.3.13", "js-sys", "log", "medea-client-api-proto 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2040,8 +2449,8 @@ dependencies = [ name = "medea-reactive" version = "0.1.0" dependencies = [ - "futures 0.3.12", - "tokio", + "futures 0.3.13", + "tokio 0.2.25", ] [[package]] @@ -2050,7 +2459,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8493c11328ceb1a0bb9b5c74af84ae4abb5386838605362dfb4c17f97fbc484" dependencies = [ - "futures 0.3.12", + "futures 0.3.13", ] [[package]] @@ -2071,6 +2480,16 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "mime_guess" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "miniz_oxide" version = "0.4.3" @@ -2094,12 +2513,25 @@ dependencies = [ "kernel32-sys", "libc", "log", - "miow", + "miow 0.2.2", "net2", "slab", "winapi 0.2.8", ] +[[package]] +name = "mio" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5dede4e2065b3842b8b0af444119f3aa331cc7cc2dd20388bfb0f5d5a38823a" +dependencies = [ + "libc", + "log", + "miow 0.3.6", + "ntapi", + "winapi 0.3.9", +] + [[package]] name = "mio-uds" version = "0.6.8" @@ -2108,7 +2540,7 @@ checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0" dependencies = [ "iovec", "libc", - "mio", + "mio 0.6.23", ] [[package]] @@ -2123,6 +2555,16 @@ dependencies = [ "ws2_32-sys", ] +[[package]] +name = "miow" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a33c1b55807fbed163481b5ba66db4b2fa6cde694a5027be10fb724206c5897" +dependencies = [ + "socket2", + "winapi 0.3.9", +] + [[package]] name = "mockall" version = "0.9.1" @@ -2156,6 +2598,24 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1255076139a83bb467426e7f8d0134968a8118844faa755985e077cf31850333" +[[package]] +name = "native-tls" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8d96b2e1c8da3957d58100b09f102c6d9cfdfced01b7ec5a8974044bb09dbd4" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nb-connect" version = "1.0.3" @@ -2194,6 +2654,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" +[[package]] +name = "ntapi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "num-integer" version = "0.1.44" @@ -2239,16 +2708,55 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9a7ab5d64814df0fe4a4b5ead45ed6c5f181ee3ff04ba344313a6c80446c5d4" [[package]] -name = "once_cell" -version = "1.6.0" +name = "once_cell" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ad167a2f54e832b82dbe003a046280dceffe5227b5f79e08e363a29638cfddd" + +[[package]] +name = "opaque-debug" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "openssl" +version = "0.10.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "038d43985d1ddca7a9900630d8cd031b56e4794eecc2e9ea39dd17aa04399a70" +dependencies = [ + "bitflags", + "cfg-if 1.0.0", + "foreign-types", + "lazy_static", + "libc", + "openssl-sys", +] + +[[package]] +name = "openssl-probe" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ad167a2f54e832b82dbe003a046280dceffe5227b5f79e08e363a29638cfddd" +checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" [[package]] -name = "opaque-debug" -version = "0.3.0" +name = "openssl-sys" +version = "0.9.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +checksum = "921fc71883267538946025deffb622905ecad223c28efbfdef9bb59a0175f3e6" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] [[package]] name = "parking" @@ -2281,6 +2789,39 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "pathdiff" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877630b3de15c0b64cc52f659345724fbf6bdad9bd9566699fc53688f3c34a34" + +[[package]] +name = "peg" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f76678828272f177ac33b7e2ac2e3e73cc6c1cd1e3e387928aa69562fa51367" +dependencies = [ + "peg-macros", + "peg-runtime", +] + +[[package]] +name = "peg-macros" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "636d60acf97633e48d266d7415a9355d4389cea327a193f87df395d88cd2b14d" +dependencies = [ + "peg-runtime", + "proc-macro2 1.0.24", + "quote 1.0.9", +] + +[[package]] +name = "peg-runtime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555b1514d2d99d78150d3c799d4c357a3e2c2a8062cd108e93a06d9057629c5" + [[package]] name = "percent-encoding" version = "2.1.0" @@ -2355,6 +2896,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" + [[package]] name = "polling" version = "2.0.2" @@ -2460,7 +3007,7 @@ checksum = "02b10678c913ecbd69350e8535c3aef91a8676c0773fc1d7b95cdd196d7f2f26" dependencies = [ "bytes 0.5.6", "heck", - "itertools", + "itertools 0.8.2", "log", "multimap", "petgraph", @@ -2477,7 +3024,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "537aa19b95acde10a12fec4301466386f757403de4cd4e5b4fa78fb5ecb18f72" dependencies = [ "anyhow", - "itertools", + "itertools 0.8.2", "proc-macro2 1.0.24", "quote 1.0.9", "syn 1.0.60", @@ -2677,8 +3224,8 @@ dependencies = [ "percent-encoding", "pin-project-lite 0.1.11", "sha1", - "tokio", - "tokio-util", + "tokio 0.2.25", + "tokio-util 0.3.1", "url", ] @@ -2728,6 +3275,41 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "reqwest" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0460542b551950620a3648c6aa23318ac6b3cd779114bd873209e6e8b5eb1c34" +dependencies = [ + "base64 0.13.0", + "bytes 1.0.1", + "encoding_rs", + "futures-core", + "futures-util", + "http", + "http-body 0.4.0", + "hyper 0.14.4", + "hyper-tls", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite 0.2.4", + "serde 1.0.123", + "serde_json", + "serde_urlencoded 0.7.0", + "tokio 1.2.0", + "tokio-native-tls", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg 0.7.0", +] + [[package]] name = "resolv-conf" version = "0.7.0" @@ -2801,6 +3383,25 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +dependencies = [ + "lazy_static", + "winapi 0.3.9", +] + [[package]] name = "scoped-tls" version = "1.0.0" @@ -2813,6 +3414,29 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "security-framework" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1759c2e3c8580017a484a7ac56d3abc5a6c1feadf88db2f3633f12ae4268c69" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f99b9d5e26d2a71633cc4f2ebae7cc9f874044e0c351a27e17892d76dce5678b" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "0.9.0" @@ -2887,6 +3511,18 @@ dependencies = [ "serde 0.8.23", ] +[[package]] +name = "serde_urlencoded" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ec5d77e2d4c73717816afac02670d5c4f534ea95ed430442cad02e7a6e32c97" +dependencies = [ + "dtoa", + "itoa", + "serde 1.0.123", + "url", +] + [[package]] name = "serde_urlencoded" version = "0.7.0" @@ -2915,7 +3551,7 @@ version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e48b35457e9d855d3dc05ef32a73e0df1e2c0fd72c38796a4ee909160c8eeec2" dependencies = [ - "darling 0.12.0", + "darling 0.12.1", "proc-macro2 1.0.24", "quote 1.0.9", "syn 1.0.60", @@ -2955,17 +3591,29 @@ dependencies = [ "syn 1.0.60", ] +[[package]] +name = "sha-1" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" +dependencies = [ + "block-buffer 0.7.3", + "digest 0.8.1", + "fake-simd", + "opaque-debug 0.2.3", +] + [[package]] name = "sha-1" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfebf75d25bd900fd1e7d11501efab59bc846dbc76196839663e6637bba9f25f" dependencies = [ - "block-buffer", + "block-buffer 0.9.0", "cfg-if 1.0.0", "cpuid-bool", - "digest", - "opaque-debug", + "digest 0.9.0", + "opaque-debug 0.3.0", ] [[package]] @@ -2974,6 +3622,16 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" +[[package]] +name = "shh" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5205eb079ac8be8ec77b7470aff4a050610d42c32819deee362ca414c926d3ab" +dependencies = [ + "libc", + "winapi 0.3.9", +] + [[package]] name = "signal-hook-registry" version = "1.3.0" @@ -3250,6 +3908,25 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "terminal_size" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ca8ced750734db02076f44132d802af0b33b09942331f4459dde8636fd2406" +dependencies = [ + "libc", + "winapi 0.3.9", +] + [[package]] name = "textwrap" version = "0.11.0" @@ -3259,6 +3936,16 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "textwrap" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "203008d98caf094106cfaba70acfed15e18ed3ddb7d94e49baec153a2b462789" +dependencies = [ + "terminal_size", + "unicode-width", +] + [[package]] name = "thiserror" version = "1.0.24" @@ -3373,16 +4060,32 @@ dependencies = [ "lazy_static", "libc", "memchr", - "mio", + "mio 0.6.23", "mio-uds", "num_cpus", "pin-project-lite 0.1.11", "signal-hook-registry", "slab", - "tokio-macros", + "tokio-macros 0.2.6", "winapi 0.3.9", ] +[[package]] +name = "tokio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8190d04c665ea9e6b6a0dc45523ade572c088d2e6566244c1122671dbf4ae3a" +dependencies = [ + "autocfg", + "bytes 1.0.1", + "libc", + "memchr", + "mio 0.7.9", + "num_cpus", + "pin-project-lite 0.2.4", + "tokio-macros 1.1.0", +] + [[package]] name = "tokio-macros" version = "0.2.6" @@ -3394,6 +4097,27 @@ dependencies = [ "syn 1.0.60", ] +[[package]] +name = "tokio-macros" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf7b11a536f46a809a8a9f0bb4237020f70ecbf115b842360afb127ea2fda57" +dependencies = [ + "proc-macro2 1.0.24", + "quote 1.0.9", + "syn 1.0.60", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio 1.2.0", +] + [[package]] name = "tokio-util" version = "0.3.1" @@ -3406,7 +4130,21 @@ dependencies = [ "futures-sink", "log", "pin-project-lite 0.1.11", - "tokio", + "tokio 0.2.25", +] + +[[package]] +name = "tokio-util" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebb7cb2f00c5ae8df755b252306272cd1790d39728363936e01827e11f0b017b" +dependencies = [ + "bytes 1.0.1", + "futures-core", + "futures-sink", + "log", + "pin-project-lite 0.2.4", + "tokio 1.2.0", ] [[package]] @@ -3424,21 +4162,21 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74a5d6e7439ecf910463667080de772a9c7ddf26bc9fb4f3252ac3862e43337d" dependencies = [ - "async-stream", + "async-stream 0.2.1", "async-trait", "base64 0.12.3", "bytes 0.5.6", "futures-core", "futures-util", "http", - "http-body", - "hyper", + "http-body 0.3.1", + "hyper 0.13.10", "percent-encoding", "pin-project 0.4.27", "prost", "prost-derive", - "tokio", - "tokio-util", + "tokio 0.2.25", + "tokio-util 0.3.1", "tower", "tower-balance", "tower-load", @@ -3490,7 +4228,7 @@ dependencies = [ "pin-project 0.4.27", "rand 0.7.3", "slab", - "tokio", + "tokio 0.2.25", "tower-discover", "tower-layer", "tower-load", @@ -3508,7 +4246,7 @@ checksum = "c4887dc2a65d464c8b9b66e0e4d51c2fd6cf5b3373afc72805b0a60bce00446a" dependencies = [ "futures-core", "pin-project 0.4.27", - "tokio", + "tokio 0.2.25", "tower-layer", "tower-service", "tracing", @@ -3539,7 +4277,7 @@ checksum = "92c3040c5dbed68abffaa0d4517ac1a454cd741044f33ab0eefab6b8d1361404" dependencies = [ "futures-core", "pin-project 0.4.27", - "tokio", + "tokio 0.2.25", "tower-layer", "tower-load", "tower-service", @@ -3554,7 +4292,7 @@ dependencies = [ "futures-core", "log", "pin-project 0.4.27", - "tokio", + "tokio 0.2.25", "tower-discover", "tower-service", ] @@ -3577,7 +4315,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce50370d644a0364bf4877ffd4f76404156a248d104e2cc234cd391ea5cdc965" dependencies = [ - "tokio", + "tokio 0.2.25", "tower-service", ] @@ -3591,7 +4329,7 @@ dependencies = [ "futures-util", "indexmap", "log", - "tokio", + "tokio 0.2.25", "tower-service", ] @@ -3603,7 +4341,7 @@ checksum = "e6727956aaa2f8957d4d9232b308fe8e4e65d99db30f42b225646e86c9b6a952" dependencies = [ "futures-core", "pin-project 0.4.27", - "tokio", + "tokio 0.2.25", "tower-layer", "tower-service", ] @@ -3621,7 +4359,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "127b8924b357be938823eaaec0608c482d40add25609481027b96198b2e4b31e" dependencies = [ "pin-project 0.4.27", - "tokio", + "tokio 0.2.25", "tower-layer", "tower-service", ] @@ -3705,14 +4443,14 @@ dependencies = [ "async-trait", "backtrace", "enum-as-inner", - "futures 0.3.12", + "futures 0.3.13", "idna", "lazy_static", "log", "rand 0.7.3", "smallvec", "thiserror", - "tokio", + "tokio 0.2.25", "url", ] @@ -3724,7 +4462,7 @@ checksum = "6759e8efc40465547b0dfce9500d733c65f969a4cbbfbe3ccf68daaa46ef179e" dependencies = [ "backtrace", "cfg-if 0.1.10", - "futures 0.3.12", + "futures 0.3.13", "ipconfig", "lazy_static", "log", @@ -3732,7 +4470,7 @@ dependencies = [ "resolv-conf", "smallvec", "thiserror", - "tokio", + "tokio 0.2.25", "trust-dns-proto", ] @@ -3742,12 +4480,32 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +[[package]] +name = "typed-builder" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f85f4270f4f449a3f2c0cf2aecc8415e388a597aeacc7d55fc749c5c968c8533" +dependencies = [ + "proc-macro2 1.0.24", + "quote 1.0.9", + "syn 1.0.60", +] + [[package]] name = "typenum" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.4" @@ -3802,6 +4560,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "urlencoding" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9232eb53352b4442e40d7900465dfc534e8cb2dc8f18656fcb2ac16112b5593" + +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom 0.2.2", +] + [[package]] name = "value-bag" version = "1.0.0-alpha.6" @@ -3811,6 +4584,12 @@ dependencies = [ "ctor", ] +[[package]] +name = "vcpkg" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00bca6106a5e23f3eee943593759b7fcddb00554332e856d990c893966879fb" + [[package]] name = "vec-arena" version = "1.0.0" @@ -3835,6 +4614,17 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" +[[package]] +name = "walkdir" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d" +dependencies = [ + "same-file", + "winapi 0.3.9", + "winapi-util", +] + [[package]] name = "want" version = "0.3.0" @@ -3845,6 +4635,32 @@ dependencies = [ "try-lock", ] +[[package]] +name = "warp" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f41be6df54c97904af01aa23e613d4521eed7ab23537cede692d4058f6449407" +dependencies = [ + "bytes 0.5.6", + "futures 0.3.13", + "headers", + "http", + "hyper 0.13.10", + "log", + "mime", + "mime_guess", + "pin-project 0.4.27", + "scoped-tls", + "serde 1.0.123", + "serde_json", + "serde_urlencoded 0.6.1", + "tokio 0.2.25", + "tower-service", + "tracing", + "tracing-futures", + "urlencoding", +] + [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" @@ -3970,6 +4786,27 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webdriver" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ff28d617c8e5648fb34eff57357ae44cd8ed792bbe11b5901a4f4a528d7b201" +dependencies = [ + "base64 0.12.3", + "bytes 0.5.6", + "cookie 0.12.0", + "http", + "log", + "serde 1.0.123", + "serde_derive", + "serde_json", + "time 0.1.43", + "tokio 0.2.25", + "unicode-segmentation", + "url", + "warp", +] + [[package]] name = "wee_alloc" version = "0.4.5" @@ -4034,6 +4871,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -4049,6 +4895,15 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "winreg" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "ws2_32-sys" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index da5690355..ceb4505ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,7 +50,7 @@ futures = { version = "0.3", features = ["compat"] } humantime-serde = "1.0" medea-client-api-proto = { version = "0.2", features = ["medea"] } medea-control-api-proto = "0.1" -medea-macro = "0.2" +medea-macro = { path = "crates/medea-macro" } rand = "0.8" rust-crypto = "0.2" serde = { version = "1.0", features = ["derive"] } @@ -83,11 +83,34 @@ url = "2.1" [dev-dependencies] actix-codec = "0.3" actix-rt = "1.1" +async-recursion = "0.3" awc = "2.0" +cucumber_rust = { version = "0.8", features = ["macros"] } derive_builder = "0.9" +fantoccini = "0.17" function_name = "0.2" +hyper = { version = "0.14", features = ["server"] } lazy_static = "1.4" +medea-control-api-mock = { path = "mock/control-api" } mockall = "0.9" +once_cell = "1.5" +reqwest = { version = "0.11", features = ["json"] } serial_test = "0.5" tempfile = "3.1" tokio = { version = "0.2", features = ["macros", "rt-threaded"] } +tokio-util = "0.6" +uuid = { version = "0.8", features = ["v4"] } +webdriver = "0.43" +[dev-dependencies.tokio_1] + package = "tokio" + version = "1.1" + features = ["fs", "macros", "rt-multi-thread"] + +[[test]] +name = "e2e" +path = "tests/e2e/main.rs" +harness = false # allows Cucumber to print output instead of libtest + +[[test]] +name = "integration" +path = "tests/integration/main.rs" diff --git a/Makefile b/Makefile index d12bffd46..7e1ab3581 100644 --- a/Makefile +++ b/Makefile @@ -109,6 +109,7 @@ release: release.crates release.npm test: @make test.unit + @make test.integration up=yes dockerized=no @make test.e2e up=yes dockerized=no @@ -127,7 +128,7 @@ up: up.dev # make down.control down.control: - kill -9 $(pidof medea-control-api-mock) + -killall medea-control-api-mock down.coturn: docker.down.coturn @@ -144,6 +145,7 @@ down.demo: docker.down.demo down.dev: @make docker.down.medea dockerized=no @make docker.down.medea dockerized=yes + @make down.control @make docker.down.coturn @@ -153,11 +155,11 @@ down.medea: docker.down.medea # Run Control API mock server. # # Usage: -# make up.control +# make up.control [background=(no|yes)] up.control: - make wait.port port=6565 - cargo run -p medea-control-api-mock + cargo build -p medea-control-api-mock + cargo run -p medea-control-api-mock $(if $(call eq,$(background),yes),&,) up.coturn: docker.up.coturn @@ -423,37 +425,68 @@ endif endif -# Run Rust E2E tests of project. +# Run Rust integration tests of project. # # Usage: -# make test.e2e [( [up=no] -# | up=yes [( [dockerized=no] [debug=(yes|no)] -# | dockerized=yes [tag=(dev|)] -# [log=(no|yes)] -# [log-to-file=(no|yes)] )] -# [wait=(5|)] )] - -test-e2e-env = RUST_BACKTRACE=1 \ +# make test.integration +# [( [up=no] +# | up=yes [( [dockerized=no] [debug=(yes|no)] +# | dockerized=yes [tag=(dev|)] +# [log=(no|yes)] [log-to-file=(no|yes)] )] +# [wait=(5|)] )] + +test-integration-env = RUST_BACKTRACE=1 \ $(if $(call eq,$(log),yes),,RUST_LOG=warn) \ MEDEA_CONTROL__STATIC_SPECS_DIR=tests/specs/ \ MEDEA_CONF=tests/medea.config.toml -test.e2e: +test.integration: ifeq ($(up),yes) make docker.up.coturn background=yes - env $(test-e2e-env) \ + env $(test-integration-env) \ make docker.up.medea debug=$(debug) background=yes log=$(log) \ dockerized=$(dockerized) \ tag=$(tag) \ log-to-file=$(log-to-file) sleep $(if $(call eq,$(wait),),5,$(wait)) endif - RUST_BACKTRACE=1 cargo test --test e2e + RUST_BACKTRACE=1 cargo test --test integration ifeq ($(up),yes) -make down endif +# Run E2E tests of project. +# +# Usage: +# make test.e2e +# [( [up=no] +# | up=yes [browser=(chrome|firefox)] +# [( [dockerized=no] +# | dockerized=yes [tag=(dev|)] [rebuild=(no|yes)] )] +# [debug=(yes|no)] +# [( [background=no] +# | background=yes [log=(no|yes)] )] + +test.e2e: +ifeq ($(up),yes) +ifeq ($(dockerized),yes) +ifeq ($(rebuild),yes) + @make docker.build image=medea debug=$(debug) tag=$(tag) + @make docker.build image=medea-control-api-mock debug=$(debug) tag=$(tag) +endif +endif + @make docker.up.e2e browser=$(browser) background=yes log=$(log) \ + dockerized=$(dockerized) tag=$(tag) debug=$(debug) + @make wait.port port=4444 +endif + cargo test --test e2e +ifeq ($(up),yes) + @make docker.down.e2e +endif + + + #################### # Waiting commands # @@ -583,6 +616,17 @@ docker.down.demo: docker-compose -f jason/demo/docker-compose.yml down --rmi=local -v +# Stop E2E tests environment in Docker Compose and remove all related +# containers. +# +# Usage: +# make docker.down.e2e + +docker.down.e2e: down.control + @make docker.down.medea dockerized=no + docker-compose -f tests/e2e/docker-compose.yml down --rmi=local -v + + # Stop Medea media server in Docker Compose environment # and remove all related containers. # @@ -745,6 +789,53 @@ docker.up.demo: docker.down.demo docker-compose -f jason/demo/docker-compose.yml up +# Run E2E tests environment in Docker Compose. +# +# Usage: +# make docker.up.e2e [browser=(chrome|firefox)] +# [( [dockerized=no] +# | dockerized=yes [tag=(dev|)] )] +# [debug=(yes|no)] +# [( [background=no] +# | background=yes [log=(no|yes)] )] + +docker-up-e2e-env = RUST_BACKTRACE=1 \ + $(if $(call eq,$(log),yes),,RUST_LOG=warn) \ + MEDEA_CONTROL__STATIC_SPECS_DIR=tests/specs/ \ + MEDEA_CONF=tests/medea.config.toml \ + COMPOSE_IMAGE_VER=$(if $(call eq,$(tag),),dev,$(tag)) \ + COMPOSE_CONTROL_MOCK_IMAGE_VER=$(if $(call eq,$(tag),),dev,$(tag)) \ + COMPOSE_WEBDRIVER_IMAGE_NAME=$(strip \ + $(if $(call eq,$(browser),firefox),\ + ghcr.io/instrumentisto/geckodriver ,\ + selenoid/chrome )) \ + COMPOSE_WEBDRIVER_IMAGE_VER=$(strip \ + $(if $(call eq,$(browser),firefox),\ + $(FIREFOX_VERSION) ,\ + $(CHROME_VERSION) )) + +docker.up.e2e: docker.down.e2e + @make build.jason debug=$(debug) dockerized=no + env $(docker-up-e2e-env) \ + docker-compose -f tests/e2e/docker-compose$(if $(call eq,$(dockerized),yes),,.host).yml \ + up $(if $(call eq,$(dockerized),yes),\ + $(if $(call eq,$(background),yes),-d,--abort-on-container-exit),-d) +ifeq ($(background),yes) +ifeq ($(log),yes) + env $(docker-up-e2e-env) \ + docker-compose -f tests/e2e/docker-compose$(if $(call eq,$(dockerized),yes),,.host).yml \ + logs -f & +endif +endif +ifneq ($(dockerized),yes) + @MEDEA_SERVER__CLIENT__HTTP__BIND_PORT=8001 \ + make docker.up.medea dockerized=no debug=$(debug) \ + background=$(background) log-to-file=$(log) + @make wait.port port=6565 + @make up.control background=$(background) +endif + + # Run Medea media server in Docker Compose environment. # # Usage: @@ -999,17 +1090,17 @@ endef cargo.version \ docker.build \ docker.down.control docker.down.coturn docker.down.demo \ - docker.down.medea docker.down.webdriver \ + docker.down.e2e docker.down.medea docker.down.webdriver \ docker.pull docker.push docker.tag docker.tar docker.untar \ - docker.up.control docker.up.coturn docker.up.demo docker.up.medea \ - docker.up.webdriver \ + docker.up.control docker.up.coturn docker.up.demo docker.up.e2e \ + docker.up.medea docker.up.webdriver \ docs docs.rust \ down down.control down.coturn down.demo down.dev down.medea \ helm helm.dir helm.down helm.lint helm.list \ helm.package helm.package.release helm.up \ minikube.boot \ release release.crates release.helm release.npm \ - test test.e2e test.unit \ + test test.e2e test.integration test.unit \ up up.control up.coturn up.demo up.dev up.jason up.medea \ wait.port \ yarn yarn.version diff --git a/docker-compose.coturn.yml b/docker-compose.coturn.yml index bdfd32d7a..a7618c3ac 100644 --- a/docker-compose.coturn.yml +++ b/docker-compose.coturn.yml @@ -16,7 +16,6 @@ services: container_name: ${COMPOSE_PROJECT_NAME}-coturn-db image: redis:alpine command: ["redis-server", "/etc/redis.conf"] - ports: - - "6379:6379" # coturn redis volumes: - ./_dev/coturn/redis.conf:/etc/redis.conf:ro + network_mode: host diff --git a/docker-compose.medea.yml b/docker-compose.medea.yml index ed15ce61b..b0f2caf35 100644 --- a/docker-compose.medea.yml +++ b/docker-compose.medea.yml @@ -8,8 +8,6 @@ services: RUST_LOG: ${RUST_LOG} MEDEA_CONF: ${MEDEA_CONF} MEDEA_CONTROL__STATIC_SPECS_DIR: ${MEDEA_CONTROL__STATIC_SPECS_DIR} - ports: - - "8080:8080" volumes: - ./${MEDEA_CONF}:/${MEDEA_CONF}:ro - ./${MEDEA_CONTROL__STATIC_SPECS_DIR}:/${MEDEA_CONTROL__STATIC_SPECS_DIR}:ro diff --git a/mock/control-api/src/api/endpoint.rs b/mock/control-api/src/api/endpoint.rs index d6a1071a2..7e707a4a1 100644 --- a/mock/control-api/src/api/endpoint.rs +++ b/mock/control-api/src/api/endpoint.rs @@ -146,22 +146,22 @@ impl From for VideoSettings { pub struct WebRtcPublishEndpoint { /// ID of [`WebRtcPublishEndpoint`]. #[serde(skip_deserializing)] - id: String, + pub id: String, /// Mode of connection for this [`WebRtcPublishEndpoint`]. - p2p: P2pMode, + pub p2p: P2pMode, /// Option to relay all media through a TURN server forcibly. #[serde(default)] - force_relay: bool, + pub force_relay: bool, /// Settings for the audio media type of the [`WebRtcPublishEndpoint`]. #[serde(default)] - audio_settings: AudioSettings, + pub audio_settings: AudioSettings, /// Settings for the video media type of the [`WebRtcPublishEndpoint`]. #[serde(default)] - video_settings: VideoSettings, + pub video_settings: VideoSettings, } impl WebRtcPublishEndpoint { @@ -207,17 +207,17 @@ impl From for WebRtcPublishEndpoint { /// [Control API]: https://tinyurl.com/yxsqplq7 #[derive(Debug, Deserialize, Serialize)] pub struct WebRtcPlayEndpoint { - /// ID of `WebRtcPlayEndpoint`. + /// ID of this [`WebRtcPlayEndpoint`]. #[serde(skip_deserializing)] - id: String, + pub id: String, /// URI in format `local://{room_id}/{member_id}/{endpoint_id}` pointing to /// [`WebRtcPublishEndpoint`] which this [`WebRtcPlayEndpoint`] plays. - src: String, + pub src: String, /// Option to relay all media through a TURN server forcibly. #[serde(default)] - force_relay: bool, + pub force_relay: bool, } impl WebRtcPlayEndpoint { diff --git a/mock/control-api/src/api/member.rs b/mock/control-api/src/api/member.rs index 0fd0020b1..0e651e1b9 100644 --- a/mock/control-api/src/api/member.rs +++ b/mock/control-api/src/api/member.rs @@ -7,46 +7,46 @@ use serde::{Deserialize, Serialize}; use super::endpoint::Endpoint; -/// Entity that represents [Control API] `Member`. +/// Entity that represents a [Control API] [`Member`]. /// /// [Control API]: https://tinyurl.com/yxsqplq7 #[derive(Deserialize, Serialize, Debug)] pub struct Member { - /// ID of `Member`. + /// ID of this [`Member`]. #[serde(skip_deserializing)] - id: String, + pub id: String, - /// Pipeline of [Control API] `Member`. + /// [Control API] pipeline of this [`Member`]. /// /// [Control API]: https://tinyurl.com/yxsqplq7 - pipeline: HashMap, + pub pipeline: HashMap, - /// Optional `Member` credentials. + /// Optional credentials of this [`Member`]. /// - /// If `None` then random credentials will be generated on Medea side. - credentials: Option, + /// If [`None`] then random credentials will be generated on Medea side. + pub credentials: Option, /// URL to which `OnJoin` Control API callback will be sent. #[serde(skip_serializing_if = "Option::is_none")] - on_join: Option, + pub on_join: Option, /// URL to which `OnLeave` Control API callback will be sent. #[serde(skip_serializing_if = "Option::is_none")] - on_leave: Option, + pub on_leave: Option, - /// Timeout of receiving heartbeat messages from the `Member` via Client - /// API. Once reached, the `Member` is considered being idle. + /// Timeout of receiving heartbeat messages from this [`Member`] via Client + /// API. Once reached, the [`Member`] is considered being idle. #[serde(default, with = "humantime_serde")] - idle_timeout: Option, + pub idle_timeout: Option, - /// Timeout of the `Member` reconnecting via Client API. - /// Once reached, the `Member` is considered disconnected. + /// Timeout of this [`Member`] reconnecting via Client API. + /// Once reached, the [`Member`] is considered disconnected. #[serde(default, with = "humantime_serde")] - reconnect_timeout: Option, + pub reconnect_timeout: Option, - /// Interval of sending pings from Medea to the `Member` via Client API. + /// Interval of sending pings from Medea to this [`Member`] via Client API. #[serde(default, with = "humantime_serde")] - ping_interval: Option, + pub ping_interval: Option, } impl Member { @@ -108,7 +108,7 @@ impl From for Member { /// Credentials of the [`Member`]. #[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "lowercase")] -enum Credentials { +pub enum Credentials { /// [Argon2] hash of the [`Member`] credentials. /// /// [Argon2]: https://en.wikipedia.org/wiki/Argon2 diff --git a/mock/control-api/src/api/mod.rs b/mock/control-api/src/api/mod.rs index 52c18a764..712dfcd19 100644 --- a/mock/control-api/src/api/mod.rs +++ b/mock/control-api/src/api/mod.rs @@ -243,7 +243,7 @@ mod create { /// Error object. Returns when some error happened on [Control API]'s side. /// /// [Control API]: https://tinyurl.com/yxsqplq7 -#[derive(Debug, Serialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct ErrorResponse { /// Medea's Control API error code. pub code: u32, @@ -268,7 +268,7 @@ impl Into for proto::Error { /// Response which returns sids. /// /// Used for create methods. -#[derive(Debug, Serialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct CreateResponse { /// URIs with which [Jason] can connect `Member`s. /// @@ -286,7 +286,7 @@ pub struct CreateResponse { /// Response which can return only error (if any). /// /// Used for delete methods. -#[derive(Debug, Serialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct Response { /// Error if something happened on [Control API]'s side. /// @@ -399,7 +399,7 @@ impl From for Element { } /// Response on request for get `Element` request. -#[derive(Debug, Serialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct SingleGetResponse { /// Requested element. #[serde(skip_serializing_if = "Option::is_none")] diff --git a/mock/control-api/src/api/room.rs b/mock/control-api/src/api/room.rs index df1dafb26..6940ee830 100644 --- a/mock/control-api/src/api/room.rs +++ b/mock/control-api/src/api/room.rs @@ -12,12 +12,12 @@ use super::member::Member; /// [Control API]: https://tinyurl.com/yxsqplq7 #[derive(Debug, Deserialize, Serialize)] pub struct Room { - /// ID of `Room`. + /// ID of this [`Room`]. #[serde(skip_deserializing)] - id: String, + pub id: String, - /// Pipeline of `Room`. - pipeline: HashMap, + /// Pipeline of this [`Room`]. + pub pipeline: HashMap, } impl Room { diff --git a/mock/control-api/src/api/ws.rs b/mock/control-api/src/api/ws.rs index 424f695d7..033bfb2c3 100644 --- a/mock/control-api/src/api/ws.rs +++ b/mock/control-api/src/api/ws.rs @@ -63,6 +63,7 @@ enum NotificationVariants<'a> { impl Notification { /// Builds `method: Created` [`Notification`]. + #[must_use] pub fn created(fid: &Fid, element: &Element) -> Notification { Self( serde_json::to_value(NotificationVariants::Created { @@ -74,6 +75,7 @@ impl Notification { } /// Builds `method: Deleted` [`Notification`]. + #[must_use] pub fn deleted(fid: &Fid) -> Notification { Self( serde_json::to_value(NotificationVariants::Deleted { @@ -84,6 +86,7 @@ impl Notification { } /// Builds `method: Broadcast` [`Notification`]. + #[must_use] pub fn broadcast(payload: Value) -> Notification { Self( serde_json::to_value(NotificationVariants::Broadcast { payload }) diff --git a/mock/control-api/src/lib.rs b/mock/control-api/src/lib.rs new file mode 100644 index 000000000..0d1a53f0f --- /dev/null +++ b/mock/control-api/src/lib.rs @@ -0,0 +1,40 @@ +//! REST mock server for gRPC [Medea]'s [Control API]. +//! +//! [Medea]: https://github.com/instrumentisto/medea +//! [Control API]: https://tinyurl.com/yxsqplq7 + +#![allow(clippy::module_name_repetitions)] + +pub mod api; +pub mod callback; +pub mod client; +pub mod prelude; + +use slog::{o, Drain}; +use slog_scope::GlobalLoggerGuard; + +pub mod proto { + pub use crate::api::{ + endpoint::{ + AudioSettings, Endpoint, P2pMode, PublishPolicy, VideoSettings, + WebRtcPlayEndpoint, WebRtcPublishEndpoint, + }, + member::{Credentials, Member}, + room::Room, + CreateResponse, Element, ErrorResponse, Response, SingleGetResponse, + }; +} + +/// Initializes [`slog`] logger outputting logs with a [`slog_term`]'s +/// decorator. +pub fn init_logger() -> GlobalLoggerGuard { + let decorator = slog_term::TermDecorator::new().build(); + let drain = slog_term::FullFormat::new(decorator).build().fuse(); + let drain = slog_envlogger::new(drain).fuse(); + let drain = slog_async::Async::new(drain).build().fuse(); + let logger = slog::Logger::root(drain, o!()); + let scope_guard = slog_scope::set_global_logger(logger); + slog_stdlog::init().unwrap(); + + scope_guard +} diff --git a/mock/control-api/src/main.rs b/mock/control-api/src/main.rs index 784eecef7..0c1cf5086 100644 --- a/mock/control-api/src/main.rs +++ b/mock/control-api/src/main.rs @@ -3,24 +3,13 @@ //! [Medea]: https://github.com/instrumentisto/medea //! [Control API]: https://tinyurl.com/yxsqplq7 -// TODO: Remove `clippy::must_use_candidate` once the issue below is resolved: -// https://github.com/rust-lang/rust-clippy/issues/4779 -#![allow(clippy::module_name_repetitions, clippy::must_use_candidate)] - -pub mod api; -pub mod callback; -pub mod client; -pub mod prelude; - use clap::{ app_from_crate, crate_authors, crate_description, crate_name, crate_version, Arg, }; -use slog::{o, Drain}; -use slog_scope::GlobalLoggerGuard; +use medea_control_api_mock::{api, callback, init_logger}; -#[actix_web::main] -async fn main() { +fn main() { dotenv::dotenv().ok(); let opts = app_from_crate!() @@ -56,20 +45,8 @@ async fn main() { let _log_guard = init_logger(); - let callback_server = callback::server::run(&opts).await; - api::run(&opts, callback_server).await; -} - -/// Initializes [`slog`] logger which will output logs with [`slog_term`]'s -/// decorator. -fn init_logger() -> GlobalLoggerGuard { - let decorator = slog_term::TermDecorator::new().build(); - let drain = slog_term::FullFormat::new(decorator).build().fuse(); - let drain = slog_envlogger::new(drain).fuse(); - let drain = slog_async::Async::new(drain).build().fuse(); - let logger = slog::Logger::root(drain, o!()); - let scope_guard = slog_scope::set_global_logger(logger); - slog_stdlog::init().unwrap(); - - scope_guard + actix_web::rt::System::new("mock-control-api").block_on(async move { + let callback_server = callback::server::run(&opts).await; + api::run(&opts, callback_server).await; + }); } diff --git a/tests/e2e/browser/client.rs b/tests/e2e/browser/client.rs new file mode 100644 index 000000000..b522383d1 --- /dev/null +++ b/tests/e2e/browser/client.rs @@ -0,0 +1,251 @@ +//! Client for a [WebDriver]. +//! +//! [WebDriver]: https://w3.org/TR/webdriver + +use std::sync::{mpsc, Arc}; + +use fantoccini::{Client, ClientBuilder, Locator}; +use futures::lock::Mutex; +use serde::Deserialize; +use serde_json::{json, Value as Json}; +use tokio_1::{self as tokio, task}; +use webdriver::{capabilities::Capabilities, common::WebWindow}; + +use crate::conf; + +use super::{js::Statement, Error, Result}; + +/// Arguments for Chrome browser. +const CHROME_ARGS: &[&str] = &[ + "--use-fake-device-for-media-stream", + "--use-fake-ui-for-media-stream", + "--disable-web-security", + "--disable-dev-shm-usage", + "--no-sandbox", +]; + +/// Arguments for Firefox browser. +const FIREFOX_ARGS: &[&str] = &[]; + +/// Result returned from the all JS code executed in a browser. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "lowercase")] +enum JsResult { + /// [`Json`] value of a successful result. + Ok(Json), + + /// [`Json`] value of an error result. + Err(Json), +} + +impl From for Result { + #[inline] + fn from(from: JsResult) -> Self { + match from { + JsResult::Ok(ok) => Self::Ok(ok), + JsResult::Err(err) => Self::Err(Error::Js(err)), + } + } +} + +/// Client for interacting with a browser through a [WebDriver] protocol. +/// +/// [WebDriver]: https://w3.org/TR/webdriver +#[derive(Clone, Debug)] +pub struct WebDriverClient(Arc>); + +impl WebDriverClient { + /// Creates a new [`WebDriverClient`] connected to a [WebDriver]. + /// + /// [WebDriver]: https://w3.org/TR/webdriver + #[inline] + pub async fn new() -> Result { + Ok(Self(Arc::new(Mutex::new(Inner::new().await?)))) + } + + /// Creates a new window in a browser and returns it's ID. + #[inline] + pub async fn new_window(&self) -> Result { + self.0.lock().await.new_window().await + } + + /// Switches to the provided [`WebWindow`] and executes the provided + /// [`Statement`] in it. + #[inline] + pub async fn switch_to_window_and_execute( + &self, + window: WebWindow, + exec: Statement, + ) -> Result { + self.0 + .lock() + .await + .switch_to_window_and_execute(window, exec) + .await + } + + /// Synchronously closes a [WebDriver] session. + /// + /// [WebDriver]: https://w3.org/TR/webdriver + pub fn blocking_close(&self) { + let (tx, rx) = mpsc::channel(); + let client = self.0.clone(); + tokio::spawn(async move { + let mut inner = client.lock().await; + inner.0.close().await.map_err(|e| dbg!("{:?}", e)).unwrap(); + tx.send(()).unwrap(); + }); + task::block_in_place(move || { + rx.recv().unwrap(); + }); + } + + /// Synchronously closes the provided [`WebWindow`]. + pub fn blocking_window_close(&self, window: WebWindow) { + let (tx, rx) = mpsc::channel(); + let client = self.0.clone(); + tokio::spawn(async move { + let mut client = client.lock().await; + client.close_window(window).await; + tx.send(()).unwrap(); + }); + task::block_in_place(move || { + rx.recv().unwrap(); + }); + } +} + +/// Inner implementation of a [`WebDriverClient`]. +struct Inner(Client); + +impl Inner { + /// Creates a new [WebDriver] session. + /// + /// [WebDriver]: https://w3.org/TR/webdriver + pub async fn new() -> Result { + Ok(Self( + ClientBuilder::native() + .capabilities(Self::get_webdriver_capabilities()) + .connect(&conf::WEBDRIVER_ADDR) + .await?, + )) + } + + /// Executes the provided [`Statement`] in the current [`WebWindow`]. + pub async fn execute(&mut self, statement: Statement) -> Result { + let (inner_js, args) = statement.prepare(); + + // language=JavaScript + let js = format!( + r#" + ( + async () => {{ + let callback = arguments[arguments.length - 1]; + try {{ + {executable_js} + callback({{ ok: lastResult }}); + }} catch (e) {{ + if (e.ptr != undefined) {{ + callback({{ + err: {{ + name: e.name(), + message: e.message(), + trace: e.trace(), + source: e.source() + }} + }}); + }} else {{ + callback({{ err: e.toString() }}); + }} + }} + }} + )(); + "#, + executable_js = inner_js, + ); + let res = self.0.execute_async(&js, args).await?; + + serde_json::from_value::(res)?.into() + } + + /// Creates a new [`WebWindow`] and returns it's ID. + /// + /// Creates a `registry` in the created [`WebWindow`]. + pub async fn new_window(&mut self) -> Result { + let window = WebWindow(self.0.new_window(true).await?.handle); + self.0.switch_to_window(window.clone()).await?; + self.0 + .goto(&format!("http://{}/index.html", *conf::FILE_SERVER_HOST)) + .await?; + self.0.wait_for_find(Locator::Id("loaded")).await?; + + self.execute(Statement::new( + // language=JavaScript + r#" + async () => { + window.registry = new Map(); + } + "#, + vec![], + )) + .await?; + + Ok(window) + } + + /// Switches to the provided [`WebWindow`] and executes the provided + /// [`Statement`]. + pub async fn switch_to_window_and_execute( + &mut self, + window: WebWindow, + exec: Statement, + ) -> Result { + self.0.switch_to_window(window).await?; + Ok(self.execute(exec).await?) + } + + /// Closes the provided [`WebWindow`]. + pub async fn close_window(&mut self, window: WebWindow) { + if self.0.switch_to_window(window).await.is_ok() { + let _ = self.0.close_window().await; + } + } + + /// Returns `moz:firefoxOptions` for a Firefox browser. + fn get_firefox_caps() -> serde_json::Value { + let mut args = FIREFOX_ARGS.to_vec(); + if *conf::HEADLESS { + args.push("--headless"); + } + json!({ + "prefs": { + "media.navigator.streams.fake": true, + "media.navigator.permission.disabled": true, + "media.autoplay.enabled": true, + "media.autoplay.enabled.user-gestures-needed ": false, + "media.autoplay.ask-permission": false, + "media.autoplay.default": 0, + }, + "args": args, + }) + } + + /// Returns `goog:chromeOptions` for a Chrome browser. + fn get_chrome_caps() -> serde_json::Value { + let mut args = CHROME_ARGS.to_vec(); + if *conf::HEADLESS { + args.push("--headless"); + } + json!({ "args": args }) + } + + /// Returns [WebDriver capabilities][1] for Chrome and Firefox browsers. + /// + /// [1]: https:/mdn.io/Web/WebDriver/Capabilities + fn get_webdriver_capabilities() -> Capabilities { + let mut caps = Capabilities::new(); + caps.insert("moz:firefoxOptions".to_string(), Self::get_firefox_caps()); + caps.insert("goog:chromeOptions".to_string(), Self::get_chrome_caps()); + caps + } +} diff --git a/tests/e2e/browser/js.rs b/tests/e2e/browser/js.rs new file mode 100644 index 000000000..87ae4d81b --- /dev/null +++ b/tests/e2e/browser/js.rs @@ -0,0 +1,147 @@ +//! JS code executable in a browser. + +use std::{iter, mem}; + +use serde_json::Value as Json; + +use crate::object::ObjectPtr; + +/// Representation of a JS code executable in a browser. +/// +/// Example of a JS expression: +/// ```js +/// async (lastResult) => { +/// const [room] = objs; +/// const [id] = args; +/// // ... +/// +/// return "foobar"; +/// } +/// ``` +pub struct Statement { + /// Actual JS code to be executed. + expression: String, + + /// Arguments for the [`Statement::expression`] which will be provided + /// as `args` array. + args: Vec, + + /// [`ObjectPtr`] to the JS objects needed by [`Statement::expression`] + /// which will be provided as `objs` array. + objs: Vec, + + /// [`Statement`] which should be executed after this [`Statement`]. + /// + /// Result returned from this [`Statement`] will be provided to the + /// [`Statement::and_then`]. + and_then: Option>, +} + +impl Statement { + /// Returns a new [`Statement`] with the provided JS code and arguments. + /// + /// Example of a JS expression: + /// ```js + /// async (lastResult) => { + /// const [room] = objs; + /// const [id] = args; + /// // ... + /// + /// return "foobar"; + /// } + /// ``` + #[inline] + #[must_use] + pub fn new>>(expression: &str, args: A) -> Self { + Self { + expression: expression.to_owned(), + args: args.into(), + objs: Vec::new(), + and_then: None, + } + } + + /// Returns a new [`Statement`] with the provided JS code, arguments and + /// objects. + #[inline] + #[must_use] + pub fn with_objs( + expression: &str, + args: Vec, + objs: Vec, + ) -> Self { + Self { + expression: expression.to_owned(), + args, + objs, + and_then: None, + } + } + + /// Executes the `another` [`Statement`] after this one being executed + /// successfully. + /// + /// The success value is passed to the next [`Statement`] as a JS lambda + /// argument. + #[inline] + pub fn and_then(mut self, another: Self) -> Self { + self.and_then = Some(Box::new(if let Some(e) = self.and_then { + e.and_then(another) + } else { + another + })); + self + } + + /// Returns a JS code which should be executed in a browser and [`Json`] + /// arguments for this code. + pub(super) fn prepare(self) -> (String, Vec) { + // language=JavaScript + let mut final_js = r#" + let lastResult; + let objs; + let args; + "# + .to_owned(); + + let mut statement = Some(Box::new(self)); + let (mut i, mut args) = (0, Vec::new()); + while let Some(mut e) = statement.take() { + final_js.push_str(&e.step_js(i)); + i += 1; + args.push(mem::take(&mut e.args).into()); + statement = e.and_then; + } + + (final_js, args) + } + + /// Returns a JS code which obtains [`Statement::objs`] JS objects. + /// + /// Should be injected to the [`Statement::step_js`] code. + fn objects_injection_js(&self) -> String { + // language=JavaScript + iter::once("objs = [];\n".to_owned()) + .chain(self.objs.iter().map(|id| { + format!("objs.push(window.registry.get('{}'));\n", id) + })) + .collect() + } + + /// Returns a JS code for this [`Statement`]. + /// + /// Doesn't generates code for the [`Statement::and_then`]. + fn step_js(&self, i: usize) -> String { + // language=JavaScript + format!( + r#" + args = arguments[{i}]; + {objs_js} + lastResult = await ({expr})(lastResult); + "#, + i = i, + objs_js = self.objects_injection_js(), + expr = self.expression, + ) + } +} diff --git a/tests/e2e/browser/mod.rs b/tests/e2e/browser/mod.rs new file mode 100644 index 000000000..4057953cd --- /dev/null +++ b/tests/e2e/browser/mod.rs @@ -0,0 +1,142 @@ +//! Interaction with browser through a [WebDriver] protocol. +//! +//! [WebDriver]: https://w3.org/TR/webdriver + +mod client; +mod js; + +use std::sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, +}; + +use derive_more::{Display, Error, From}; +use serde_json::Value as Json; +use webdriver::common::WebWindow; + +use self::client::WebDriverClient; + +#[doc(inline)] +pub use self::js::Statement; + +/// All errors which can happen while working with a browser. +#[derive(Debug, Display, Error, From)] +pub enum Error { + /// JS exception was thrown while executing a JS code. + #[from(ignore)] + Js(#[error(not(source))] Json), + + /// Error occurred while executing some browser action by a [WebDriver]. + /// + /// [WebDriver]: https://w3.org/TR/webdriver + WebDriverCmd(fantoccini::error::CmdError), + + /// Error occurred while attempting to establish a [WebDriver] session. + /// + /// [WebDriver]: https://w3.org/TR/webdriver + WebDriverSession(fantoccini::error::NewSessionError), + + /// Failed to deserialize a result of the executed JS code. + /// + /// Should never happen. + Deserialize(serde_json::Error), +} + +/// Shortcut for a [`Result`] with an [`Error`](enum@Error) inside. +/// +/// [`Result`]: std::result::Result +type Result = std::result::Result; + +/// [WebDriver] handle of a browser window. +/// +/// All JS code executed by [`Window::execute()`] will run in the right browser +/// window. +/// +/// Window is closed once all [`WebWindow`]s for this window are [`Drop`]ped. +/// +/// [WebDriver]: https://w3.org/TR/webdriver +pub struct Window { + /// Client for interacting with a browser through [WebDriver]. + /// + /// [WebDriver]: https://w3.org/TR/webdriver + client: WebDriverClient, + + /// ID of the window in which this [`Window`] should execute everything. + window: WebWindow, + + /// Count of this [`Window`] references. + /// + /// Used in a [`Drop`] implementation of this [`Window`]. + rc: Arc, +} + +impl Clone for Window { + fn clone(&self) -> Self { + self.rc.fetch_add(1, Ordering::SeqCst); + Self { + client: self.client.clone(), + window: self.window.clone(), + rc: Arc::clone(&self.rc), + } + } +} + +impl Drop for Window { + fn drop(&mut self) { + if self.rc.fetch_sub(1, Ordering::SeqCst) == 1 { + self.client.blocking_window_close(self.window.clone()); + } + } +} + +impl Window { + /// Creates a new [`Window`] in the provided [`WebDriverClient`]. + #[must_use] + async fn new(client: WebDriverClient) -> Self { + let window = client.new_window().await.unwrap(); + Self { + client, + window, + rc: Arc::new(AtomicUsize::new(1)), + } + } + + /// Executes the provided [`Statement`] in this [`Window`]. + #[inline] + pub async fn execute(&self, exec: Statement) -> Result { + self.client + .switch_to_window_and_execute(self.window.clone(), exec) + .await + } +} + +/// Root [WebDriver] client for some browser. +/// +/// This client can create new [`Window`]s. +/// +/// [WebDriver] session will be closed on this object's [`Drop`]. +/// +/// [WebDriver]: https://w3.org/TR/webdriver +pub struct WindowFactory(WebDriverClient); + +impl WindowFactory { + /// Returns a new [`WindowFactory`]. + #[inline] + pub async fn new() -> Result { + Ok(Self(WebDriverClient::new().await?)) + } + + /// Creates and returns a new [`Window`]. + #[inline] + #[must_use] + pub async fn new_window(&self) -> Window { + Window::new(self.0.clone()).await + } +} + +impl Drop for WindowFactory { + #[inline] + fn drop(&mut self) { + self.0.blocking_close(); + } +} diff --git a/tests/e2e/conf.rs b/tests/e2e/conf.rs new file mode 100644 index 000000000..7392b070c --- /dev/null +++ b/tests/e2e/conf.rs @@ -0,0 +1,69 @@ +//! All configurable properties of E2E tests runner. + +use std::env; + +use once_cell::sync::Lazy; + +/// Generates static config variable which will be lazily obtained from the +/// environment variables falling back to a default one. +macro_rules! env_var { + ( + $(#[$meta:meta])* + $name:ident || $default:expr + ) => { + $(#[$meta])* + pub static $name: Lazy = Lazy::new(|| { + env::var(stringify!($name)) + .unwrap_or_else(|_| $default.to_string()) + }); + }; +} + +env_var!( + /// Address of a [WebDriver] client. + /// + /// Default: `http://127.0.0.1:4444` + /// + /// [WebDriver]: https://w3.org/TR/webdriver + WEBDRIVER_ADDR + || "http://127.0.0.1:4444" +); + +env_var!( + /// Address of a Control API mock server. + /// + /// Default: `http://127.0.0.1:8000/control-api` + CONTROL_API_ADDR + || "http://127.0.0.1:8000/control-api" +); + +env_var!( + /// Address a Client API WebSocket endpoint. + /// + /// Default: `ws://127.0.0.1:8001/ws` + CLIENT_API_ADDR + || "ws://127.0.0.1:8001/ws" +); + +env_var!( + /// Host of a [`FileServer`]. + /// + /// Default: `127.0.0.1:30000` + /// + /// [`FileServer`]: crate::file_server::FileServer + FILE_SERVER_HOST + || "127.0.0.1:30000" +); + +env_var!( + /// Path to a Cucumber features which should be run. + FEATURES_PATH + || "tests/e2e/features" +); + +/// Indicator whether tests should run in a headless browser's mode. +/// +/// Default: `true` +pub static HEADLESS: Lazy = Lazy::new(|| { + env::var("HEADLESS").map_or(true, |v| v.to_ascii_lowercase() == "true") +}); diff --git a/tests/e2e/control.rs b/tests/e2e/control.rs new file mode 100644 index 000000000..db5422c3e --- /dev/null +++ b/tests/e2e/control.rs @@ -0,0 +1,62 @@ +//! HTTP client interacting with Medea via its Control API. + +use derive_more::{Display, Error, From}; +use medea_control_api_mock::{ + api::{Response, SingleGetResponse}, + proto::{CreateResponse, Element}, +}; + +use crate::conf; + +/// All errors which can happen while working with a Control API. +#[derive(Debug, Display, Error, From)] +pub enum Error { + Reqwest(reqwest::Error), +} + +type Result = std::result::Result; + +/// Client of a Control API. +pub struct Client(reqwest::Client); + +impl Client { + /// Returns a new Control API [`Client`]. + #[inline] + #[must_use] + pub fn new() -> Self { + Self(reqwest::Client::new()) + } + + /// Creates the provided media [`Element`] in the provided `path` on a Medea + /// media server. + pub async fn create( + &self, + path: &str, + element: Element, + ) -> Result { + Ok(self + .0 + .post(&get_url(path)) + .json(&element) + .send() + .await? + .json() + .await?) + } + + /// Deletes a media [`Element`] identified by the provided `path`. + pub async fn delete(&self, path: &str) -> Result { + Ok(self.0.delete(&get_url(path)).send().await?.json().await?) + } + + /// Returns a media [`Element`] identified by the provided `path`. + pub async fn get(&self, path: &str) -> Result { + Ok(self.0.get(&get_url(path)).send().await?.json().await?) + } +} + +/// Returns URL of a media [`Element`] identified by the provided `path`. +#[must_use] +fn get_url(path: &str) -> String { + format!("{}/{}", *conf::CONTROL_API_ADDR, path) +} diff --git a/tests/e2e/docker-compose.host.yml b/tests/e2e/docker-compose.host.yml new file mode 100644 index 000000000..dd69d905d --- /dev/null +++ b/tests/e2e/docker-compose.host.yml @@ -0,0 +1,40 @@ +version: "2" + +services: + frontend: + container_name: ${COMPOSE_PROJECT_NAME}-frontend + image: nginx:stable-alpine + ports: + - "30000:30000" # frontend http + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + - ./index.html:/usr/share/nginx/html/index.html:ro + - ../../jason/pkg/:/usr/share/nginx/html/pkg/:ro + network_mode: host + coturn: + container_name: ${COMPOSE_PROJECT_NAME}-coturn + image: instrumentisto/coturn:4.5 + depends_on: ["coturn-db"] + command: + - --log-file=stdout + volumes: + - ../../_dev/coturn/turnserver.conf:/etc/coturn/turnserver.conf:ro + - ../../.cache/coturn/data/:/var/lib/coturn/ + network_mode: host + coturn-db: + container_name: ${COMPOSE_PROJECT_NAME}-coturn-db + image: redis:alpine + command: ["redis-server", "/etc/redis.conf"] + ports: + - "6379:6379" # coturn redis + volumes: + - ../../_dev/coturn/redis.conf:/etc/redis.conf:ro + network_mode: host + webdriver: + container_name: ${COMPOSE_PROJECT_NAME}-webdriver + image: ${COMPOSE_WEBDRIVER_IMAGE_NAME}:${COMPOSE_WEBDRIVER_IMAGE_VER} + depends_on: ["coturn", "frontend"] + ports: + - "4444:4444" # browser webdriver + shm_size: 512m + network_mode: host diff --git a/tests/e2e/docker-compose.yml b/tests/e2e/docker-compose.yml new file mode 100644 index 000000000..6076e22f8 --- /dev/null +++ b/tests/e2e/docker-compose.yml @@ -0,0 +1,58 @@ +version: "2" + +services: + frontend: + container_name: ${COMPOSE_PROJECT_NAME}-frontend + image: nginx:stable-alpine + ports: + - "4444:4444" # browser webdriver + - "6379:6379" # coturn redis + - "6565:6565" # medea grpc + - "8000:8000" # control-api-mock http + - "8001:8001" # medea http (ws) + - "30000:30000" # frontend http + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + - ./index.html:/usr/share/nginx/html/index.html:ro + - ../../jason/pkg/:/usr/share/nginx/html/pkg/:ro + medea: + container_name: ${COMPOSE_PROJECT_NAME}-medea + image: ${COMPOSE_IMAGE_NAME}:${COMPOSE_IMAGE_VER} + depends_on: ["coturn"] + environment: + RUST_LOG: ${RUST_LOG} + MEDEA_CONF: ${MEDEA_CONF} + MEDEA_SERVER__CLIENT__HTTP__BIND_PORT: 8001 + MEDEA_CONTROL__STATIC_SPECS_DIR: ${MEDEA_CONTROL__STATIC_SPECS_DIR} + volumes: + - ../../${MEDEA_CONF}:/${MEDEA_CONF}:ro + - ../../${MEDEA_CONTROL__STATIC_SPECS_DIR}:/${MEDEA_CONTROL__STATIC_SPECS_DIR}:ro + network_mode: service:frontend + coturn: + container_name: ${COMPOSE_PROJECT_NAME}-coturn + image: instrumentisto/coturn:4.5 + depends_on: ["coturn-db"] + command: + - --log-file=stdout + volumes: + - ../../_dev/coturn/turnserver.conf:/etc/coturn/turnserver.conf:ro + - ../../.cache/coturn/data/:/var/lib/coturn/ + network_mode: service:frontend + coturn-db: + container_name: ${COMPOSE_PROJECT_NAME}-coturn-db + image: redis:alpine + command: ["redis-server", "/etc/redis.conf"] + volumes: + - ../../_dev/coturn/redis.conf:/etc/redis.conf:ro + network_mode: service:frontend + control-api-mock: + container_name: ${COMPOSE_PROJECT_NAME}-control-api-mock + image: ${COMPOSE_CONTROL_MOCK_IMAGE_NAME}:${COMPOSE_CONTROL_MOCK_IMAGE_VER} + depends_on: ["medea"] + network_mode: service:frontend + webdriver: + container_name: ${COMPOSE_PROJECT_NAME}-webdriver + image: ${COMPOSE_WEBDRIVER_IMAGE_NAME}:${COMPOSE_WEBDRIVER_IMAGE_VER} + depends_on: ["control-api-mock", "coturn", "frontend", "medea"] + shm_size: 512m + network_mode: service:frontend diff --git a/tests/e2e/features/on_new_connection_fires.feature b/tests/e2e/features/on_new_connection_fires.feature new file mode 100644 index 000000000..beb60ebce --- /dev/null +++ b/tests/e2e/features/on_new_connection_fires.feature @@ -0,0 +1,22 @@ +Feature: `on_new_connection` callback + + Scenario: Member joined with enabled media + Given room with joined member Alice + And member Bob + When Bob joins the room + Then Alice receives connection with Bob + And Bob receives connection with Alice + + Scenario: Member joined with disabled media + Given room with member Alice with disabled media publishing + And joined member Bob + When Alice joins the room + Then Alice receives connection with Bob + And Bob receives connection with Alice + + Scenario: Member joined without WebRTC endpoints + Given room with member Alice with no WebRTC endpoints + And joined member Bob with no WebRTC endpoints + When Alice joins the room + Then Alice doesn't receive connection with Bob + And Bob doesn't receive connection with Alice diff --git a/tests/e2e/index.html b/tests/e2e/index.html new file mode 100644 index 000000000..a06ba9407 --- /dev/null +++ b/tests/e2e/index.html @@ -0,0 +1,18 @@ + + + + Medea's E2E test + + + + diff --git a/tests/e2e/main.rs b/tests/e2e/main.rs index 607f7e8bb..a42745caf 100644 --- a/tests/e2e/main.rs +++ b/tests/e2e/main.rs @@ -1,90 +1,17 @@ -#![allow(clippy::module_name_repetitions)] +mod browser; +mod conf; +mod control; +mod object; +mod steps; +mod world; -mod callbacks; -mod grpc_control_api; -pub mod signalling; +use cucumber_rust::WorldInit as _; +use tokio_1 as tokio; -/// Polls `$name` [`Stream`] until finds provided `$pattern`. -/// -/// When provided `$pattern` found - executes provided `$body`. -/// -/// This macro can be used only in the `async` blocks. -/// -/// # Usage -/// -/// ```ignore -/// enum Foo { -/// A(u32), -/// B(u32), -/// } -/// -/// let (tx, mut rx) = mpsc::unbounded(); -/// tx.unbounded_send(Foo::A(1)); -/// tx.unbounded_send(Foo::B(1)); -/// tx.unbounded_send(Foo::B(2)); -/// -/// if_let_next! { -/// Foo::B(i) = rx { -/// assert_eq!(i, 1); -/// } -/// } -/// assert_eq!(rx.next().await.unwrap(), Foo::B(2)); -/// ``` -#[macro_export] -macro_rules! if_let_next { - ($pattern:pat = $name:ident $body:block ) => { - loop { - if let $pattern = $name.select_next_some().await { - $body; - break; - } - } - }; -} - -/// Equality comparisons for the enum variants. -/// -/// This macro will ignore all content of the enum, it just compare -/// enum variants not they data. -#[macro_export] -macro_rules! enum_eq { - ($e:path, $val:ident) => { - if let $e { .. } = $val { - true - } else { - false - } - }; -} +use self::world::World; -/// Expands to the [`module_path`] and function name, but `::` replaced with -/// `__`. -/// -/// Can be used only with [`function_name::named`] macro. -/// -/// # Example -/// -/// ``` -/// use function_name::named; -/// -/// use crate::test_name; -/// -/// mod foo { -/// mod bar { -/// #[named] -/// fn baz() { -/// assert_eq!(test_name!(), "e2e__foo__bar__baz"); -/// } -/// } -/// } -/// -/// foo::bar::baz(); -/// ``` -#[macro_export] -macro_rules! test_name { - () => { - concat!(module_path!(), "::", function_name!()) - .replace("::", "__") - .as_str() - }; +#[tokio::main] +async fn main() { + let runner = World::init(&[conf::FEATURES_PATH.as_str()]); + runner.run_and_exit().await; } diff --git a/tests/e2e/nginx.conf b/tests/e2e/nginx.conf new file mode 100644 index 000000000..fbe99e6d6 --- /dev/null +++ b/tests/e2e/nginx.conf @@ -0,0 +1,27 @@ +worker_processes 1; +events { + worker_connections 1024; +} + +http { + default_type application/octet-stream; + sendfile on; + + keepalive_timeout 65; + + server { + listen 30000; + server_name localhost; + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + + types { + application/javascript js; + application/wasm wasm; + text/html html; + } + } + } +} diff --git a/tests/e2e/object/connection.rs b/tests/e2e/object/connection.rs new file mode 100644 index 000000000..97f5ab65a --- /dev/null +++ b/tests/e2e/object/connection.rs @@ -0,0 +1,2 @@ +/// Representation of the `Connection` JS object. +pub struct Connection; diff --git a/tests/e2e/object/connections_store.rs b/tests/e2e/object/connections_store.rs new file mode 100644 index 000000000..4bf0d7bbd --- /dev/null +++ b/tests/e2e/object/connections_store.rs @@ -0,0 +1,63 @@ +//! [`Object`] storing all the [`Connection`]s thrown by +//! `Room.on_new_connection` callback. + +use crate::{ + browser::Statement, + object::{connection::Connection, Object}, +}; + +/// Storage for [`Connection`]s thrown by `Room.on_new_connection` callback. +pub struct ConnectionStore; + +impl Object { + /// Returns a [`Connection`] of the provided remote member. + /// + /// Returns [`None`] if a [`Connection`] with the provided remote member + /// doesn't exist. + pub async fn get( + &self, + remote_id: String, + ) -> Result>, super::Error> { + let connection = self + .execute_and_fetch(Statement::new( + // language=JavaScript + r#" + async (store) => { + const [id] = args; + return store.connections.get(id); + } + "#, + [remote_id.into()], + )) + .await?; + + Ok((!connection.is_undefined().await?).then(|| connection)) + } + + /// Returns a [`Connection`] for the provided remote member, waiting it if + /// it doesn't exists at the moment. + pub async fn wait_for_connection( + &self, + remote_id: String, + ) -> Result, super::Error> { + self.execute_and_fetch(Statement::new( + // language=JavaScript + r#" + async (store) => { + const [remoteId] = args; + let conn = store.connections.get(remoteId); + if (conn !== undefined) { + return conn; + } else { + let waiter = new Promise((resolve) => { + store.subs.set(remoteId, resolve); + }); + return await waiter; + } + } + "#, + [remote_id.into()], + )) + .await + } +} diff --git a/tests/e2e/object/jason.rs b/tests/e2e/object/jason.rs new file mode 100644 index 000000000..b083f35fb --- /dev/null +++ b/tests/e2e/object/jason.rs @@ -0,0 +1,39 @@ +//! [`Object`] representing a `Jason` JS object. + +use crate::{ + browser::Statement, + object::{room::Room, Builder, Object}, +}; + +/// Representation of a `Jason` JS object. +pub struct Jason; + +impl Builder for Jason { + #[inline] + fn build(self) -> Statement { + Statement::new( + // language=JavaScript + r#"async () => new window.rust.Jason()"#, + [], + ) + } +} + +impl Object { + /// Returns a new [`Room`] initiated in this [`Jason`] [`Object`]. + pub async fn init_room(&self) -> Result, super::Error> { + self.execute_and_fetch(Statement::new( + // language=JavaScript + r#" + async (jason) => { + let room = await jason.init_room(); + room.on_failed_local_media(() => {}); + room.on_connection_loss(() => {}); + return room; + } + "#, + [], + )) + .await + } +} diff --git a/tests/e2e/object/mod.rs b/tests/e2e/object/mod.rs new file mode 100644 index 000000000..85d083488 --- /dev/null +++ b/tests/e2e/object/mod.rs @@ -0,0 +1,184 @@ +//! Browser-side objects. + +pub mod connection; +pub mod connections_store; +mod jason; +mod room; + +use std::{marker::PhantomData, sync::mpsc}; + +use derive_more::{Display, Error, From}; +use serde_json::Value as Json; +use tokio_1::{self as tokio, task}; +use uuid::Uuid; + +use crate::browser::{self, Statement}; + +pub use self::{ + jason::Jason, + room::{MediaKind, MediaSourceKind, Room}, +}; + +/// All errors which can happen while working with [`Object`]s. +#[derive(Debug, Display, Error, From)] +pub enum Error { + /// Error while interacting with a browser. + Browser(browser::Error), + + /// Failed JS object type casting. + TypeCast, +} + +/// Pointer to a JS object on a browser's side. +#[derive(Clone, Debug, Display)] +pub struct ObjectPtr(String); + +/// Representation of some JS object on a browser's side. +/// +/// JS object on browser's side will be removed on this [`Object`]'s [`Drop`]. +pub struct Object { + /// Pointer to the JS object on a browser's side. + ptr: ObjectPtr, + + /// [`browser::Window`] where this [`Object`] exists. + window: browser::Window, + + /// Type of this [`Object`]. + _type: PhantomData, +} + +impl Drop for Object { + /// Removes this [`Object`] on a browser's side. + fn drop(&mut self) { + let ptr = self.ptr.clone(); + let window = self.window.clone(); + let (tx, rx) = mpsc::channel(); + tokio::spawn(async move { + window + .execute(Statement::new( + // language=JavaScript + r#" + async () => { + const [id] = args; + window.registry.delete(id); + } + "#, + [ptr.to_string().into()], + )) + .await + .unwrap(); + tx.send(()).unwrap(); + }); + task::block_in_place(move || { + rx.recv().unwrap(); + }); + } +} + +impl Object { + /// Returns a new [`Object`] with the provided ID and [`browser::Window`]. + #[inline] + #[must_use] + pub fn new(id: String, window: browser::Window) -> Self { + Self { + ptr: ObjectPtr(id), + window, + _type: PhantomData, + } + } + + /// Executes the provided [`Statement`] and returns the resulting + /// [`Object`]. + pub async fn execute_and_fetch( + &self, + statement: Statement, + ) -> Result, Error> { + let id = Uuid::new_v4().to_string(); + self.execute(statement.and_then(Statement::new( + // language=JavaScript + r#" + async (obj) => { + const [id] = args; + window.registry.set(id, obj); + } + "#, + [id.clone().into()], + ))) + .await?; + + Ok(Object::new(id, self.window.clone())) + } + + /// Indicates whether this [`Object`] is `undefined`. + pub async fn is_undefined(&self) -> Result { + self.execute(Statement::new( + // language=JavaScript + r#" + async (o) => { + return o === undefined; + } + "#, + [], + )) + .await? + .as_bool() + .ok_or(Error::TypeCast) + } + + /// Executes the provided [`Statement`] in a browser. + /// + /// JS object representing this [`Object`] will be passed to the provided + /// [`Statement`] as a lambda argument. + #[inline] + async fn execute(&self, js: Statement) -> Result { + self.window + .execute(self.get_obj().and_then(js)) + .await + .map_err(Error::Browser) + } + + /// Returns a [`Statement`] obtaining JS object of this [`Object`]. + fn get_obj(&self) -> Statement { + Statement::new( + // language=JavaScript + r#" + async () => { + const [id] = args; + return window.registry.get(id); + } + "#, + [self.ptr.to_string().into()], + ) + } +} + +impl Object { + /// Spawns the provided [`Object`] in the provided [`browser::Window`]. + pub async fn spawn( + obj: T, + window: browser::Window, + ) -> Result, Error> { + let id = Uuid::new_v4().to_string(); + window + .execute(obj.build().and_then(Statement::new( + // language=JavaScript + r#" + async (obj) => { + const [id] = args; + window.registry.set(id, obj); + } + "#, + [id.clone().into()], + ))) + .await?; + + Ok(Object::new(id, window)) + } +} + +/// JS object builder for an [`Object`]. +pub trait Builder { + /// Returns a [`Statement`] creating a desired JS object. + #[must_use] + fn build(self) -> Statement; +} diff --git a/tests/e2e/object/room.rs b/tests/e2e/object/room.rs new file mode 100644 index 000000000..8a665f14f --- /dev/null +++ b/tests/e2e/object/room.rs @@ -0,0 +1,113 @@ +//! [`Object`] representing a `Room` JS object. + +use crate::{ + browser::Statement, + object::{connections_store::ConnectionStore, Object}, +}; + +/// Representation of a `Room` JS object. +pub struct Room; + +/// Representation of a `MediaKind` JS enum. +pub enum MediaKind { + Audio, + Video, +} + +/// Representation of a `MediaSourceKind` JS enum. +pub enum MediaSourceKind { + Device, + Display, +} + +impl MediaSourceKind { + /// Converts this [`MediaSourceKind`] to a JS code for this enum variant. + fn as_js(&self) -> String { + match self { + MediaSourceKind::Device => "window.rust.MediaSourceKind.DEVICE", + MediaSourceKind::Display => "window.rust.MediaSourceKind.DISPLAY", + } + .to_owned() + } +} + +impl Object { + /// Joins a [`Room`] with the provided URI. + pub async fn join(&self, uri: String) -> Result<(), super::Error> { + self.execute(Statement::new( + // language=JavaScript + r#" + async (room) => { + const [uri] = args; + await room.join(uri); + } + "#, + [uri.into()], + )) + .await?; + Ok(()) + } + + /// Disables media publishing for the provided [`MediaKind`] and + /// [`MediaSourceKind`]. + /// + /// If the provided `source_kind` is [`None`], then media publishing will be + /// disabled for all [`MediaSourceKind`]s. + pub async fn disable_media_send( + &self, + kind: MediaKind, + source_kind: Option, + ) -> Result<(), super::Error> { + let media_source_kind = source_kind + .as_ref() + .map_or_else(String::new, MediaSourceKind::as_js); + let disable = match kind { + MediaKind::Audio => "room.disable_audio()".to_owned(), + MediaKind::Video => { + format!("room.disable_video({})", media_source_kind) + } + }; + self.execute(Statement::new( + // language=JavaScript + &format!( + r#" + async (room) => {{ + await {}; + }} + "#, + disable, + ), + [], + )) + .await?; + Ok(()) + } + + /// Returns a [`ConnectionStore`] of this [`Room`]. + pub async fn connections_store( + &self, + ) -> Result, super::Error> { + self.execute_and_fetch(Statement::new( + // language=JavaScript + r#" + async (room) => { + let store = { + connections: new Map(), + subs: new Map() + }; + room.on_new_connection((conn) => { + let id = conn.get_remote_member_id(); + store.connections.set(id, conn); + let sub = store.subs.get(id); + if (sub !== undefined) { + sub(conn); + } + }); + return store; + } + "#, + [], + )) + .await + } +} diff --git a/tests/e2e/steps/connection.rs b/tests/e2e/steps/connection.rs new file mode 100644 index 000000000..030ac5dd8 --- /dev/null +++ b/tests/e2e/steps/connection.rs @@ -0,0 +1,33 @@ +use cucumber_rust::then; + +use crate::world::World; + +#[then(regex = r"^(\S+) receives connection with (\S+)$")] +async fn then_member_receives_connection( + world: &mut World, + id: String, + responder_id: String, +) { + let member = world.get_member(&id).unwrap(); + member + .connections() + .wait_for_connection(responder_id.clone()) + .await + .unwrap(); + assert!(true); +} + +#[then(regex = r"^(\S+) doesn't receive connection with (\S+)$")] +async fn then_member_doesnt_receive_connection( + world: &mut World, + id: String, + responder_id: String, +) { + let member = world.get_member(&id).unwrap(); + assert!(member + .connections() + .get(responder_id) + .await + .unwrap() + .is_none()) +} diff --git a/tests/e2e/steps/media_state.rs b/tests/e2e/steps/media_state.rs new file mode 100644 index 000000000..fa0e8a75f --- /dev/null +++ b/tests/e2e/steps/media_state.rs @@ -0,0 +1,31 @@ +use cucumber_rust::when; + +use crate::{object::MediaKind, world::World}; + +use super::parse_media_kind; + +#[when(regex = r"^Member (\S+) (disables|mutes) (audio|video|all)$")] +async fn when_disables_mutes( + world: &mut World, + id: String, + disable_or_mutes: String, + audio_or_video: String, +) { + let member = world.get_member(&id).unwrap(); + if disable_or_mutes == "disables" { + if let Some(kind) = parse_media_kind(&audio_or_video) { + member.disable_media_send(kind, None).await.unwrap(); + } else { + member + .disable_media_send(MediaKind::Audio, None) + .await + .unwrap(); + member + .disable_media_send(MediaKind::Video, None) + .await + .unwrap(); + } + } else { + todo!("Muting is unimplemented atm.") + } +} diff --git a/tests/e2e/steps/mod.rs b/tests/e2e/steps/mod.rs new file mode 100644 index 000000000..46b2411a4 --- /dev/null +++ b/tests/e2e/steps/mod.rs @@ -0,0 +1,198 @@ +mod connection; +mod media_state; + +use std::{convert::Infallible, str::FromStr}; + +use async_recursion::async_recursion; +use cucumber_rust::{given, when}; + +use crate::{ + object::MediaKind, + world::{MemberBuilder, World}, +}; + +#[given(regex = "^(?:room with )?(joined )?member(?:s)? (\\S+)\ + (?:(?:, | and )(\\S+)(?: and (\\S+)?)?)?\ + (?: with (no (play |publish )?WebRTC endpoints\ + |(?:disabled|muted) (media|audio|video) \ + (publishing|playing)?))?$")] +#[async_recursion] +async fn new_given_member( + world: &mut World, + joined: Matched, + first_member_id: String, + second_member_id: String, + third_member_id: String, + media_settings: MediaSettings, + not_endpoint_direction: Direction, + disabled_media_type: DisabledMediaType, + disabled_direction: Direction, +) { + let endpoints_disabled = media_settings == MediaSettings::NoWebRtcEndpoint; + let all_endpoints_disabled = + endpoints_disabled && not_endpoint_direction == Direction::None; + let is_send_disabled = endpoints_disabled + && (all_endpoints_disabled + || not_endpoint_direction == Direction::Publish); + let is_recv_disabled = endpoints_disabled + && (all_endpoints_disabled + || not_endpoint_direction == Direction::Play); + + let member_builder = MemberBuilder { + id: first_member_id.clone(), + is_send: !is_send_disabled, + is_recv: !is_recv_disabled, + }; + world.create_member(member_builder).await.unwrap(); + if joined.0 { + world.join_room(&first_member_id).await.unwrap(); + world + .wait_for_interconnection(&first_member_id) + .await + .unwrap(); + } + + let member = world.get_member(&first_member_id).unwrap(); + match media_settings { + MediaSettings::DisabledMedia => { + let is_audio = !(disabled_media_type != DisabledMediaType::Audio); + let is_video = !(disabled_media_type != DisabledMediaType::Video); + let is_publish = !(disabled_direction != Direction::Publish); + let is_play = !(disabled_direction != Direction::Play); + + if is_publish { + if is_audio { + member + .disable_media_send(MediaKind::Audio, None) + .await + .unwrap(); + } + if is_video { + member + .disable_media_send(MediaKind::Video, None) + .await + .unwrap(); + } + } + if is_play { + todo!("Play disabling is not implemented atm"); + } + } + MediaSettings::MutedMedia => { + todo!("Muting is not implemented atm"); + } + _ => (), + } + + if !second_member_id.is_empty() { + new_given_member( + world, + joined, + second_member_id, + third_member_id, + String::new(), + media_settings, + not_endpoint_direction, + disabled_media_type, + disabled_direction, + ) + .await; + } +} + +#[when(regex = r"^(\S+) joins the room$")] +async fn when_member_joins_room(world: &mut World, id: String) { + world.join_room(&id).await.unwrap(); +} + +/// Parses [`MediaKind`] from the given `step` description match. +#[must_use] +fn parse_media_kind(step: &str) -> Option { + match step { + "audio" => Some(MediaKind::Audio), + "video" => Some(MediaKind::Video), + "all" => None, + _ => unreachable!(), + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct Matched(pub bool); + +impl FromStr for Matched { + type Err = Infallible; + + #[inline] + fn from_str(s: &str) -> Result { + Ok(Self(!s.is_empty())) + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum MediaSettings { + DisabledMedia, + MutedMedia, + NoWebRtcEndpoint, + None, +} + +impl FromStr for MediaSettings { + type Err = Infallible; + + fn from_str(s: &str) -> Result { + Ok(if s.contains("no WebRTC endpoints") { + Self::NoWebRtcEndpoint + } else if s.contains("disabled") { + Self::DisabledMedia + } else if s.contains("muted") { + Self::MutedMedia + } else { + Self::None + }) + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum DisabledMediaType { + Audio, + Video, + All, + None, +} + +impl FromStr for DisabledMediaType { + type Err = Infallible; + + fn from_str(s: &str) -> Result { + Ok(if s.contains("audio") { + Self::Audio + } else if s.contains("video") { + Self::Video + } else if s.contains("media") { + Self::All + } else { + Self::None + }) + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum Direction { + Publish, + Play, + None, +} + +impl FromStr for Direction { + type Err = Infallible; + + fn from_str(s: &str) -> Result { + Ok(if s.contains("publishing") { + Self::Publish + } else if s.contains("playing") { + Self::Play + } else { + Self::None + }) + } +} diff --git a/tests/e2e/world/member.rs b/tests/e2e/world/member.rs new file mode 100644 index 000000000..84e6390e7 --- /dev/null +++ b/tests/e2e/world/member.rs @@ -0,0 +1,144 @@ +//! Medea media server member representation. + +use derive_more::{Display, Error, From}; + +use crate::{ + conf, + object::{ + self, connections_store::ConnectionStore, MediaKind, MediaSourceKind, + Object, Room, + }, +}; + +/// All errors which can happen while working with a [`Member`]. +#[derive(Debug, Display, Error, From)] +pub enum Error { + /// [`Room`] or a [`ConnectionStore`] object errored. + Object(object::Error), +} + +/// Shortcut for a [`Result`] containing an [`Error`](enum@Error). +/// +/// [`Result`]: std::result::Result +type Result = std::result::Result; + +/// Builder of a [`Member`]. +pub struct Builder { + /// ID with which a [`Member`] will be created. + pub id: String, + + /// Indicator whether a [`Member`] will publish media. + pub is_send: bool, + + /// Indicator whether a [`Member`] will receive media. + pub is_recv: bool, +} + +impl Builder { + /// Creates a new [`Member`] out of this [`Builder`] configuration. + pub async fn build(self, room: Object) -> Result { + let connection_store = room.connections_store().await?; + Ok(Member { + id: self.id, + is_send: self.is_send, + is_recv: self.is_recv, + is_joined: false, + room, + connection_store, + }) + } +} + +/// [`Object`] representing a `Member` connected to a media server. +pub struct Member { + /// ID of this [`Member`] on a media server. + id: String, + + /// Indicator whether this [`Member`] should publish media. + is_send: bool, + + /// Indicator whether this [`Member`] should receive media. + is_recv: bool, + + /// Indicator whether this [`Member`] is joined a [`Room`] on a media + /// server. + is_joined: bool, + + /// [`Room`]'s [`Object`] that this [`Member`] is intended to join. + room: Object, + + /// Storage of [`Connection`]s thrown by this [`Member`]'s [`Room`]. + /// + /// [`Connection`]: object::connection::Connection + connection_store: Object, +} + +impl Member { + /// Returns ID of this [`Member`] on a media server. + #[inline] + #[must_use] + pub fn id(&self) -> &str { + &self.id + } + + /// Indicates whether this [`Member`] should publish media. + #[inline] + #[must_use] + pub fn is_send(&self) -> bool { + self.is_send + } + + /// Indicator whether this [`Member`] should receive media. + #[inline] + #[must_use] + pub fn is_recv(&self) -> bool { + self.is_recv + } + + /// Indicates whether this [`Member`] is joined a [`Room`] on a media + /// server. + #[inline] + #[must_use] + pub fn is_joined(&self) -> bool { + self.is_joined + } + + /// Joins a [`Room`] with the provided ID. + pub async fn join_room(&mut self, room_id: &str) -> Result<()> { + self.room + .join(format!( + "{}/{}/{}?token=test", + *conf::CLIENT_API_ADDR, + room_id, + self.id, + )) + .await?; + self.is_joined = true; + Ok(()) + } + + /// Disables media publishing for the provided [`MediaKind`] and + /// [`MediaSourceKind`]. + /// + /// If the provided `source_kind` is [`None`], then media publishing will be + /// disabled for all [`MediaSourceKind`]s. + #[inline] + pub async fn disable_media_send( + &self, + kind: MediaKind, + source_kind: Option, + ) -> Result<()> { + self.room.disable_media_send(kind, source_kind).await?; + Ok(()) + } + + /// Returns reference to the Storage of [`Connection`]s thrown by this + /// [`Member`]'s [`Room`]. + /// + /// [`Connection`]: object::connection::Connection + #[inline] + #[must_use] + pub fn connections(&self) -> &Object { + &self.connection_store + } +} diff --git a/tests/e2e/world/mod.rs b/tests/e2e/world/mod.rs new file mode 100644 index 000000000..7b4932e6f --- /dev/null +++ b/tests/e2e/world/mod.rs @@ -0,0 +1,236 @@ +//! E2E tests [`World`][1]. +//! +//! [1]: cucumber_rust::World + +pub mod member; + +use std::collections::HashMap; + +use async_trait::async_trait; +use cucumber_rust::WorldInit; +use derive_more::{Display, Error, From}; +use medea_control_api_mock::proto; +use uuid::Uuid; + +use crate::{ + browser::{self, WindowFactory}, + control, + object::{self, Jason, Object}, +}; + +pub use self::member::{Builder as MemberBuilder, Member}; + +/// All errors which can happen while working with [`World`]. +#[derive(Debug, Display, Error, From)] +pub enum Error { + Control(control::Error), + Object(object::Error), + Member(member::Error), + Browser(browser::Error), + MemberNotFound(#[error(not(source))] String), +} + +type Result = std::result::Result; + +/// [`World`][1] used by all E2E tests. +/// +/// [1]: cucumber_rust::World +#[derive(WorldInit)] +pub struct World { + /// ID of the `Room` created for this [`World`]. + room_id: String, + + /// Client of a Medea Control API. + control_client: control::Client, + + /// All [`Member`]s created in this [`World`]. + members: HashMap, + + /// All [`Jason`] [`Object`]s created in this [`World`]. + jasons: Vec>, + + /// [WebDriver] client that all [`Object`]s of this [`World`] will be + /// created with. + /// + /// [WebDriver]: https://w3.org/TR/webdriver + window_factory: WindowFactory, +} + +#[async_trait(?Send)] +impl cucumber_rust::World for World { + type Error = Error; + + async fn new() -> Result { + let room_id = Uuid::new_v4().to_string(); + + let control_client = control::Client::new(); + control_client + .create( + &room_id, + proto::Element::Room(proto::Room { + id: room_id.clone(), + pipeline: HashMap::new(), + }), + ) + .await?; + + Ok(Self { + room_id, + control_client, + window_factory: WindowFactory::new().await?, + members: HashMap::new(), + jasons: Vec::new(), + }) + } +} + +impl World { + /// Creates a new [`Member`] from the provided [`MemberBuilder`]. + /// + /// `Room` for this [`Member`] will be created, but joining won't be done. + pub async fn create_member( + &mut self, + builder: MemberBuilder, + ) -> Result<()> { + let mut pipeline = HashMap::new(); + if builder.is_send { + pipeline.insert( + "publish".to_string(), + proto::Endpoint::WebRtcPublishEndpoint( + proto::WebRtcPublishEndpoint { + id: "publish".to_string(), + p2p: proto::P2pMode::Always, + force_relay: false, + audio_settings: proto::AudioSettings::default(), + video_settings: proto::VideoSettings::default(), + }, + ), + ); + } + if builder.is_recv { + self.members.values().filter(|m| m.is_send()).for_each(|m| { + let endpoint_id = format!("play-{}", m.id()); + pipeline.insert( + endpoint_id.clone(), + proto::Endpoint::WebRtcPlayEndpoint( + proto::WebRtcPlayEndpoint { + id: endpoint_id, + src: format!( + "local://{}/{}/publish", + self.room_id, + m.id(), + ), + force_relay: false, + }, + ), + ); + }); + } + + self.control_client + .create( + &format!("{}/{}", self.room_id, builder.id), + proto::Element::Member(proto::Member { + id: builder.id.clone(), + pipeline, + credentials: Some(proto::Credentials::Plain( + "test".to_string(), + )), + on_join: None, + on_leave: None, + idle_timeout: None, + reconnect_timeout: None, + ping_interval: None, + }), + ) + .await?; + + if builder.is_send { + let recv_endpoints: HashMap<_, _> = self + .members + .values() + .filter_map(|m| { + m.is_recv().then(|| { + let endpoint_id = format!("play-{}", builder.id); + let id = format!( + "{}/{}/{}", + self.room_id, + m.id(), + endpoint_id, + ); + let elem = proto::Element::WebRtcPlayEndpoint( + proto::WebRtcPlayEndpoint { + id: endpoint_id, + src: format!( + "local://{}/{}/publish", + self.room_id, builder.id, + ), + force_relay: false, + }, + ); + (id, elem) + }) + }) + .collect(); + for (path, element) in recv_endpoints { + self.control_client.create(&path, element).await?; + } + } + let jason = + Object::spawn(Jason, self.window_factory.new_window().await) + .await?; + let room = jason.init_room().await?; + let member = builder.build(room).await?; + + self.members.insert(member.id().to_string(), member); + self.jasons.push(jason); + Ok(()) + } + + /// Returns reference to a [`Member`] with the provided ID. + /// + /// Returns [`None`] if a [`Member`] with the provided ID doesn't exist. + #[inline] + #[must_use] + pub fn get_member(&self, member_id: &str) -> Option<&Member> { + self.members.get(member_id) + } + + /// Joins a [`Member`] with the provided ID to the `Room` created for this + /// [`World`]. + pub async fn join_room(&mut self, member_id: &str) -> Result<()> { + let member = self + .members + .get_mut(member_id) + .ok_or_else(|| Error::MemberNotFound(member_id.to_string()))?; + member.join_room(&self.room_id).await?; + Ok(()) + } + + /// Waits until a [`Member`] with the provided ID will connect with his + /// responders. + pub async fn wait_for_interconnection( + &mut self, + member_id: &str, + ) -> Result<()> { + let interconnected_members: Vec<_> = self + .members + .values() + .filter_map(|m| { + (m.is_joined() + && m.id() != member_id + && (m.is_recv() || m.is_send())) + .then(|| m.id().to_string()) + }) + .collect(); + let member = self + .members + .get_mut(member_id) + .ok_or_else(|| Error::MemberNotFound(member_id.to_string()))?; + let connections = member.connections(); + for id in interconnected_members { + connections.wait_for_connection(id).await?; + } + Ok(()) + } +} diff --git a/tests/e2e/callbacks/member.rs b/tests/integration/callbacks/member.rs similarity index 100% rename from tests/e2e/callbacks/member.rs rename to tests/integration/callbacks/member.rs diff --git a/tests/e2e/callbacks/mod.rs b/tests/integration/callbacks/mod.rs similarity index 100% rename from tests/e2e/callbacks/mod.rs rename to tests/integration/callbacks/mod.rs diff --git a/tests/e2e/grpc_control_api/create.rs b/tests/integration/grpc_control_api/create.rs similarity index 100% rename from tests/e2e/grpc_control_api/create.rs rename to tests/integration/grpc_control_api/create.rs diff --git a/tests/e2e/grpc_control_api/credentials.rs b/tests/integration/grpc_control_api/credentials.rs similarity index 100% rename from tests/e2e/grpc_control_api/credentials.rs rename to tests/integration/grpc_control_api/credentials.rs diff --git a/tests/e2e/grpc_control_api/delete.rs b/tests/integration/grpc_control_api/delete.rs similarity index 100% rename from tests/e2e/grpc_control_api/delete.rs rename to tests/integration/grpc_control_api/delete.rs diff --git a/tests/e2e/grpc_control_api/mod.rs b/tests/integration/grpc_control_api/mod.rs similarity index 100% rename from tests/e2e/grpc_control_api/mod.rs rename to tests/integration/grpc_control_api/mod.rs diff --git a/tests/e2e/grpc_control_api/rpc_settings.rs b/tests/integration/grpc_control_api/rpc_settings.rs similarity index 100% rename from tests/e2e/grpc_control_api/rpc_settings.rs rename to tests/integration/grpc_control_api/rpc_settings.rs diff --git a/tests/e2e/grpc_control_api/signaling.rs b/tests/integration/grpc_control_api/signaling.rs similarity index 100% rename from tests/e2e/grpc_control_api/signaling.rs rename to tests/integration/grpc_control_api/signaling.rs diff --git a/tests/integration/main.rs b/tests/integration/main.rs new file mode 100644 index 000000000..607f7e8bb --- /dev/null +++ b/tests/integration/main.rs @@ -0,0 +1,90 @@ +#![allow(clippy::module_name_repetitions)] + +mod callbacks; +mod grpc_control_api; +pub mod signalling; + +/// Polls `$name` [`Stream`] until finds provided `$pattern`. +/// +/// When provided `$pattern` found - executes provided `$body`. +/// +/// This macro can be used only in the `async` blocks. +/// +/// # Usage +/// +/// ```ignore +/// enum Foo { +/// A(u32), +/// B(u32), +/// } +/// +/// let (tx, mut rx) = mpsc::unbounded(); +/// tx.unbounded_send(Foo::A(1)); +/// tx.unbounded_send(Foo::B(1)); +/// tx.unbounded_send(Foo::B(2)); +/// +/// if_let_next! { +/// Foo::B(i) = rx { +/// assert_eq!(i, 1); +/// } +/// } +/// assert_eq!(rx.next().await.unwrap(), Foo::B(2)); +/// ``` +#[macro_export] +macro_rules! if_let_next { + ($pattern:pat = $name:ident $body:block ) => { + loop { + if let $pattern = $name.select_next_some().await { + $body; + break; + } + } + }; +} + +/// Equality comparisons for the enum variants. +/// +/// This macro will ignore all content of the enum, it just compare +/// enum variants not they data. +#[macro_export] +macro_rules! enum_eq { + ($e:path, $val:ident) => { + if let $e { .. } = $val { + true + } else { + false + } + }; +} + +/// Expands to the [`module_path`] and function name, but `::` replaced with +/// `__`. +/// +/// Can be used only with [`function_name::named`] macro. +/// +/// # Example +/// +/// ``` +/// use function_name::named; +/// +/// use crate::test_name; +/// +/// mod foo { +/// mod bar { +/// #[named] +/// fn baz() { +/// assert_eq!(test_name!(), "e2e__foo__bar__baz"); +/// } +/// } +/// } +/// +/// foo::bar::baz(); +/// ``` +#[macro_export] +macro_rules! test_name { + () => { + concat!(module_path!(), "::", function_name!()) + .replace("::", "__") + .as_str() + }; +} diff --git a/tests/e2e/signalling/add_endpoints_synchronization.rs b/tests/integration/signalling/add_endpoints_synchronization.rs similarity index 100% rename from tests/e2e/signalling/add_endpoints_synchronization.rs rename to tests/integration/signalling/add_endpoints_synchronization.rs diff --git a/tests/e2e/signalling/command_validation.rs b/tests/integration/signalling/command_validation.rs similarity index 100% rename from tests/e2e/signalling/command_validation.rs rename to tests/integration/signalling/command_validation.rs diff --git a/tests/e2e/signalling/ice_restart.rs b/tests/integration/signalling/ice_restart.rs similarity index 100% rename from tests/e2e/signalling/ice_restart.rs rename to tests/integration/signalling/ice_restart.rs diff --git a/tests/e2e/signalling/mod.rs b/tests/integration/signalling/mod.rs similarity index 100% rename from tests/e2e/signalling/mod.rs rename to tests/integration/signalling/mod.rs diff --git a/tests/e2e/signalling/pub_sub_signallng.rs b/tests/integration/signalling/pub_sub_signallng.rs similarity index 100% rename from tests/e2e/signalling/pub_sub_signallng.rs rename to tests/integration/signalling/pub_sub_signallng.rs diff --git a/tests/e2e/signalling/rpc_settings.rs b/tests/integration/signalling/rpc_settings.rs similarity index 100% rename from tests/e2e/signalling/rpc_settings.rs rename to tests/integration/signalling/rpc_settings.rs diff --git a/tests/e2e/signalling/three_pubs.rs b/tests/integration/signalling/three_pubs.rs similarity index 100% rename from tests/e2e/signalling/three_pubs.rs rename to tests/integration/signalling/three_pubs.rs diff --git a/tests/e2e/signalling/track_disable.rs b/tests/integration/signalling/track_disable.rs similarity index 100% rename from tests/e2e/signalling/track_disable.rs rename to tests/integration/signalling/track_disable.rs diff --git a/tests/e2e/signalling/track_mute.rs b/tests/integration/signalling/track_mute.rs similarity index 100% rename from tests/e2e/signalling/track_mute.rs rename to tests/integration/signalling/track_mute.rs