diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b4fcb50fb..87728ad05 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,7 +39,7 @@ jobs: - uses: actions/cache@v3 name: Elixir Deps Cache env: - cache-name: cache-elixir-deps + cache-name: cache-elixir-deps-${{ env.MIX_ENV }} with: path: deps key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }} @@ -48,7 +48,7 @@ jobs: - uses: actions/cache@v3 name: Elixir Build Cache env: - cache-name: cache-elixir-build + cache-name: cache-elixir-build-${{ env.MIX_ENV }} with: path: _build key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }} @@ -67,7 +67,7 @@ jobs: E2E_MAX_WAIT_SECONDS: 20 run: | # XXX: This can fail when coveralls is down - mix coveralls.github --umbrella + mix coveralls.github --umbrella --warnings-as-errors - name: Test Report uses: dorny/test-reporter@v1 if: success() || failure() @@ -75,6 +75,188 @@ jobs: name: Elixir Unit Test Report path: _build/test/lib/*/test-junit-report.xml reporter: java-junit + type-check: + runs-on: ubuntu-latest + env: + MIX_ENV: dev + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v3 + - uses: erlef/setup-beam@v1 + with: + otp-version: '25' + elixir-version: '1.14' + - uses: actions/cache@v3 + name: Elixir Deps Cache + env: + cache-name: cache-elixir-deps-${{ env.MIX_ENV }} + with: + path: deps + key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }} + restore-keys: | + ${{ runner.os }}-${{ env.cache-name }}- + - uses: actions/cache@v3 + name: Elixir Build Cache + env: + cache-name: cache-elixir-build-${{ env.MIX_ENV }} + with: + path: _build + key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }} + - name: Install Dependencies + run: mix deps.get --only $MIX_ENV + - name: Compile Dependencies + run: mix deps.compile --skip-umbrella-children + - name: Compile Application + run: mix compile + # Don't cache PLTs based on mix.lock hash, as Dialyzer can incrementally update even old ones + # Cache key based on Elixir & Erlang version (also useful when running in matrix) + - name: Restore PLT cache + uses: actions/cache@v3 + env: + cache-name: cache-erlang-plt-${{ env.MIX_ENV }} + with: + key: ${{ runner.os }}-${{ steps.setup-beam.outputs.elixir-version }}-${{ steps.setup-beam.outputs.otp-version }}-plt + restore-keys: | + ${{ runner.os }}-${{ steps.setup-beam.outputs.elixir-version }}-${{ steps.setup-beam.outputs.otp-version }}-plt + path: priv/plts + - name: Create PLTs + if: steps.plt_cache.outputs.cache-hit != 'true' + run: mix dialyzer --plt + - name: Run Dialyzer + run: mix dialyzer --format dialyxir + static-analysis: + runs-on: ubuntu-latest + env: + MIX_ENV: test + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v3 + - uses: erlef/setup-beam@v1 + with: + otp-version: '25' + elixir-version: '1.14' + - uses: actions/cache@v3 + name: Elixir Deps Cache + env: + cache-name: cache-elixir-deps-${{ env.MIX_ENV }} + with: + path: deps + key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }} + restore-keys: | + ${{ runner.os }}-${{ env.cache-name }}- + - uses: actions/cache@v3 + name: Elixir Build Cache + env: + cache-name: cache-elixir-build-${{ env.MIX_ENV }} + with: + path: _build + key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }} + - name: Install Dependencies + run: mix deps.get --only $MIX_ENV + - name: Compile Dependencies + run: mix deps.compile --skip-umbrella-children + - name: Compile Application + run: mix compile --force --warnings-as-errors + - name: Check Formatting + run: mix format --check-formatted + - name: Run Credo + run: mix credo --strict + migrations-and-seed-test: + runs-on: ubuntu-latest + env: + MIX_ENV: dev + POSTGRES_HOST: localhost + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + FZ_VPN_WG_ADAPTER: FzVpn.Interface.WGAdapter.Sandbox + services: + postgres: + image: postgres:15 + ports: + - 5432:5432 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: + - name: Install package dependencies + run: | + sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' + wget -qO- https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo tee /etc/apt/trusted.gpg.d/pgdg.asc &>/dev/null + sudo apt update + sudo apt-get install -q -y \ + net-tools \ + wireguard \ + postgresql-client + - uses: actions/checkout@v3 + - uses: erlef/setup-beam@v1 + with: + otp-version: '25' + elixir-version: '1.14' + - uses: actions/cache@v3 + name: Elixir Deps Cache + env: + cache-name: cache-elixir-deps-${{ env.MIX_ENV }}-${{ env.MIX_ENV }} + with: + path: deps + key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }} + restore-keys: | + ${{ runner.os }}-${{ env.cache-name }}- + - uses: actions/cache@v3 + name: Elixir Build Cache + env: + cache-name: cache-elixir-build-${{ env.MIX_ENV }} + with: + path: _build + key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }} + - name: Install Dependencies + run: mix deps.get --only $MIX_ENV + - name: Compile + run: mix compile + - name: Download main branch DB dump + id: download-artifact + uses: dawidd6/action-download-artifact@v2 + if: "!contains(github.ref, 'main')" + with: + branch: master + name: db-dump + path: apps/fz_http/priv/repo/ + search_artifacts: true + workflow_conclusion: completed + if_no_artifact_found: fail + - name: Create Database + run: | + mix ecto.create + - name: Restore DB dump + if: "!contains(github.ref, 'main')" + env: + PGPASSWORD: postgres + run: | + mix ecto.load + - name: Run new migrations + run: | + mix ecto.migrate + - name: Dump DB + if: "contains(github.ref, 'main')" + env: + PGPASSWORD: postgres + run: | + pg_dump firezone_dev \ + -U postgres -h localhost \ + --file apps/fz_http/priv/repo/structure.sql \ + --no-acl \ + --no-owner + - name: Upload main branch DB dump + if: "contains(github.ref, 'main')" + uses: actions/upload-artifact@v3 + with: + name: db-dump + path: apps/fz_http/priv/repo/structure.sql + - name: Run Seed + run: mix ecto.seed acceptance-test: runs-on: ubuntu-latest env: @@ -110,11 +292,8 @@ jobs: --cap-add=IPC_LOCK steps: - uses: nanasess/setup-chromedriver@v1 - with: - chromedriver-version: '108.0.5359.71' - run: | export DISPLAY=:99 - chromedriver --url-base=/wd/hub & sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & - name: Install package dependencies run: | @@ -129,7 +308,7 @@ jobs: - uses: actions/cache@v3 name: Elixir Deps Cache env: - cache-name: cache-elixir-deps + cache-name: cache-elixir-deps-${{ env.MIX_ENV }}-${{ env.MIX_ENV }} with: path: deps key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }} @@ -138,21 +317,21 @@ jobs: - uses: actions/cache@v3 name: Elixir Build Cache env: - cache-name: cache-elixir-build + cache-name: cache-elixir-build-${{ env.MIX_ENV }} with: path: _build key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }} - uses: actions/cache@v3 name: Yarn Deps Cache env: - cache-name: cache-yarn-build + cache-name: cache-yarn-build-${{ env.MIX_ENV }} with: path: apps/fz_http/assets/node_modules key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }} - uses: actions/cache@v3 name: Assets Cache env: - cache-name: cache-assets-build + cache-name: cache-assets-build-${{ env.MIX_ENV }} with: path: apps/fz_http/priv/static/dist key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }} @@ -174,17 +353,20 @@ jobs: run: | mix ecto.create mix ecto.migrate - - name: Run Tests and Upload Coverage Report + - name: Run Acceptance Tests env: MIX_TEST_PARTITION: ${{ matrix.MIX_TEST_PARTITION }} - E2E_MAX_WAIT_SECONDS: 20 + E2E_MAX_WAIT_SECONDS: 5 run: | mix test --only acceptance:true \ --partitions=${{ env.MIX_TEST_PARTITIONS }} \ --no-compile \ --no-archives-check \ --no-deps-check \ - || mix test --failed + || pkill -f chromedriver \ + || mix test --only acceptance:true --failed \ + || pkill -f chromedriver \ + || mix test --only acceptance:true --failed - name: Save Screenshots if: always() uses: actions/upload-artifact@v3 diff --git a/apps/fz_http/lib/fz_http/config.ex b/apps/fz_http/lib/fz_http/config.ex index 123b00842..21cc8e1e0 100644 --- a/apps/fz_http/lib/fz_http/config.ex +++ b/apps/fz_http/lib/fz_http/config.ex @@ -5,10 +5,9 @@ defmodule FzHttp.Config do """ if Mix.env() != :test do - defdelegate put_env(app, key, value), to: Application defdelegate fetch_env!(app, key), to: Application else - def put_env(app \\ :fz_http, key, value) do + def put_env_override(app \\ :fz_http, key, value) do Process.put(key_function(app, key), value) :ok end diff --git a/apps/fz_http/lib/fz_http/saml/start_proxy.ex b/apps/fz_http/lib/fz_http/saml/start_proxy.ex index 5c8388cc8..a1c56fbdd 100644 --- a/apps/fz_http/lib/fz_http/saml/start_proxy.ex +++ b/apps/fz_http/lib/fz_http/saml/start_proxy.ex @@ -60,7 +60,7 @@ defmodule FzHttp.SAML.StartProxy do |> set_service_provider() |> set_identity_providers(providers) - FzHttp.Config.put_env(:samly, Samly.Provider, samly_configs) + Application.put_env(:samly, Samly.Provider, samly_configs) Samly.Provider.refresh_providers() end end diff --git a/apps/fz_http/priv/repo/seeds.exs b/apps/fz_http/priv/repo/seeds.exs index bc97c9915..904c7e321 100644 --- a/apps/fz_http/priv/repo/seeds.exs +++ b/apps/fz_http/priv/repo/seeds.exs @@ -13,9 +13,48 @@ alias FzHttp.{ ConnectivityChecks, Devices, - Users + Users, + ApiTokens, + Rules, + MFA } +{:ok, unprivileged_user1} = + Users.create_unprivileged_user(%{ + email: "firezone-unprivileged-1@localhost" + }) + +{:ok, _device} = + Devices.create_device(%{ + user_id: unprivileged_user1.id, + name: "My Device", + description: "foo bar", + preshared_key: "27eCDMVRVFfMVS5Rfnn9n7as4M6MemGY/oghmdrwX2E=", + public_key: "4Fo+SBnDJ6hi8qzPt3nWLwgjCVwvpjHL35qJeatKwEc=", + remote_ip: %Postgrex.INET{address: {127, 5, 0, 1}}, + rx_bytes: 123_917_823, + tx_bytes: 1_934_475_211_087_234 + }) + +{:ok, mfa_user} = + Users.create_unprivileged_user(%{ + email: "firezone-mfa@localhost", + password: "firezone1234", + password_confirmation: "firezone1234" + }) + +secret = NimbleTOTP.secret() + +MFA.create_method( + %{ + name: "Google Authenticator", + type: :totp, + payload: %{"secret" => Base.encode64(secret)}, + code: NimbleTOTP.verification_code(secret) + }, + mfa_user.id +) + {:ok, user} = Users.create_admin_user(%{ email: "firezone@localhost", @@ -23,6 +62,10 @@ alias FzHttp.{ password_confirmation: "firezone1234" }) +{:ok, _api_token} = ApiTokens.create_user_api_token(user, %{"expires_in" => 5}) +{:ok, _api_token} = ApiTokens.create_user_api_token(user, %{"expires_in" => 30}) +{:ok, _api_token} = ApiTokens.create_user_api_token(user, %{"expires_in" => 1}) + {:ok, _device} = Devices.create_device(%{ user_id: user.id, @@ -150,3 +193,30 @@ alias FzHttp.{ response_code: 400, url: "https://ping-dev.firez.one/0.20.0" }) + +Rules.create_rule(%{ + destination: "10.0.0.0/24", + port_type: :tcp, + port_range: "100-200" +}) + +Rules.create_rule(%{ + destination: "1.2.3.4" +}) + +FzHttp.Configurations.put!( + :openid_connect_providers, + [ + %{ + "id" => "vault", + "discovery_document_uri" => "https://common.auth0.com/.well-known/openid-configuration", + "client_id" => "CLIENT_ID", + "client_secret" => "CLIENT_SECRET", + "redirect_uri" => "http://localhost:13000/auth/oidc/vault/callback/", + "response_type" => "code", + "scope" => "openid email offline_access", + "label" => "OIDC Vault", + "auto_create_users" => true + } + ] +) diff --git a/apps/fz_http/test/fz_http/devices_test.exs b/apps/fz_http/test/fz_http/devices_test.exs index 0e0636a9e..1a5c5a090 100644 --- a/apps/fz_http/test/fz_http/devices_test.exs +++ b/apps/fz_http/test/fz_http/devices_test.exs @@ -56,7 +56,7 @@ defmodule FzHttp.DevicesTest do } test "prevents creating more than max_devices_per_user", %{device: device} do - FzHttp.Config.put_env(:max_devices_per_user, 1) + FzHttp.Config.put_env_override(:max_devices_per_user, 1) assert {:error, %Ecto.Changeset{ @@ -83,7 +83,7 @@ defmodule FzHttp.DevicesTest do end test "soft limit max network range for IPv6", %{device: device} do - FzHttp.Config.put_env(:wireguard_ipv6_network, "fd00::/20") + FzHttp.Config.put_env_override(:wireguard_ipv6_network, "fd00::/20") attrs = %{@device_attrs | ipv4: nil, ipv6: nil, user_id: device.user_id} assert {:ok, _device} = Devices.create_device(attrs) end @@ -91,7 +91,7 @@ defmodule FzHttp.DevicesTest do test "returns error when device IP can't be assigned due to CIDR pool exhaustion", %{ device: device } do - FzHttp.Config.put_env(:wireguard_ipv4_network, "10.3.2.0/30") + FzHttp.Config.put_env_override(:wireguard_ipv4_network, "10.3.2.0/30") attrs = %{@device_attrs | ipv4: nil, ipv6: nil, user_id: device.user_id} assert {:ok, _device} = Devices.create_device(attrs) diff --git a/apps/fz_http/test/fz_http/rules_test.exs b/apps/fz_http/test/fz_http/rules_test.exs index 6c10d49a1..a007ac2d6 100644 --- a/apps/fz_http/test/fz_http/rules_test.exs +++ b/apps/fz_http/test/fz_http/rules_test.exs @@ -4,8 +4,8 @@ defmodule FzHttp.RulesTest do alias FzHttp.Rules setup do - FzHttp.Config.put_env(:wireguard_ipv4_network, "100.64.0.0/10") - FzHttp.Config.put_env(:wireguard_ipv6_network, "fd00::0/106") + FzHttp.Config.put_env_override(:wireguard_ipv4_network, "100.64.0.0/10") + FzHttp.Config.put_env_override(:wireguard_ipv6_network, "fd00::0/106") :ok end diff --git a/apps/fz_http/test/fz_http/telemetry_test.exs b/apps/fz_http/test/fz_http/telemetry_test.exs index 5a3247ade..bc82fbd6c 100644 --- a/apps/fz_http/test/fz_http/telemetry_test.exs +++ b/apps/fz_http/test/fz_http/telemetry_test.exs @@ -115,7 +115,7 @@ defmodule FzHttp.TelemetryTest do describe "database" do test "local hostname" do - FzHttp.Config.put_env(:fz_http, FzHttp.Repo, hostname: "localhost") + FzHttp.Config.put_env_override(:fz_http, FzHttp.Repo, hostname: "localhost") ping_data = Telemetry.ping_data() @@ -123,7 +123,7 @@ defmodule FzHttp.TelemetryTest do end test "local url" do - FzHttp.Config.put_env(:fz_http, FzHttp.Repo, url: "postgres://127.0.0.1") + FzHttp.Config.put_env_override(:fz_http, FzHttp.Repo, url: "postgres://127.0.0.1") ping_data = Telemetry.ping_data() @@ -131,7 +131,7 @@ defmodule FzHttp.TelemetryTest do end test "external hostname" do - FzHttp.Config.put_env(:fz_http, FzHttp.Repo, hostname: "firezone.dev") + FzHttp.Config.put_env_override(:fz_http, FzHttp.Repo, hostname: "firezone.dev") ping_data = Telemetry.ping_data() @@ -139,7 +139,7 @@ defmodule FzHttp.TelemetryTest do end test "external url" do - FzHttp.Config.put_env(:fz_http, FzHttp.Repo, url: "postgres://firezone.dev") + FzHttp.Config.put_env_override(:fz_http, FzHttp.Repo, url: "postgres://firezone.dev") ping_data = Telemetry.ping_data() @@ -149,7 +149,7 @@ defmodule FzHttp.TelemetryTest do describe "email" do test "outbound set" do - FzHttp.Config.put_env(:fz_http, FzHttpWeb.Mailer, + FzHttp.Config.put_env_override(:fz_http, FzHttpWeb.Mailer, adapter: Swoosh.Adapters.NoopAdapter, from_email: "test@firezone.dev" ) @@ -160,7 +160,7 @@ defmodule FzHttp.TelemetryTest do end test "outbound unset" do - FzHttp.Config.put_env(:fz_http, FzHttpWeb.Mailer, + FzHttp.Config.put_env_override(:fz_http, FzHttpWeb.Mailer, adapter: SwooshAdapters.NoopAdapter, from_email: nil ) diff --git a/apps/fz_http/test/fz_http_web/acceptance/admin_test.exs b/apps/fz_http/test/fz_http_web/acceptance/admin_test.exs index 1570f7708..dbef633bd 100644 --- a/apps/fz_http/test/fz_http_web/acceptance/admin_test.exs +++ b/apps/fz_http/test/fz_http_web/acceptance/admin_test.exs @@ -478,7 +478,6 @@ defmodule FzHttpWeb.Acceptance.AdminTest do |> assert_el(Query.text("Updated successfully.")) |> assert_el(Query.text("foo-bar-buz")) |> assert_el(Query.text("Sneaky ID")) - |> assert_el(Query.text("http://localhost:4002/autX/saml#foo")) assert [saml_identity_provider] = FzHttp.Configurations.get!(:saml_identity_providers) @@ -495,21 +494,6 @@ defmodule FzHttpWeb.Acceptance.AdminTest do auto_create_users: true } - assert FzHttp.Config.fetch_env!(:samly, Samly.Provider) == [ - identity_providers: [ - %{ - base_url: "http://localhost:4002/autX/saml#foo", - id: "foo-bar-buz", - metadata: saml_metadata, - sign_metadata: false, - sign_requests: false, - signed_assertion_in_resp: false, - signed_envelopes_in_resp: false, - sp_id: "firezone" - } - ] - ] - # Edit session = session diff --git a/apps/fz_http/test/fz_http_web/acceptance/authentication_test.exs b/apps/fz_http/test/fz_http_web/acceptance/authentication_test.exs index d6433203c..b6631cc97 100644 --- a/apps/fz_http/test/fz_http_web/acceptance/authentication_test.exs +++ b/apps/fz_http/test/fz_http_web/acceptance/authentication_test.exs @@ -361,7 +361,7 @@ defmodule FzHttpWeb.Acceptance.AuthenticationTest do |> visit(~p"/") |> assert_el(Query.link("Sign in with email")) |> click(Query.link("Sign in with email")) - |> assert_el(Query.text("Sign In")) + |> assert_el(Query.text("Sign In", minimum: 1)) |> fill_form(%{ "Email" => email, "Password" => password diff --git a/apps/fz_http/test/fz_http_web/plug/path_prefix_test.exs b/apps/fz_http/test/fz_http_web/plug/path_prefix_test.exs index 3b06eca18..5370ae1ff 100644 --- a/apps/fz_http/test/fz_http_web/plug/path_prefix_test.exs +++ b/apps/fz_http/test/fz_http_web/plug/path_prefix_test.exs @@ -11,17 +11,17 @@ defmodule FzHttpWeb.Plug.PathPrefixTest do describe "call/2" do test "does nothing when path prefix is not configured" do - FzHttp.Config.put_env(:path_prefix, nil) + FzHttp.Config.put_env_override(:path_prefix, nil) conn = conn(:get, "/") assert call(conn, []) == conn - FzHttp.Config.put_env(:path_prefix, "/") + FzHttp.Config.put_env_override(:path_prefix, "/") conn = conn(:get, "/foo") assert call(conn, []) == conn end test "removes prefix from conn.request_path" do - FzHttp.Config.put_env(:path_prefix, "/vpn/") + FzHttp.Config.put_env_override(:path_prefix, "/vpn/") conn = conn(:get, "/vpn/foo") assert returned_conn = call(conn, []) assert returned_conn.request_path == "/foo" @@ -30,7 +30,7 @@ defmodule FzHttpWeb.Plug.PathPrefixTest do end test "removes prefix from conn.path_info" do - FzHttp.Config.put_env(:path_prefix, "/vpn/") + FzHttp.Config.put_env_override(:path_prefix, "/vpn/") conn = conn(:get, "/vpn/foo") assert returned_conn = call(conn, []) assert returned_conn.path_info == ["foo"] @@ -39,7 +39,7 @@ defmodule FzHttpWeb.Plug.PathPrefixTest do end test "redirects users from not prefixed path" do - FzHttp.Config.put_env(:path_prefix, "/vpn/") + FzHttp.Config.put_env_override(:path_prefix, "/vpn/") conn = conn(:get, "/foo") assert returned_conn = call(conn, []) diff --git a/apps/fz_http/test/support/acceptance_case.ex b/apps/fz_http/test/support/acceptance_case.ex index 932f30a05..d31c36d03 100644 --- a/apps/fz_http/test/support/acceptance_case.ex +++ b/apps/fz_http/test/support/acceptance_case.ex @@ -112,10 +112,6 @@ defmodule FzHttpWeb.AcceptanceCase do {:error, e} -> raise Wallaby.QueryError, Query.ErrorMessage.message(query, e) - - error -> - raise Wallaby.ExpectationNotMetError, - "Wallaby has encountered an internal error: #{inspect(error)} with session: #{inspect(session)}" end assert_has(session, query) diff --git a/config/test.exs b/config/test.exs index 387de70c2..acbc014f1 100644 --- a/config/test.exs +++ b/config/test.exs @@ -71,7 +71,8 @@ config :wallaby, screenshot_on_failure: true, # XXX: Contribute to Wallaby to make this configurable on the per-process level, # along with buffer to write logs only on process failure - js_logger: false + js_logger: false, + hackney_options: [timeout: 10_000, recv_timeout: 10_000] config :ex_unit, formatters: [JUnitFormatter, ExUnit.CLIFormatter],