├── vbt_new
├── test
│ └── test_helper.exs
├── priv
│ └── templates
│ │ ├── priv
│ │ └── repo
│ │ │ └── migrations
│ │ │ └── .gitignore.eex
│ │ ├── dialyzer.ignore-warnings.eex
│ │ ├── .env.development.eex
│ │ ├── lib
│ │ ├── otp_app_schemas.ex.eex
│ │ ├── otp_app_schemas
│ │ │ └── base.ex.eex
│ │ ├── otp_app.ex.eex
│ │ ├── otp_app_app.ex.eex
│ │ ├── otp_app_web
│ │ │ └── views
│ │ │ │ └── error_view.ex.eex
│ │ ├── otp_app_config.ex.eex
│ │ └── otp_app_app
│ │ │ └── release.ex.eex
│ │ ├── test
│ │ ├── support
│ │ │ ├── otp_app_test_boundary.ex.eex
│ │ │ └── otp_app_test
│ │ │ │ ├── sentry_client.ex.eex
│ │ │ │ └── web
│ │ │ │ └── test_plug.ex.eex
│ │ └── web
│ │ │ ├── views
│ │ │ └── error_view_test.exs.eex
│ │ │ └── endpoint_test.exs.eex
│ │ ├── rel
│ │ └── bin
│ │ │ ├── check_config.sh.eex
│ │ │ ├── seed.sh.eex
│ │ │ ├── migrate.sh.eex
│ │ │ └── rollback.sh.eex
│ │ ├── .formatter.exs.eex
│ │ ├── .dockerignore.eex
│ │ ├── .tool-versions.eex
│ │ ├── .github
│ │ ├── pull_request_template.md.eex
│ │ └── workflows
│ │ │ ├── test.yaml.eex
│ │ │ ├── prod.yaml.eex
│ │ │ ├── develop.yaml.eex
│ │ │ ├── stage.yaml.eex
│ │ │ └── actions
│ │ │ └── test
│ │ │ └── action.yaml.eex
│ │ ├── entrypoint.sh.eex
│ │ ├── docker-compose.yml.eex
│ │ ├── .credo.exs.eex
│ │ ├── Makefile.eex
│ │ └── README.md.eex
├── test_projects
│ └── expected_state
│ │ ├── priv
│ │ ├── repo
│ │ │ ├── migrations
│ │ │ │ └── .gitignore
│ │ │ └── seeds.exs
│ │ └── gettext
│ │ │ ├── en
│ │ │ └── LC_MESSAGES
│ │ │ │ └── errors.po
│ │ │ └── errors.pot
│ │ ├── .tool-versions
│ │ ├── dialyzer.ignore-warnings
│ │ ├── lib
│ │ ├── skafolder_tester_schemas.ex
│ │ ├── skafolder_tester_web
│ │ │ ├── views
│ │ │ │ ├── page_view.ex
│ │ │ │ ├── layout_view.ex
│ │ │ │ ├── error_view.ex
│ │ │ │ └── error_helpers.ex
│ │ │ ├── controllers
│ │ │ │ └── page_controller.ex
│ │ │ ├── gettext.ex
│ │ │ ├── templates
│ │ │ │ ├── page
│ │ │ │ │ └── index.html.eex
│ │ │ │ └── layout
│ │ │ │ │ └── app.html.eex
│ │ │ ├── channels
│ │ │ │ └── user_socket.ex
│ │ │ ├── router.ex
│ │ │ ├── telemetry.ex
│ │ │ └── endpoint.ex
│ │ ├── skafolder_tester_schemas
│ │ │ └── base.ex
│ │ ├── skafolder_tester
│ │ │ └── repo.ex
│ │ ├── skafolder_tester.ex
│ │ ├── skafolder_tester_app.ex
│ │ ├── skafolder_tester_config.ex
│ │ ├── skafolder_tester_web.ex
│ │ └── skafolder_tester_app
│ │ │ └── release.ex
│ │ ├── test
│ │ ├── test_helper.exs
│ │ ├── support
│ │ │ ├── skafolder_tester_test_boundary.ex
│ │ │ └── skafolder_tester_test
│ │ │ │ ├── sentry_client.ex
│ │ │ │ ├── web
│ │ │ │ ├── test_plug.ex
│ │ │ │ ├── channel_case.ex
│ │ │ │ └── conn_case.ex
│ │ │ │ └── data_case.ex
│ │ └── skafolder_tester_web
│ │ │ ├── views
│ │ │ ├── page_view_test.exs
│ │ │ ├── layout_view_test.exs
│ │ │ └── error_view_test.exs
│ │ │ ├── controllers
│ │ │ └── page_controller_test.exs
│ │ │ └── endpoint_test.exs
│ │ ├── .env.development
│ │ ├── rel
│ │ └── bin
│ │ │ ├── check_config.sh
│ │ │ ├── seed.sh
│ │ │ ├── migrate.sh
│ │ │ └── rollback.sh
│ │ ├── .formatter.exs
│ │ ├── .dockerignore
│ │ ├── config
│ │ ├── test.exs
│ │ ├── config.exs
│ │ ├── dev.exs
│ │ └── prod.exs
│ │ ├── .github
│ │ ├── pull_request_template.md
│ │ └── workflows
│ │ │ ├── test.yaml
│ │ │ ├── prod.yaml
│ │ │ ├── develop.yaml
│ │ │ ├── stage.yaml
│ │ │ └── actions
│ │ │ └── test
│ │ │ └── action.yaml
│ │ ├── docker-compose.yml
│ │ ├── entrypoint.sh
│ │ ├── .gitignore
│ │ ├── .credo.exs
│ │ ├── Makefile
│ │ └── README.md
├── config
│ └── config.exs
├── .formatter.exs
├── README.md
├── .gitignore
├── lib
│ └── mix
│ │ └── vbt
│ │ ├── mix_file.ex
│ │ ├── source_file.ex
│ │ └── config_file.ex
├── mix.exs
└── .credo.exs
├── .tool-versions
├── test
├── support
│ ├── vbt
│ │ ├── templates
│ │ │ ├── greetings.text.eex
│ │ │ ├── greetings.html.eex
│ │ │ ├── layout.html.eex
│ │ │ └── layout.text.eex
│ │ ├── test_mailer.ex
│ │ ├── test_asset.ex
│ │ ├── schemas
│ │ │ ├── serial
│ │ │ │ ├── account.ex
│ │ │ │ └── token.ex
│ │ │ └── uuid
│ │ │ │ ├── account.ex
│ │ │ │ └── token.ex
│ │ ├── test_repo.ex
│ │ └── graphql_server.ex
│ └── test_application.exs
├── vbt
│ ├── absinthe
│ │ ├── relay
│ │ │ ├── type_resolver_test.exs
│ │ │ └── schema_test.exs
│ │ └── instrumentation_test.exs
│ ├── aws_test.exs
│ ├── ecto_test.exs
│ ├── kubernetes
│ │ └── probe_test.exs
│ ├── credo
│ │ └── check
│ │ │ ├── graphql
│ │ │ └── mutation_field_test.exs
│ │ │ └── readability
│ │ │ └── multiline_simple_do_test.exs
│ ├── fixed_job_test.exs
│ ├── auth_test.exs
│ ├── telemetry
│ │ └── oban_test.exs
│ └── graphql
│ │ ├── types_test.exs
│ │ └── case_test.exs
├── test_helper.exs
└── vbt_test.exs
├── .env.development
├── priv
└── test_repo
│ └── migrations
│ ├── 20200110083812_initialize_oban.exs
│ ├── 20200206103529_migrate_oban_10.exs
│ ├── 20200409064904_alter_accounts_adapt_tokens.exs
│ └── 20200114120644_create_accounts.exs
├── .formatter.exs
├── lib
├── vbt
│ ├── absinthe
│ │ ├── resolver_helper.ex
│ │ ├── relay
│ │ │ └── type_resolver.ex
│ │ ├── schema.ex
│ │ └── schema
│ │ │ └── normalize_errors.ex
│ ├── kubernetes
│ │ └── probe.ex
│ ├── application.ex
│ ├── telemetry
│ │ └── oban.ex
│ ├── aws.ex
│ ├── error.ex
│ ├── graphql
│ │ └── types.ex
│ ├── credo
│ │ └── check
│ │ │ ├── consistency
│ │ │ └── module_layout.ex
│ │ │ ├── readability
│ │ │ └── multiline_simple_do.ex
│ │ │ └── graphql
│ │ │ └── mutation_field.ex
│ ├── aws
│ │ └── test.ex
│ ├── ecto.ex
│ └── uri.ex
└── vbt.ex
├── config
└── config.exs
├── .github
├── pull_request_template.md
└── workflows
│ ├── vbt.yaml
│ └── vbt_new.yaml
├── .gitignore
├── entrypoint.sh
├── docker-compose.yaml
├── Dockerfile
├── README.md
├── .credo.exs
├── Makefile
└── mix.exs
/vbt_new/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
2 |
--------------------------------------------------------------------------------
/.tool-versions:
--------------------------------------------------------------------------------
1 | elixir 1.12-otp-24
2 | erlang 24.0
3 |
--------------------------------------------------------------------------------
/vbt_new/priv/templates/priv/repo/migrations/.gitignore.eex:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/priv/repo/migrations/.gitignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/support/vbt/templates/greetings.text.eex:
--------------------------------------------------------------------------------
1 | Hello <%= @name %>,
2 |
--------------------------------------------------------------------------------
/test/support/vbt/templates/greetings.html.eex:
--------------------------------------------------------------------------------
1 |
Hello <%= @name %>,
2 |
--------------------------------------------------------------------------------
/.env.development:
--------------------------------------------------------------------------------
1 | PGDATABASE=vbt_dev
2 | PGHOST=db
3 | PGUSER=postgres
4 | DOCKER=true
5 |
--------------------------------------------------------------------------------
/vbt_new/config/config.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | config :phoenix, :json_library, Jason
4 |
--------------------------------------------------------------------------------
/test/support/vbt/templates/layout.html.eex:
--------------------------------------------------------------------------------
1 | <%= @inner_content %>
2 | Best regards, VBT
3 |
--------------------------------------------------------------------------------
/test/support/vbt/templates/layout.text.eex:
--------------------------------------------------------------------------------
1 | <%= @inner_content %>
2 |
3 | Best regards,
4 | VBT
5 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/.tool-versions:
--------------------------------------------------------------------------------
1 | elixir 1.12-otp-24
2 | erlang 24.0
3 | postgres 12.2
4 |
--------------------------------------------------------------------------------
/vbt_new/.formatter.exs:
--------------------------------------------------------------------------------
1 | # Used by "mix format"
2 | [
3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
4 | ]
5 |
--------------------------------------------------------------------------------
/vbt_new/priv/templates/dialyzer.ignore-warnings.eex:
--------------------------------------------------------------------------------
1 | # subtle problem which happens when no routes are defined
2 | lib/phoenix/router.ex
3 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/dialyzer.ignore-warnings:
--------------------------------------------------------------------------------
1 | # subtle problem which happens when no routes are defined
2 | lib/phoenix/router.ex
3 |
--------------------------------------------------------------------------------
/vbt_new/priv/templates/.env.development.eex:
--------------------------------------------------------------------------------
1 | BUILD_PATH=_builds
2 |
3 | # database settings
4 | PGDATABASE=<%= app %>_dev
5 | PGHOST=db
6 | PGUSER=postgres
7 |
--------------------------------------------------------------------------------
/vbt_new/priv/templates/lib/otp_app_schemas.ex.eex:
--------------------------------------------------------------------------------
1 | defmodule <%= Mix.Vbt.schemas_module_name %> do
2 | use Boundary, exports: {:all, except: [Base]}
3 | end
4 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/lib/skafolder_tester_schemas.ex:
--------------------------------------------------------------------------------
1 | defmodule SkafolderTesterSchemas do
2 | use Boundary, exports: {:all, except: [Base]}
3 | end
4 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
2 | Ecto.Adapters.SQL.Sandbox.mode(SkafolderTester.Repo, :manual)
3 | VBT.Aws.Test.setup()
4 |
--------------------------------------------------------------------------------
/vbt_new/priv/templates/test/support/otp_app_test_boundary.ex.eex:
--------------------------------------------------------------------------------
1 | defmodule <%= Mix.Vbt.test_module_name() %> do
2 | use Boundary, check: [in: false, out: false]
3 | end
4 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/.env.development:
--------------------------------------------------------------------------------
1 | BUILD_PATH=_builds
2 |
3 | # database settings
4 | PGDATABASE=skafolder_tester_dev
5 | PGHOST=db
6 | PGUSER=postgres
7 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/test/support/skafolder_tester_test_boundary.ex:
--------------------------------------------------------------------------------
1 | defmodule SkafolderTesterTest do
2 | use Boundary, check: [in: false, out: false]
3 | end
4 |
--------------------------------------------------------------------------------
/test/vbt/absinthe/relay/type_resolver_test.exs:
--------------------------------------------------------------------------------
1 | defmodule VBT.Absinthe.Relay.TypeResolverTest do
2 | use ExUnit.Case, async: true
3 | doctest VBT.Absinthe.Relay.TypeResolver
4 | end
5 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/test/skafolder_tester_web/views/page_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule SkafolderTesterWeb.PageViewTest do
2 | use SkafolderTesterTest.Web.ConnCase, async: true
3 | end
4 |
--------------------------------------------------------------------------------
/vbt_new/priv/templates/rel/bin/check_config.sh.eex:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | script_dir=$(cd $(dirname ${BASH_SOURCE[0]}) && pwd)
3 | args="$@"
4 | $script_dir/<%= app %> eval "<%= Mix.Vbt.app_module_name() %>.Release.check_config()"
5 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/rel/bin/check_config.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | script_dir=$(cd $(dirname ${BASH_SOURCE[0]}) && pwd)
3 | args="$@"
4 | $script_dir/skafolder_tester eval "SkafolderTesterApp.Release.check_config()"
5 |
--------------------------------------------------------------------------------
/priv/test_repo/migrations/20200110083812_initialize_oban.exs:
--------------------------------------------------------------------------------
1 | defmodule VBT.TestRepo.Migrations.InitializeOban do
2 | use Ecto.Migration
3 |
4 | def up, do: Oban.Migrations.up()
5 | def down, do: Oban.Migrations.down()
6 | end
7 |
--------------------------------------------------------------------------------
/priv/test_repo/migrations/20200206103529_migrate_oban_10.exs:
--------------------------------------------------------------------------------
1 | defmodule VBT.TestRepo.Migrations.MigrateOban10 do
2 | use Ecto.Migration
3 |
4 | def up, do: Oban.Migrations.up()
5 | def down, do: Oban.Migrations.down()
6 | end
7 |
--------------------------------------------------------------------------------
/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | Ecto.Adapters.SQL.Sandbox.mode(VBT.TestRepo, :manual)
2 | VBT.Aws.Test.setup()
3 | Application.ensure_all_started(:credo)
4 | ExUnit.start()
5 | VBT.Absinthe.Instrumentation.set_long_operation_threshold(:infinity)
6 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/lib/skafolder_tester_web/views/page_view.ex:
--------------------------------------------------------------------------------
1 | # credo:disable-for-this-file Credo.Check.Readability.Specs
2 |
3 | defmodule SkafolderTesterWeb.PageView do
4 | use SkafolderTesterWeb, :view
5 | end
6 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/lib/skafolder_tester_web/views/layout_view.ex:
--------------------------------------------------------------------------------
1 | # credo:disable-for-this-file Credo.Check.Readability.Specs
2 |
3 | defmodule SkafolderTesterWeb.LayoutView do
4 | use SkafolderTesterWeb, :view
5 | end
6 |
--------------------------------------------------------------------------------
/.formatter.exs:
--------------------------------------------------------------------------------
1 | # Used by "mix format"
2 | [
3 | inputs: ["*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}"],
4 | import_deps: [:stream_data, :ecto, :ecto_sql, :absinthe, :plug, :phoenix],
5 | locals_without_parens: [gen: 1, gen: 2]
6 | ]
7 |
--------------------------------------------------------------------------------
/test/support/vbt/test_mailer.ex:
--------------------------------------------------------------------------------
1 | defmodule VBT.TestMailer do
2 | @moduledoc false
3 | use VBT.Mailer, templates: "templates", oban_worker: [queue: "mailer"]
4 |
5 | @impl VBT.Mailer
6 | @spec config :: map
7 | def config, do: %{}
8 | end
9 |
--------------------------------------------------------------------------------
/vbt_new/priv/templates/.formatter.exs.eex:
--------------------------------------------------------------------------------
1 | [
2 | import_deps: [:absinthe, :ecto, :ecto_enum, :ecto_sql, :phoenix],
3 | inputs: [
4 | "*.{ex,exs}",
5 | "priv/*/seeds.exs",
6 | "priv/*/migrations/*.exs",
7 | "{config,lib,test}/**/*.{ex,exs}"
8 | ]
9 | ]
10 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/.formatter.exs:
--------------------------------------------------------------------------------
1 | [
2 | import_deps: [:absinthe, :ecto, :ecto_enum, :ecto_sql, :phoenix],
3 | inputs: [
4 | "*.{ex,exs}",
5 | "priv/*/seeds.exs",
6 | "priv/*/migrations/*.exs",
7 | "{config,lib,test}/**/*.{ex,exs}"
8 | ]
9 | ]
10 |
--------------------------------------------------------------------------------
/vbt_new/priv/templates/.dockerignore.eex:
--------------------------------------------------------------------------------
1 | .env
2 | .env.*
3 | .tool-versions
4 | *.sql
5 | *.md
6 | *.pgdump
7 | .gitignore
8 | .DS_Store
9 | Dockerfile
10 | Makefile
11 | README*
12 | docker-compose.yml
13 | .git/
14 | _docker/
15 | _build/
16 | deps/
17 | doc/
18 | cover/
19 | priv/static/
20 | terraform/
21 | kubernetes/
22 | tmp/*
23 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/test/skafolder_tester_web/controllers/page_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule SkafolderTesterWeb.PageControllerTest do
2 | use SkafolderTesterTest.Web.ConnCase
3 |
4 | test "GET /", %{conn: conn} do
5 | conn = get(conn, "/")
6 | assert html_response(conn, 200) =~ "Welcome to Phoenix!"
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/test/support/vbt/test_asset.ex:
--------------------------------------------------------------------------------
1 | # credo:disable-for-this-file Credo.Check.Readability.Specs
2 |
3 | defmodule VBT.TestAsset do
4 | @moduledoc false
5 | defstruct path: nil
6 |
7 | def new(path), do: %__MODULE__{path: path}
8 |
9 | defimpl VBT.Aws.S3.Hostable do
10 | def path(test_struct), do: test_struct.path
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/lib/skafolder_tester_web/controllers/page_controller.ex:
--------------------------------------------------------------------------------
1 | # credo:disable-for-this-file Credo.Check.Readability.Specs
2 |
3 | defmodule SkafolderTesterWeb.PageController do
4 | use SkafolderTesterWeb, :controller
5 |
6 | def index(conn, _params) do
7 | render(conn, "index.html")
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/.dockerignore:
--------------------------------------------------------------------------------
1 | .env
2 | .env.*
3 | .tool-versions
4 | *.sql
5 | *.md
6 | *.pgdump
7 | .gitignore
8 | .DS_Store
9 | Dockerfile
10 | Makefile
11 | README*
12 | docker-compose.yml
13 | .git/
14 | _docker/
15 | _build/
16 | deps/
17 | doc/
18 | cover/
19 | priv/static/
20 | terraform/
21 | kubernetes/
22 | tmp/*
23 |
--------------------------------------------------------------------------------
/vbt_new/priv/templates/test/support/otp_app_test/sentry_client.ex.eex:
--------------------------------------------------------------------------------
1 | defmodule <%= Mix.Vbt.test_module_name() %>.SentryClient do
2 | @moduledoc false
3 | # credo:disable-for-this-file Credo.Check.Readability.Specs
4 |
5 | @doc false
6 | def send_event(event, _opts) do
7 | send(self(), {:sentry_report, event})
8 | :ok
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/test/support/skafolder_tester_test/sentry_client.ex:
--------------------------------------------------------------------------------
1 | defmodule SkafolderTesterTest.SentryClient do
2 | @moduledoc false
3 | # credo:disable-for-this-file Credo.Check.Readability.Specs
4 |
5 | @doc false
6 | def send_event(event, _opts) do
7 | send(self(), {:sentry_report, event})
8 | :ok
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/test/support/vbt/schemas/serial/account.ex:
--------------------------------------------------------------------------------
1 | defmodule VBT.Schemas.Serial.Account do
2 | @moduledoc false
3 | use Ecto.Schema
4 |
5 | @primary_key {:id, :id, autogenerate: true}
6 |
7 | schema "accounts_serial_id" do
8 | field :name, :string
9 | field :email, :string
10 | field :password_hash, :string
11 | has_many :tokens, VBT.Schemas.Serial.Token
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/test/support/vbt/schemas/uuid/account.ex:
--------------------------------------------------------------------------------
1 | defmodule VBT.Schemas.Uuid.Account do
2 | @moduledoc false
3 | use Ecto.Schema
4 |
5 | @primary_key {:id, :binary_id, autogenerate: true}
6 |
7 | schema "accounts_uuid" do
8 | field :name, :string
9 | field :email, :string
10 | field :password_hash, :string
11 | has_many :tokens, VBT.Schemas.Uuid.Token
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/test/skafolder_tester_web/views/layout_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule SkafolderTesterWeb.LayoutViewTest do
2 | use SkafolderTesterTest.Web.ConnCase, async: true
3 |
4 | # When testing helpers, you may want to import Phoenix.HTML and
5 | # use functions such as safe_to_string() to convert the helper
6 | # result into an HTML string.
7 | # import Phoenix.HTML
8 | end
9 |
--------------------------------------------------------------------------------
/test/support/vbt/schemas/serial/token.ex:
--------------------------------------------------------------------------------
1 | defmodule VBT.Schemas.Serial.Token do
2 | @moduledoc false
3 | use Ecto.Schema
4 |
5 | @primary_key {:id, :binary_id, autogenerate: true}
6 |
7 | schema "tokens_serial_id" do
8 | field :hash, :binary
9 | field :type, :string
10 | field :used_at, :utc_datetime
11 | field :expires_at, :utc_datetime
12 | belongs_to :account, VBT.Schemas.Serial.Account
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/test/support/vbt/schemas/uuid/token.ex:
--------------------------------------------------------------------------------
1 | defmodule VBT.Schemas.Uuid.Token do
2 | @moduledoc false
3 | use Ecto.Schema
4 |
5 | @primary_key {:id, :binary_id, autogenerate: true}
6 |
7 | schema "tokens_uuid" do
8 | field :hash, :binary
9 | field :type, :string
10 | field :used_at, :utc_datetime
11 | field :expires_at, :utc_datetime
12 | belongs_to :account, VBT.Schemas.Uuid.Account, type: :binary_id
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/priv/repo/seeds.exs:
--------------------------------------------------------------------------------
1 | # Script for populating the database. You can run it as:
2 | #
3 | # mix run priv/repo/seeds.exs
4 | #
5 | # Inside the script, you can read and write to any of your
6 | # repositories directly:
7 | #
8 | # SkafolderTester.Repo.insert!(%SkafolderTester.SomeSchema{})
9 | #
10 | # We recommend using the bang functions (`insert!`, `update!`
11 | # and so on) as they will fail if something goes wrong.
12 |
--------------------------------------------------------------------------------
/vbt_new/priv/templates/lib/otp_app_schemas/base.ex.eex:
--------------------------------------------------------------------------------
1 | defmodule <%= Mix.Vbt.schemas_module_name %>.Base do
2 | defmacro __using__(_) do
3 | quote do
4 | use Ecto.Schema
5 |
6 | import Ecto.Changeset
7 | import EctoEnum
8 |
9 | @primary_key {:id, :binary_id, autogenerate: true}
10 | @foreign_key_type :binary_id
11 | @timestamps_opts [type: :utc_datetime_usec]
12 |
13 | @type t :: %__MODULE__{}
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/lib/skafolder_tester_schemas/base.ex:
--------------------------------------------------------------------------------
1 | defmodule SkafolderTesterSchemas.Base do
2 | defmacro __using__(_) do
3 | quote do
4 | use Ecto.Schema
5 |
6 | import Ecto.Changeset
7 | import EctoEnum
8 |
9 | @primary_key {:id, :binary_id, autogenerate: true}
10 | @foreign_key_type :binary_id
11 | @timestamps_opts [type: :utc_datetime_usec]
12 |
13 | @type t :: %__MODULE__{}
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/vbt_new/README.md:
--------------------------------------------------------------------------------
1 | # VbtNew
2 |
3 | This project powers the `vbt.new` mix task.
4 |
5 | If you want to manually test this task, simply invoke `mix vbt.new` from this project's folder. Note that the entire task is tested via `mix test`. The first test run will take about 15 minutes, but subsequent runs will have a fairly reasonable time of about 5 seconds.
6 |
7 | Since this project generates a mix archive, you shouldn't add any runtime dependencies to the project (compile-time dependencies are fine).
8 |
--------------------------------------------------------------------------------
/test/vbt/aws_test.exs:
--------------------------------------------------------------------------------
1 | defmodule VBT.AwsTest do
2 | use ExUnit.Case, async: false
3 | alias VBT.Aws
4 |
5 | describe "client" do
6 | setup do
7 | Application.delete_env(:vbt, :ex_aws_client)
8 | end
9 |
10 | test "returns ExAws by default" do
11 | assert Aws.client() == ExAws
12 | end
13 |
14 | test "returns configured module" do
15 | Application.put_env(:vbt, :ex_aws_client, MyExAwsClient)
16 | assert Aws.client() == MyExAwsClient
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/vbt/absinthe/resolver_helper.ex:
--------------------------------------------------------------------------------
1 | defmodule VBT.Absinthe.ResolverHelper do
2 | # TODO: remove in the next version.
3 | @moduledoc deprecated: "Use VBT.Absinthe.Schema instead."
4 |
5 | alias VBT.Absinthe.Schema.NormalizeErrors
6 |
7 | # TODO: remove in the next version.
8 | @deprecated "Use VBT.Absinthe.Schema instead."
9 | # credo:disable-for-next-line Credo.Check.Readability.Specs
10 | def changeset_errors(changeset, opts \\ []),
11 | do: NormalizeErrors.changeset_errors(changeset, opts)
12 | end
13 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/lib/skafolder_tester/repo.ex:
--------------------------------------------------------------------------------
1 | defmodule SkafolderTester.Repo do
2 | use VBT.Repo,
3 | otp_app: :skafolder_tester,
4 | adapter: Ecto.Adapters.Postgres
5 |
6 | @impl Ecto.Repo
7 | def init(_type, config) do
8 | config =
9 | Keyword.merge(
10 | config,
11 | url: SkafolderTesterConfig.database_url(),
12 | pool_size: SkafolderTesterConfig.database_pool_size(),
13 | ssl: SkafolderTesterConfig.database_ssl()
14 | )
15 |
16 | {:ok, config}
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/lib/vbt/kubernetes/probe.ex:
--------------------------------------------------------------------------------
1 | defmodule VBT.Kubernetes.Probe do
2 | @moduledoc """
3 | Plug for handling kubernetes liveness probe checks.
4 |
5 | To use it, add `plug VBT.Kubernetes.Probe, "/healthz"` in your endpoint.
6 | """
7 |
8 | @behaviour Plug
9 | import Plug.Conn
10 |
11 | @impl Plug
12 | def init(path), do: String.split(path, "/", trim: true)
13 |
14 | @impl Plug
15 | def call(conn, path_info) do
16 | if conn.method == "GET" and conn.path_info == path_info,
17 | do: conn |> resp(:ok, "") |> halt(),
18 | else: conn
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/config/config.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | config :phoenix, :json_library, Jason
4 | config :ex_aws, json_codec: Jason
5 |
6 | if Mix.env() == :test do
7 | config :logger, level: :warn
8 | config :phoenix, :json_library, Jason
9 | config :stream_data, max_runs: if(System.get_env("CI"), do: 100, else: 10)
10 |
11 | config :vbt, VBT.GraphqlServer,
12 | server: false,
13 | secret_key_base: String.duplicate("0", 64),
14 | pubsub_server: VBT.GraphqlServer.PubSub
15 |
16 | config :vbt, ecto_repos: [VBT.TestRepo]
17 |
18 | config :bcrypt_elixir, :log_rounds, 4
19 | end
20 |
--------------------------------------------------------------------------------
/vbt_new/priv/templates/.tool-versions.eex:
--------------------------------------------------------------------------------
1 | <%=
2 | versions = Mix.Vbt.tool_versions()
3 |
4 | tool_versions =
5 | %{
6 | elixir: "#{versions.elixir.major}.#{versions.elixir.minor}-otp-#{versions.erlang.major}",
7 | erlang: "#{versions.erlang.major}.#{versions.erlang.minor}",
8 | nodejs: to_string(versions.nodejs),
9 | postgres: "#{versions.postgres.major}.#{versions.postgres.minor}"
10 | }
11 |
12 | entries =
13 | for {tool, version} <- tool_versions,
14 | tool != :nodejs or File.dir?("assets"),
15 | do: "#{tool} #{version}"
16 |
17 | Enum.join(entries, "\n")
18 | %>
19 |
--------------------------------------------------------------------------------
/vbt_new/priv/templates/test/web/views/error_view_test.exs.eex:
--------------------------------------------------------------------------------
1 | defmodule <%= Mix.Vbt.web_module_name() %>.ErrorViewTest do
2 | use <%= Mix.Vbt.test_module_name() %>.Web.ConnCase, async: true
3 |
4 | # Bring render/3 and render_to_string/3 for testing custom views
5 | import Phoenix.View
6 |
7 | test "renders 404.json" do
8 | assert render(<%= Mix.Vbt.web_module_name() %>.ErrorView, "404.json", []) == %{errors: [%{message: "Not Found"}]}
9 | end
10 |
11 | test "renders 500.json" do
12 | assert render(<%= Mix.Vbt.web_module_name() %>.ErrorView, "500.json", []) ==
13 | %{errors: [%{message: "Internal Server Error"}]}
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/test/skafolder_tester_web/views/error_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule SkafolderTesterWeb.ErrorViewTest do
2 | use SkafolderTesterTest.Web.ConnCase, async: true
3 |
4 | # Bring render/3 and render_to_string/3 for testing custom views
5 | import Phoenix.View
6 |
7 | test "renders 404.json" do
8 | assert render(SkafolderTesterWeb.ErrorView, "404.json", []) == %{
9 | errors: [%{message: "Not Found"}]
10 | }
11 | end
12 |
13 | test "renders 500.json" do
14 | assert render(SkafolderTesterWeb.ErrorView, "500.json", []) ==
15 | %{errors: [%{message: "Internal Server Error"}]}
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/config/test.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | config :bcrypt_elixir, :log_rounds, 1
4 |
5 | # Configure your database
6 | #
7 | # The MIX_TEST_PARTITION environment variable can be used
8 | # to provide built-in test partitioning in CI environment.
9 | # Run `mix help test` for more information.
10 | config :skafolder_tester, SkafolderTester.Repo, pool: Ecto.Adapters.SQL.Sandbox
11 |
12 | # We don't run a server during test. If one is required,
13 | # you can enable the server option below.
14 | config :skafolder_tester, SkafolderTesterWeb.Endpoint, server: false
15 |
16 | # Print only warnings and errors during test
17 | config :logger, level: :warn
18 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ## Changes
2 |
3 |
4 | ## Ticket
5 |
6 |
7 | ## Checklist:
8 |
9 | - [ ] I have performed a self-review of my own code
10 | - [ ] I have made corresponding changes to the documentation
11 | - [ ] I have tried to find clearer solution before commenting hard-to-understand parts of code
12 | - [ ] I have added tests that prove my fix is effective or that my feature works
13 |
14 | ## Deployment TODO
15 |
16 |
--------------------------------------------------------------------------------
/vbt_new/priv/templates/rel/bin/seed.sh.eex:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | function myHelp () {
3 | # Using a here doc with standard out.
4 | cat <<-END
5 | Usage:
6 | ------
7 | -h | --help
8 | Display this help
9 | --file FILE_NAME
10 | Seeds specified file. Folder for seed file is "priv/repo/" + seed_file_name
11 | ------
12 | If no options provided, it will seed default seed file: "priv/repo/seeds.exs"
13 | END
14 | };
15 |
16 | if [ "$1" == "-h" ] || [ "$1" == "--help" ]; then
17 | myHelp;
18 | exit 0;
19 | fi
20 |
21 | script_dir=$(cd $(dirname ${BASH_SOURCE[0]}) && pwd)
22 | args="$@"
23 | $script_dir/<%= app %> eval "<%= Mix.Vbt.app_module_name() %>.Release.seed(~w($args))"
24 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/rel/bin/seed.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | function myHelp () {
3 | # Using a here doc with standard out.
4 | cat <<-END
5 | Usage:
6 | ------
7 | -h | --help
8 | Display this help
9 | --file FILE_NAME
10 | Seeds specified file. Folder for seed file is "priv/repo/" + seed_file_name
11 | ------
12 | If no options provided, it will seed default seed file: "priv/repo/seeds.exs"
13 | END
14 | };
15 |
16 | if [ "$1" == "-h" ] || [ "$1" == "--help" ]; then
17 | myHelp;
18 | exit 0;
19 | fi
20 |
21 | script_dir=$(cd $(dirname ${BASH_SOURCE[0]}) && pwd)
22 | args="$@"
23 | $script_dir/skafolder_tester eval "SkafolderTesterApp.Release.seed(~w($args))"
24 |
--------------------------------------------------------------------------------
/vbt_new/.gitignore:
--------------------------------------------------------------------------------
1 | # The directory Mix will write compiled artifacts to.
2 | /_build/
3 |
4 | # If you run "mix test --cover", coverage assets end up here.
5 | /cover/
6 |
7 | # The directory Mix downloads your dependencies sources to.
8 | /deps/
9 |
10 | # Where third-party dependencies like ExDoc output generated docs.
11 | /doc/
12 |
13 | # Ignore .fetch files in case you like to edit your project deps locally.
14 | /.fetch
15 |
16 | # If the VM crashes, it generates a dump, let's ignore it too.
17 | erl_crash.dump
18 |
19 | # Also ignore archive artifacts (built via "mix archive.build").
20 | *.ez
21 |
22 | # Ignore package tarball (built via "mix hex.build").
23 | vbt_new-*.tar
24 |
25 | /tmp/
26 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/lib/skafolder_tester.ex:
--------------------------------------------------------------------------------
1 | defmodule SkafolderTester do
2 | use Boundary, deps: [SkafolderTesterConfig, SkafolderTesterSchemas]
3 |
4 | @spec start_link :: Supervisor.on_start()
5 | def start_link do
6 | Supervisor.start_link(
7 | [
8 | SkafolderTester.Repo,
9 | {Phoenix.PubSub, name: SkafolderTester.PubSub}
10 | ],
11 | strategy: :one_for_one,
12 | name: __MODULE__
13 | )
14 | end
15 |
16 | @spec child_spec(any) :: Supervisor.child_spec()
17 | def child_spec(_arg) do
18 | %{
19 | id: __MODULE__,
20 | type: :supervisor,
21 | start: {__MODULE__, :start_link, []}
22 | }
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/vbt_new/priv/templates/.github/pull_request_template.md.eex:
--------------------------------------------------------------------------------
1 | ## Changes
2 |
3 |
4 | ## Ticket
5 |
6 |
7 | ## Checklist:
8 |
9 | - [ ] I have performed a self-review of my own code
10 | - [ ] I have made corresponding changes to the documentation
11 | - [ ] I have tried to find clearer solution before commenting hard-to-understand parts of code
12 | - [ ] I have added tests that prove my fix is effective or that my feature works
13 |
14 | ## Deployment TODO
15 |
16 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/lib/skafolder_tester_app.ex:
--------------------------------------------------------------------------------
1 | # credo:disable-for-this-file Credo.Check.Readability.Specs
2 |
3 | defmodule SkafolderTesterApp do
4 | use Boundary, deps: [SkafolderTester, SkafolderTesterConfig, SkafolderTesterWeb]
5 | use Application
6 |
7 | def start(_type, _args) do
8 | SkafolderTesterConfig.validate!()
9 |
10 | Supervisor.start_link(
11 | [
12 | SkafolderTester,
13 | SkafolderTesterWeb
14 | ],
15 | strategy: :one_for_one,
16 | name: __MODULE__
17 | )
18 | end
19 |
20 | def config_change(changed, _new, removed) do
21 | SkafolderTesterWeb.Endpoint.config_change(changed, removed)
22 | :ok
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ## Changes
2 |
3 |
4 | ## Ticket
5 |
6 |
7 | ## Checklist:
8 |
9 | - [ ] I have performed a self-review of my own code
10 | - [ ] I have made corresponding changes to the documentation
11 | - [ ] I have tried to find clearer solution before commenting hard-to-understand parts of code
12 | - [ ] I have added tests that prove my fix is effective or that my feature works
13 |
14 | ## Deployment TODO
15 |
16 |
--------------------------------------------------------------------------------
/test/vbt_test.exs:
--------------------------------------------------------------------------------
1 | defmodule VBTTest do
2 | use ExUnit.Case, async: true
3 |
4 | doctest VBT
5 |
6 | describe "validate" do
7 | test "returns :ok if condition is met" do
8 | assert VBT.validate(1 + 1 == 2, :some_error) == :ok
9 | end
10 |
11 | test "returns error if condition is not met" do
12 | assert VBT.validate(1 + 1 == 11, :some_error) == {:error, :some_error}
13 | end
14 | end
15 |
16 | describe "authorize" do
17 | test "returns :ok if condition is met" do
18 | assert VBT.authorize(1 + 1 == 2) == :ok
19 | end
20 |
21 | test "returns error if condition is not met" do
22 | assert VBT.authorize(1 + 1 == 11) == {:error, :unauthorized}
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/test/vbt/ecto_test.exs:
--------------------------------------------------------------------------------
1 | defmodule VBT.EctoTest do
2 | use ExUnit.Case, async: true
3 | doctest VBT.Ecto
4 |
5 | import VBT.Ecto
6 | alias Ecto.Adapters.SQL.Sandbox
7 | alias Ecto.Multi
8 | alias VBT.TestRepo
9 |
10 | setup do
11 | Sandbox.checkout(VBT.TestRepo)
12 | :ok
13 | end
14 |
15 | describe "multi_operation_result" do
16 | test "raises if invalid field is accessed" do
17 | result =
18 | Multi.new()
19 | |> Multi.run(:foo, fn _, _ -> {:ok, 1} end)
20 | |> TestRepo.transaction()
21 |
22 | assert_raise(
23 | KeyError,
24 | "key :bar not found in: %{foo: 1}",
25 | fn -> multi_operation_result(result, :bar) end
26 | )
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # The directory Mix will write compiled artifacts to.
2 | /_build/
3 |
4 | # If you run "mix test --cover", coverage assets end up here.
5 | /cover/
6 |
7 | # The directory Mix downloads your dependencies sources to.
8 | /deps/
9 |
10 | # Where third-party dependencies like ExDoc output generated docs.
11 | /doc/
12 |
13 | # Ignore .fetch files in case you like to edit your project deps locally.
14 | /.fetch
15 |
16 | # If the VM crashes, it generates a dump, let's ignore it too.
17 | erl_crash.dump
18 |
19 | # Also ignore archive artifacts (built via "mix archive.build").
20 | *.ez
21 |
22 | # Ignore package tarball (built via "mix hex.build").
23 | vbt-*.tar
24 |
25 | # Files generated by running tests inside docker
26 | .bash_history
27 | .cache/
28 |
--------------------------------------------------------------------------------
/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Add local user with USER_NAME
4 | # Either use the LOCAL_USER_ID if passed in at runtime or fallback to 9001
5 | USER_NAME=$APP_USER
6 | USER_ID=${LOCAL_USER_ID:-9001}
7 |
8 | # Add user if it doesn't exist
9 | useradd --uid $USER_ID \
10 | --shell /bin/bash \
11 | --non-unique \
12 | --comment "App user" \
13 | --create-home $USER_NAME \
14 | --home-dir /opt/app
15 |
16 | # Change ownership of library directories
17 | chown -R $USER_NAME. /opt/cache
18 | chown -R $USER_NAME. $WORKDIR
19 |
20 | # Run the command attached to the process with PID 1 so that signals get
21 | # passed to the process/app being run
22 | echo "Starting with USER=$USER_NAME UID=$USER_ID"
23 | exec /usr/local/sbin/pid1 -u $USER_NAME -g $USER_NAME "$@"
24 |
--------------------------------------------------------------------------------
/vbt_new/priv/templates/lib/otp_app.ex.eex:
--------------------------------------------------------------------------------
1 | defmodule <%= Mix.Vbt.context_module_name() %> do
2 | use Boundary, deps: [<%= Mix.Vbt.config_module_name() %>, <%= Mix.Vbt.schemas_module_name() %>]
3 |
4 | @spec start_link :: Supervisor.on_start()
5 | def start_link do
6 | Supervisor.start_link(
7 | [
8 | <%= Mix.Vbt.context_module_name() %>.Repo,
9 | {Phoenix.PubSub, name: <%= Mix.Vbt.context_module_name() %>.PubSub}
10 | ],
11 | strategy: :one_for_one,
12 | name: __MODULE__
13 | )
14 | end
15 |
16 | @spec child_spec(any) :: Supervisor.child_spec()
17 | def child_spec(_arg) do
18 | %{
19 | id: __MODULE__,
20 | type: :supervisor,
21 | start: {__MODULE__, :start_link, []}
22 | }
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/vbt_new/priv/templates/rel/bin/migrate.sh.eex:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | function myHelp () {
3 | # Using a here doc with standard out.
4 | cat <<-END
5 | Usage:
6 | ------
7 | -h | --help
8 | Display this help
9 | -n NUMBER | --step NUMBER
10 | Runs the specific number of migrations
11 | --to VERSION
12 | Runs all until the supplied version is reached
13 | --all
14 | Runs all available if true
15 | ------
16 | If no options provided, it will run all available migrations.
17 | END
18 | };
19 |
20 | if [ "$1" == "-h" ] || [ "$1" == "--help" ]; then
21 | myHelp;
22 | exit 0;
23 | fi
24 |
25 | script_dir=$(cd $(dirname ${BASH_SOURCE[0]}) && pwd)
26 | args="$@"
27 | $script_dir/<%= app %> eval "<%= Mix.Vbt.app_module_name() %>.Release.migrate(~w($args))"
28 |
--------------------------------------------------------------------------------
/vbt_new/priv/templates/rel/bin/rollback.sh.eex:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | function myHelp () {
3 | # Using a here doc with standard out.
4 | cat <<-END
5 | Usage:
6 | ------
7 | -h | --help
8 | Display this help
9 | -n NUMBER | --step NUMBER
10 | Runs the specific number of migrations
11 | --to VERSION
12 | Runs all until the supplied version is reached
13 | --all
14 | Runs all available if true
15 | ------
16 | If no options provided, it will run a single step rollback.
17 | END
18 | };
19 |
20 | if [ "$1" == "-h" ] || [ "$1" == "--help" ]; then
21 | myHelp;
22 | exit 0;
23 | fi
24 |
25 | script_dir=$(cd $(dirname ${BASH_SOURCE[0]}) && pwd)
26 | args="$@"
27 | $script_dir/<%= app %> eval "<%= Mix.Vbt.app_module_name() %>.Release.rollback(~w($args))"
28 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/rel/bin/migrate.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | function myHelp () {
3 | # Using a here doc with standard out.
4 | cat <<-END
5 | Usage:
6 | ------
7 | -h | --help
8 | Display this help
9 | -n NUMBER | --step NUMBER
10 | Runs the specific number of migrations
11 | --to VERSION
12 | Runs all until the supplied version is reached
13 | --all
14 | Runs all available if true
15 | ------
16 | If no options provided, it will run all available migrations.
17 | END
18 | };
19 |
20 | if [ "$1" == "-h" ] || [ "$1" == "--help" ]; then
21 | myHelp;
22 | exit 0;
23 | fi
24 |
25 | script_dir=$(cd $(dirname ${BASH_SOURCE[0]}) && pwd)
26 | args="$@"
27 | $script_dir/skafolder_tester eval "SkafolderTesterApp.Release.migrate(~w($args))"
28 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/rel/bin/rollback.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | function myHelp () {
3 | # Using a here doc with standard out.
4 | cat <<-END
5 | Usage:
6 | ------
7 | -h | --help
8 | Display this help
9 | -n NUMBER | --step NUMBER
10 | Runs the specific number of migrations
11 | --to VERSION
12 | Runs all until the supplied version is reached
13 | --all
14 | Runs all available if true
15 | ------
16 | If no options provided, it will run a single step rollback.
17 | END
18 | };
19 |
20 | if [ "$1" == "-h" ] || [ "$1" == "--help" ]; then
21 | myHelp;
22 | exit 0;
23 | fi
24 |
25 | script_dir=$(cd $(dirname ${BASH_SOURCE[0]}) && pwd)
26 | args="$@"
27 | $script_dir/skafolder_tester eval "SkafolderTesterApp.Release.rollback(~w($args))"
28 |
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: '3.4'
2 | services:
3 | web:
4 | build:
5 | context: .
6 | target: build
7 | ports:
8 | - "4000:4000"
9 | depends_on:
10 | - db
11 | volumes:
12 | - .:/opt/app
13 | - build_cache:/opt/cache
14 | env_file: .env.development
15 | environment:
16 | - LOCAL_USER_ID=${LOCAL_USER_ID}
17 | tty: true
18 | stdin_open: true
19 | command: '/bin/bash -c "while true; do sleep 10; done;"'
20 | ulimits:
21 | nofile: 1024
22 | nproc: 63090
23 | db:
24 | image: "postgres:12.2-alpine"
25 | volumes:
26 | - db:/var/lib/postgresql/data
27 | ports:
28 | - "5434:5432"
29 | environment:
30 | - POSTGRES_HOST_AUTH_METHOD=trust
31 | volumes:
32 | db: {}
33 | build_cache: {}
34 |
--------------------------------------------------------------------------------
/test/support/vbt/test_repo.ex:
--------------------------------------------------------------------------------
1 | defmodule VBT.TestRepo do
2 | @moduledoc false
3 |
4 | # credo:disable-for-this-file Credo.Check.Readability.Specs
5 |
6 | use VBT.Repo, otp_app: :vbt, adapter: Ecto.Adapters.Postgres
7 |
8 | @impl Ecto.Repo
9 | def init(_type, opts),
10 | do: {:ok, opts |> Keyword.merge(access_opts()) |> Keyword.merge(common_opts())}
11 |
12 | defp common_opts do
13 | [
14 | database: "vbt_test",
15 | pool: Ecto.Adapters.SQL.Sandbox,
16 | show_sensitive_data_on_connection_error: true
17 | ]
18 | end
19 |
20 | defp access_opts do
21 | if System.get_env("CI") == "true" or System.get_env("DOCKER") == "true",
22 | do: [username: "postgres", password: "postgres"],
23 | else: [socket_dir: "/var/run/postgresql"]
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/vbt_new/priv/templates/lib/otp_app_app.ex.eex:
--------------------------------------------------------------------------------
1 | # credo:disable-for-this-file Credo.Check.Readability.Specs
2 |
3 | defmodule <%= Mix.Vbt.app_module_name() %> do
4 | use Boundary, deps: [<%= Mix.Vbt.context_module_name() %>, <%= Mix.Vbt.config_module_name() %>, <%= Mix.Vbt.web_module_name() %>]
5 | use Application
6 |
7 | def start(_type, _args) do
8 | <%= Mix.Vbt.config_module_name() %>.validate!()
9 |
10 | Supervisor.start_link(
11 | [
12 | <%= Mix.Vbt.context_module_name() %>,
13 | <%= Mix.Vbt.web_module_name() %>
14 | ],
15 | strategy: :one_for_one,
16 | name: __MODULE__
17 | )
18 | end
19 |
20 | def config_change(changed, _new, removed) do
21 | <%= Mix.Vbt.web_module_name() %>.Endpoint.config_change(changed, removed)
22 | :ok
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/vbt/application.ex:
--------------------------------------------------------------------------------
1 | defmodule VBT.Application do
2 | @moduledoc false
3 |
4 | # credo:disable-for-this-file Credo.Check.Readability.Specs
5 |
6 | use Application
7 | alias VBT.{Absinthe, Telemetry}
8 |
9 | test_children =
10 | if Mix.env() == :test do
11 | [
12 | VBT.TestRepo,
13 | {Oban, repo: VBT.TestRepo, crontab: false, queues: false, plugins: false},
14 | {Phoenix.PubSub, [name: VBT.GraphqlServer.PubSub, adapter: Phoenix.PubSub.PG2]},
15 | VBT.GraphqlServer
16 | ]
17 | else
18 | []
19 | end
20 |
21 | def start(_type, _args) do
22 | VBT.FixedJob.init_time_provider()
23 | Telemetry.Oban.install_handler()
24 |
25 | Absinthe.Instrumentation.configure()
26 | Supervisor.start_link(unquote(test_children), strategy: :one_for_one, name: VBT.Supervisor)
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/vbt_new/lib/mix/vbt/mix_file.ex:
--------------------------------------------------------------------------------
1 | defmodule Mix.Vbt.MixFile do
2 | @moduledoc false
3 | alias Mix.Vbt.SourceFile
4 |
5 | @spec append_config(SourceFile.t(), String.t() | atom, String.t()) :: SourceFile.t()
6 | def append_config(file, name, element) do
7 | content =
8 | String.replace(
9 | file.content,
10 |
11 | # Match def or defp with the given name, and its inner body, stopping at the
12 | # last non-whitespace character before the last `]` in the function body which is placed
13 | # right before the closing `end`. The matched string therefore includes the entire list
14 | # minus the closing bracket.
15 | ~r/\s*def(p?)\s*#{name}\s*do.*?[^\s](?=\s*\]\s*end)/s,
16 |
17 | # Inject entire match and append the desired element
18 | "\\0, #{element} "
19 | )
20 |
21 | %{file | content: content}
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.4'
2 | services:
3 | web:
4 | image: "skafolder_tester:latest"
5 | ports:
6 | - "4000:4000"
7 | depends_on:
8 | - db
9 | volumes:
10 | - .:/opt/app
11 | - build_cache:/opt/cache
12 | - $SSH_AUTH_SOCK:${SSH_AUTH_SOCK}
13 | env_file: .env.development
14 | environment:
15 | - LOCAL_USER_ID=${LOCAL_USER_ID}
16 | - SSH_AUTH_SOCK=${SSH_AUTH_SOCK}
17 | tty: true
18 | stdin_open: true
19 | command: '/bin/bash -c "while true; do sleep 10; done;"'
20 | ulimits:
21 | nofile: 1024
22 | nproc: 63090
23 | db:
24 | image: "postgres:12.2-alpine"
25 | volumes:
26 | - db:/var/lib/postgresql/data
27 | ports:
28 | - "5434:5432"
29 | environment:
30 | - POSTGRES_HOST_AUTH_METHOD=trust
31 |
32 | volumes:
33 | db: {}
34 | build_cache: {}
35 |
--------------------------------------------------------------------------------
/vbt_new/priv/templates/entrypoint.sh.eex:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Add local user with USER_NAME
4 | # Either use the LOCAL_USER_ID if passed in at runtime or fallback to 9001
5 | USER_NAME=$APP_USER
6 | USER_ID=${LOCAL_USER_ID:-9001}
7 |
8 | # Add user if it doesn't exist
9 | useradd --uid $USER_ID \
10 | --shell /bin/bash \
11 | --non-unique \
12 | --comment "App user" \
13 | --create-home $USER_NAME \
14 | --home-dir /opt/app
15 |
16 | # Change ownership of library directories
17 | chown -R $USER_NAME. /opt/cache
18 | chown -R $USER_NAME. $WORKDIR
19 |
20 | runuser -l $USER_NAME -c "mkdir -p /opt/app/.ssh"
21 | runuser -l $USER_NAME -c "ssh-keyscan github.com > /opt/app/.ssh/known_hosts"
22 |
23 | # Run the command attached to the process with PID 1 so that signals get
24 | # passed to the process/app being run
25 | echo "Starting with USER=$USER_NAME UID=$USER_ID"
26 | exec /usr/local/sbin/pid1 -u $USER_NAME -g $USER_NAME "$@"
27 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Add local user with USER_NAME
4 | # Either use the LOCAL_USER_ID if passed in at runtime or fallback to 9001
5 | USER_NAME=$APP_USER
6 | USER_ID=${LOCAL_USER_ID:-9001}
7 |
8 | # Add user if it doesn't exist
9 | useradd --uid $USER_ID \
10 | --shell /bin/bash \
11 | --non-unique \
12 | --comment "App user" \
13 | --create-home $USER_NAME \
14 | --home-dir /opt/app
15 |
16 | # Change ownership of library directories
17 | chown -R $USER_NAME. /opt/cache
18 | chown -R $USER_NAME. $WORKDIR
19 |
20 | runuser -l $USER_NAME -c "mkdir -p /opt/app/.ssh"
21 | runuser -l $USER_NAME -c "ssh-keyscan github.com > /opt/app/.ssh/known_hosts"
22 |
23 | # Run the command attached to the process with PID 1 so that signals get
24 | # passed to the process/app being run
25 | echo "Starting with USER=$USER_NAME UID=$USER_ID"
26 | exec /usr/local/sbin/pid1 -u $USER_NAME -g $USER_NAME "$@"
27 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/lib/skafolder_tester_web/gettext.ex:
--------------------------------------------------------------------------------
1 | # credo:disable-for-this-file Credo.Check.Readability.Specs
2 |
3 | defmodule SkafolderTesterWeb.Gettext do
4 | @moduledoc """
5 | A module providing Internationalization with a gettext-based API.
6 |
7 | By using [Gettext](https://hexdocs.pm/gettext),
8 | your module gains a set of macros for translations, for example:
9 |
10 | import SkafolderTesterWeb.Gettext
11 |
12 | # Simple translation
13 | gettext("Here is the string to translate")
14 |
15 | # Plural translation
16 | ngettext("Here is the string to translate",
17 | "Here are the strings to translate",
18 | 3)
19 |
20 | # Domain-based translation
21 | dgettext("errors", "Here is the error message to translate")
22 |
23 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
24 | """
25 | use Gettext, otp_app: :skafolder_tester
26 | end
27 |
--------------------------------------------------------------------------------
/vbt_new/priv/templates/docker-compose.yml.eex:
--------------------------------------------------------------------------------
1 | version: '3.4'
2 | services:
3 | web:
4 | image: "<%= app %>:latest"
5 | ports:
6 | - "4000:4000"
7 | depends_on:
8 | - db
9 | volumes:
10 | - .:/opt/app
11 | - build_cache:/opt/cache
12 | - $SSH_AUTH_SOCK:${SSH_AUTH_SOCK}
13 | env_file: .env.development
14 | environment:
15 | - LOCAL_USER_ID=${LOCAL_USER_ID}
16 | - SSH_AUTH_SOCK=${SSH_AUTH_SOCK}
17 | tty: true
18 | stdin_open: true
19 | command: '/bin/bash -c "while true; do sleep 10; done;"'
20 | ulimits:
21 | nofile: 1024
22 | nproc: 63090
23 | db:
24 | image: "postgres:<%= Mix.Vbt.tool_versions().postgres.major %>.<%= Mix.Vbt.tool_versions().postgres.minor %>-alpine"
25 | volumes:
26 | - db:/var/lib/postgresql/data
27 | ports:
28 | - "5434:5432"
29 | environment:
30 | - POSTGRES_HOST_AUTH_METHOD=trust
31 |
32 | volumes:
33 | db: {}
34 | build_cache: {}
35 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 |
2 | FROM verybigthings/elixir:1.12 AS build
3 |
4 | ARG WORKDIR=/opt/app
5 | ARG APP_USER=user
6 |
7 | ENV WORKDIR=$WORKDIR
8 | ENV APP_USER=$APP_USER
9 | ENV CACHE_DIR=/opt/cache
10 | ENV MIX_HOME=$CACHE_DIR/mix
11 | ENV HEX_HOME=$CACHE_DIR/hex
12 | ENV BUILD_PATH=$CACHE_DIR/_build
13 | ENV REBAR_CACHE_DIR=$CACHE_DIR/rebar
14 |
15 | RUN apt-get update && apt-get install -y \
16 | bash \
17 | git \
18 | inotify-tools \
19 | less \
20 | locales \
21 | make \
22 | postgresql-client \
23 | postgresql-contrib \
24 | vim
25 |
26 | WORKDIR $WORKDIR
27 |
28 | ENV PHOENIX_VERSION 1.5.3
29 |
30 | RUN mix local.hex --force && \
31 | mix local.rebar --force
32 | RUN mix archive.install hex phx_new $PHOENIX_VERSION --force
33 |
34 | # Set entrypoint
35 | COPY entrypoint.sh /usr/local/bin/entrypoint.sh
36 | RUN chmod +x /usr/local/bin/entrypoint.sh
37 | ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
38 |
39 | CMD ["/bin/bash", "-c", "while true; do sleep 10; done;"]
40 |
--------------------------------------------------------------------------------
/vbt_new/priv/templates/lib/otp_app_web/views/error_view.ex.eex:
--------------------------------------------------------------------------------
1 | defmodule <%= Mix.Vbt.web_module_name() %>.ErrorView do
2 | use <%= Mix.Vbt.web_module_name() %>, :view
3 |
4 | def render("500.json", %{kind: kind, reason: reason, stack: stack}) do
5 | # sending formatted exception to frontend on develop, preview, and stage
6 | message =
7 | if System.get_env("RELEASE_LEVEL") in ~w/develop preview stage/,
8 | do: Exception.format(kind, reason, stack),
9 | else: "Internal Server Error"
10 |
11 | %{errors: [%{message: message}]}
12 | end
13 |
14 | # By default, Phoenix returns the status message from
15 | # the template name. For example, "404.json" becomes
16 | # "Not Found".
17 | def template_not_found(template, _assigns) do
18 | message = Phoenix.Controller.status_message_from_template(template)
19 |
20 | if Path.extname(template) == ".json",
21 | do: %{errors: [%{message: message}]},
22 | else: message
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/test/support/test_application.exs:
--------------------------------------------------------------------------------
1 | defmodule Credo.Test.FilenameGenerator do
2 | # credo:disable-for-this-file
3 |
4 | use GenServer
5 |
6 | def start_link(opts \\ []) do
7 | {:ok, _pid} = GenServer.start_link(__MODULE__, opts, name: __MODULE__)
8 | end
9 |
10 | def next do
11 | number = GenServer.call(__MODULE__, {:next})
12 | "test-untitled.#{number}.ex"
13 | end
14 |
15 | # callbacks
16 |
17 | def init(_) do
18 | {:ok, 1}
19 | end
20 |
21 | def handle_call({:next}, _from, current_state) do
22 | {:reply, current_state + 1, current_state + 1}
23 | end
24 | end
25 |
26 | defmodule Credo.Test.Application do
27 | use Application
28 |
29 | def start(_type, _args) do
30 | import Supervisor.Spec, warn: false
31 |
32 | children = [
33 | worker(Credo.Test.FilenameGenerator, [])
34 | ]
35 |
36 | opts = [strategy: :one_for_one, name: Credo.Test.Application.Supervisor]
37 | Supervisor.start_link(children, opts)
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/test/vbt/kubernetes/probe_test.exs:
--------------------------------------------------------------------------------
1 | defmodule VBT.Kubernetes.ProbeTest do
2 | use ExUnit.Case, async: true
3 | use Plug.Test
4 | alias VBT.Kubernetes.Probe
5 |
6 | describe "liveness" do
7 | test "responds with 200 on the configured liveness path" do
8 | conn = Probe.call(conn(:get, "/healthz"), Probe.init("/healthz"))
9 | assert conn.state == :set
10 | assert conn.status == 200
11 | assert conn.resp_body == ""
12 | end
13 |
14 | test "leaves connection intact on other paths" do
15 | in_conn = conn(:get, "/another_path")
16 | out_conn = Probe.call(in_conn, Probe.init("/healthz"))
17 | assert out_conn == in_conn
18 | assert out_conn.state == :unset
19 | end
20 |
21 | test "leaves connection intact if method is not get" do
22 | in_conn = conn(:post, "/healthz")
23 | out_conn = Probe.call(in_conn, Probe.init("/healthz"))
24 | assert out_conn == in_conn
25 | assert out_conn.state == :unset
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/.gitignore:
--------------------------------------------------------------------------------
1 | # The directory Mix will write compiled artifacts to.
2 | /_build/
3 |
4 | # If you run "mix test --cover", coverage assets end up here.
5 | /cover/
6 |
7 | # The directory Mix downloads your dependencies sources to.
8 | /deps/
9 |
10 | # Where 3rd-party dependencies like ExDoc output generated docs.
11 | /doc/
12 |
13 | # Ignore .fetch files in case you like to edit your project deps locally.
14 | /.fetch
15 |
16 | # If the VM crashes, it generates a dump, let's ignore it too.
17 | erl_crash.dump
18 |
19 | # Also ignore archive artifacts (built via "mix archive.build").
20 | *.ez
21 |
22 | # Ignore package tarball (built via "mix hex.build").
23 | skafolder_tester-*.tar
24 |
25 | # Since we are building assets from assets/,
26 | # we ignore priv/static. You may want to comment
27 | # this depending on your deployment strategy.
28 | /priv/static/
29 |
30 | # Build folder inside devstack container
31 | /_builds/
32 |
33 | # Ignore ssh folder generated by docker
34 | .ssh
35 |
--------------------------------------------------------------------------------
/vbt_new/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule VbtNew.MixProject do
2 | use Mix.Project
3 |
4 | def project do
5 | [
6 | app: :vbt_new,
7 | version: "0.1.0",
8 | elixir: "~> 1.12",
9 | start_permanent: Mix.env() == :prod,
10 | deps: deps(),
11 | preferred_cli_env: preferred_cli_env(),
12 | dialyzer: dialyzer(),
13 | aliases: aliases()
14 | ]
15 | end
16 |
17 | def application do
18 | [
19 | extra_applications: [:logger, :inets]
20 | ]
21 | end
22 |
23 | defp deps do
24 | [
25 | {:vbt, path: "..", only: [:dev, :test], runtime: false}
26 | ]
27 | end
28 |
29 | defp preferred_cli_env do
30 | [
31 | credo: :test,
32 | dialyzer: :test,
33 | "archive.build": :prod
34 | ]
35 | end
36 |
37 | defp dialyzer do
38 | [
39 | plt_add_apps: ~w/mix eex inets ssl/a
40 | ]
41 | end
42 |
43 | defp aliases do
44 | [
45 | "archive.build": ["compile", "archive.build --include-dot-files"]
46 | ]
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/test/support/skafolder_tester_test/web/test_plug.ex:
--------------------------------------------------------------------------------
1 | defmodule SkafolderTesterTest.Web.TestPlug do
2 | @behaviour Plug
3 | import Phoenix.ConnTest
4 |
5 | @spec dispatch((Plug.Conn.t() -> Plug.Conn.t())) :: Plug.Conn.t()
6 | def dispatch(fun) do
7 | code =
8 | fun
9 | |> :erlang.term_to_binary()
10 | |> Base.url_encode64(padding: false)
11 |
12 | build_conn()
13 | |> Plug.Conn.put_req_header("content-type", "application/json")
14 | |> Plug.Conn.put_req_header("accept", "application/json")
15 | |> dispatch(SkafolderTesterWeb.Endpoint, :get, "/test_execute/#{code}")
16 | end
17 |
18 | @impl Plug
19 | def init(opts), do: opts
20 |
21 | @impl Plug
22 | def call(conn, _opts) do
23 | with %Plug.Conn{method: "GET", path_info: ["test_execute", code]} <- conn do
24 | fun =
25 | code
26 | |> Base.url_decode64!(padding: false)
27 | |> :erlang.binary_to_term()
28 |
29 | fun.(conn)
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/lib/vbt/telemetry/oban.ex:
--------------------------------------------------------------------------------
1 | defmodule VBT.Telemetry.Oban do
2 | @moduledoc false
3 |
4 | # credo:disable-for-this-file Credo.Check.Readability.Specs
5 |
6 | require Logger
7 |
8 | def install_handler do
9 | events = Enum.map([:stop, :exception], &[:oban, :job, &1])
10 | :telemetry.attach_many(__MODULE__, events, &handle_event/4, nil)
11 | end
12 |
13 | defp handle_event([:oban, :job, :stop], _measure, meta, nil) do
14 | Logger.debug(~s/processed job id=#{meta.id} in queue #{inspect(meta.queue)}/)
15 | end
16 |
17 | defp handle_event([:oban, :job, :exception], _measure, meta, nil) do
18 | Logger.error("""
19 | failed processing job id=#{meta.id} in queue #{inspect(meta.queue)}:
20 |
21 | #{Exception.format(normalize_kind(meta.kind), meta.error, meta.stacktrace)}
22 | """)
23 | end
24 |
25 | defp normalize_kind(kind) when kind in ~w/error exit throw/a, do: kind
26 | defp normalize_kind({:EXIT, pid} = kind) when is_pid(pid), do: kind
27 | defp normalize_kind(_other), do: :error
28 | end
29 |
--------------------------------------------------------------------------------
/vbt_new/priv/templates/test/support/otp_app_test/web/test_plug.ex.eex:
--------------------------------------------------------------------------------
1 | defmodule <%= Mix.Vbt.test_module_name() %>.Web.TestPlug do
2 | @behaviour Plug
3 | import Phoenix.ConnTest
4 |
5 | @spec dispatch((Plug.Conn.t() -> Plug.Conn.t())) :: Plug.Conn.t()
6 | def dispatch(fun) do
7 | code =
8 | fun
9 | |> :erlang.term_to_binary()
10 | |> Base.url_encode64(padding: false)
11 |
12 | build_conn()
13 | |> Plug.Conn.put_req_header("content-type", "application/json")
14 | |> Plug.Conn.put_req_header("accept", "application/json")
15 | |> dispatch(<%= Mix.Vbt.web_module_name() %>.Endpoint, :get, "/test_execute/#{code}")
16 | end
17 |
18 | @impl Plug
19 | def init(opts), do: opts
20 |
21 | @impl Plug
22 | def call(conn, _opts) do
23 | with %Plug.Conn{method: "GET", path_info: ["test_execute", code]} <- conn do
24 | fun =
25 | code
26 | |> Base.url_decode64!(padding: false)
27 | |> :erlang.binary_to_term()
28 |
29 | fun.(conn)
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/lib/skafolder_tester_web/views/error_view.ex:
--------------------------------------------------------------------------------
1 | # credo:disable-for-this-file Credo.Check.Readability.Specs
2 |
3 | defmodule SkafolderTesterWeb.ErrorView do
4 | use SkafolderTesterWeb, :view
5 |
6 | def render("500.json", %{kind: kind, reason: reason, stack: stack}) do
7 | # sending formatted exception to frontend on develop, preview, and stage
8 | message =
9 | if System.get_env("RELEASE_LEVEL") in ~w/develop preview stage/,
10 | do: Exception.format(kind, reason, stack),
11 | else: "Internal Server Error"
12 |
13 | %{errors: [%{message: message}]}
14 | end
15 |
16 | # By default, Phoenix returns the status message from
17 | # the template name. For example, "404.json" becomes
18 | # "Not Found".
19 | def template_not_found(template, _assigns) do
20 | message = Phoenix.Controller.status_message_from_template(template)
21 |
22 | if Path.extname(template) == ".json",
23 | do: %{errors: [%{message: message}]},
24 | else: message
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/vbt_new/priv/templates/lib/otp_app_config.ex.eex:
--------------------------------------------------------------------------------
1 | defmodule <%= Mix.Vbt.config_module_name() %> do
2 | use Boundary
3 |
4 | use Provider,
5 | source: Provider.SystemEnv,
6 | params: [
7 | {:release_level, dev: "dev"},
8 |
9 | # database
10 | {:database_url, dev: dev_database_url()},
11 | {:database_pool_size, type: :integer, default: 10},
12 | {:database_ssl, type: :boolean, default: false},
13 |
14 | # endpoint
15 | {:host, dev: "localhost"},
16 | {:port, type: :integer, default: 4000, test: 4002},
17 | {:secret_key_base, dev: "<%= System.get_env("SECRET_KEY_BASE") || Mix.Vbt.random_string(64) %>"}
18 | ]
19 |
20 | if Mix.env() in ~w/dev test/a do
21 | defp dev_database_url do
22 | database_host = System.get_env("PGHOST", "localhost")
23 | database_name = if ci?(), do: "<%= app %>_test", else: "<%= app %>_#{unquote(Mix.env())}"
24 | "postgresql://postgres:postgres@#{database_host}/#{database_name}"
25 | end
26 |
27 | defp ci?, do: System.get_env("CI") == "true"
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/lib/skafolder_tester_config.ex:
--------------------------------------------------------------------------------
1 | defmodule SkafolderTesterConfig do
2 | use Boundary
3 |
4 | use Provider,
5 | source: Provider.SystemEnv,
6 | params: [
7 | {:release_level, dev: "dev"},
8 |
9 | # database
10 | {:database_url, dev: dev_database_url()},
11 | {:database_pool_size, type: :integer, default: 10},
12 | {:database_ssl, type: :boolean, default: false},
13 |
14 | # endpoint
15 | {:host, dev: "localhost"},
16 | {:port, type: :integer, default: 4000, test: 4002},
17 | {:secret_key_base, dev: "test_only_secret_key_base"}
18 | ]
19 |
20 | if Mix.env() in ~w/dev test/a do
21 | defp dev_database_url do
22 | database_host = System.get_env("PGHOST", "localhost")
23 |
24 | database_name =
25 | if ci?(), do: "skafolder_tester_test", else: "skafolder_tester_#{unquote(Mix.env())}"
26 |
27 | "postgresql://postgres:postgres@#{database_host}/#{database_name}"
28 | end
29 |
30 | defp ci?, do: System.get_env("CI") == "true"
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](http://vbt-common-docs.verybigthings.com)
2 |
3 | # VBT
4 |
5 | A library which contains various utilities used in different VBT projects.
6 |
7 | In addition, this library contains Credo checks which are specific to the VBT development process, and so it doesn't make sense for them to be submitted to the Credo project.
8 |
9 | ## Project scaffolding
10 |
11 | First, make sure that your system-wide Elixir version is 1.10 or higher. If you're using asdf, check the contents of `~/.tool-versions`.
12 |
13 | Next, install the most recent version of the scaffolder:
14 |
15 | ```
16 | wget -q http://vbt-common-docs.verybigthings.com/vbt_new.ez -O /tmp/vbt_new.ez && \
17 | mix archive.install --force /tmp/vbt_new.ez
18 | ```
19 |
20 | Run the previous command even if the scaffolder is already installed, because its code changes frequently.
21 |
22 | After the scaffolder is installed, invoke `mix help vbt.new` for usage instructions.
23 |
24 | The source code of the scaffolder is in the `vbt_new` folder.
25 |
--------------------------------------------------------------------------------
/lib/vbt/aws.ex:
--------------------------------------------------------------------------------
1 | defmodule VBT.Aws do
2 | @moduledoc """
3 | Helper module for working with AWS.
4 |
5 | This module together with other `VBT.Aws.*` modules provide various helper functions for working
6 | with AWS. These modules are wrappers around `ExAws`.
7 |
8 | If you need to directly interact with AWS, feel free to use `ExAws` function. For simplified
9 | testing, instead of directly invoking `ExAws` functions, use the `client/0` function from this
10 | module to obtain the implementation of `ExAws.Behaviour`. See `VBT.Aws.Test` for details.
11 | """
12 |
13 | @type response :: response(any)
14 | @type response(success_type) :: {:ok, success_type} | {:error, reason :: any}
15 |
16 | @doc """
17 | Returns AWS client module.
18 |
19 | Invoke this function when making AWS requests to obtain the module which implements
20 | `ExAws.Behaviour`. For example, to make a request:
21 |
22 | VBT.Aws.client().request(ExAws.S3.list_buckets(), region: "eu-west-1")
23 |
24 | By default, this function returns `ExAws`. In tests, you can setup a mock module using the
25 | `VBT.Aws.Test` module.
26 | """
27 | @spec client() :: module()
28 | def client, do: Application.get_env(:vbt, :ex_aws_client, ExAws)
29 | end
30 |
--------------------------------------------------------------------------------
/priv/test_repo/migrations/20200409064904_alter_accounts_adapt_tokens.exs:
--------------------------------------------------------------------------------
1 | defmodule VBT.TestRepo.Migrations.AlterAccountsAdaptTokens do
2 | use Ecto.Migration
3 |
4 | def up do
5 | execute "truncate table tokens_serial_id"
6 |
7 | alter table(:tokens_serial_id) do
8 | add :hash, :binary, null: false
9 | add :type, :string, null: false
10 | modify :account_id, :integer, null: true
11 | end
12 |
13 | create unique_index(:tokens_serial_id, [:hash])
14 |
15 | execute "truncate table tokens_uuid"
16 |
17 | alter table(:tokens_uuid) do
18 | add :hash, :binary, null: false
19 | add :type, :string, null: false
20 | modify :account_id, :uuid, null: true
21 | end
22 |
23 | create unique_index(:tokens_uuid, [:hash])
24 | end
25 |
26 | def down do
27 | execute "truncate table tokens_serial_id"
28 |
29 | alter table(:tokens_serial_id) do
30 | remove :type
31 | remove :hash
32 | modify :account_id, :integer, null: false
33 | end
34 |
35 | execute "truncate table tokens_uuid"
36 |
37 | alter table(:tokens_uuid) do
38 | remove :type
39 | remove :hash
40 | modify :account_id, :uuid, null: false
41 | end
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/.credo.exs:
--------------------------------------------------------------------------------
1 | %{
2 | configs: [
3 | %{
4 | name: "default",
5 | files: %{
6 | included: ["lib/", "src/", "test/", "web/", "apps/"],
7 | excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"]
8 | },
9 | requires: [],
10 | strict: true,
11 | color: true,
12 | checks: [
13 | # extra enabled checks
14 | {Credo.Check.Readability.AliasAs, []},
15 | {Credo.Check.Readability.SinglePipe, []},
16 | {Credo.Check.Readability.Specs, []},
17 | {Credo.Check.Readability.WithCustomTaggedTuple, []},
18 | {VBT.Credo.Check.Consistency.FileLocation,
19 | ignore_folder_namespace: %{
20 | "lib/<%= app %>_web" => ~w/channels controllers views/,
21 | "test/<%= app %>_web" => ~w/channels controllers views/
22 | }},
23 | {VBT.Credo.Check.Consistency.ModuleLayout, []},
24 | {VBT.Credo.Check.Graphql.MutationField, []},
25 | {VBT.Credo.Check.Readability.MultilineSimpleDo, []},
26 |
27 | # disabled checks
28 | {Credo.Check.Design.TagTODO, false},
29 |
30 | # obsolete checks
31 | {Credo.Check.Refactor.MapInto, false},
32 | {Credo.Check.Warning.LazyLogging, false}
33 | ]
34 | }
35 | ]
36 | }
37 |
--------------------------------------------------------------------------------
/vbt_new/.credo.exs:
--------------------------------------------------------------------------------
1 | %{
2 | configs: [
3 | %{
4 | name: "default",
5 | files: %{
6 | included: ["lib/", "src/", "test/", "web/", "apps/"],
7 | excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"]
8 | },
9 | requires: [],
10 | strict: true,
11 | color: true,
12 | checks: [
13 | # extra enabled checks
14 | {Credo.Check.Readability.AliasAs, []},
15 | {Credo.Check.Readability.SinglePipe, []},
16 | {Credo.Check.Readability.Specs, []},
17 | {Credo.Check.Readability.WithCustomTaggedTuple, []},
18 | {VBT.Credo.Check.Consistency.FileLocation,
19 | ignore_folder_namespace: %{
20 | "lib/<%= app %>_web" => ~w/channels controllers views/,
21 | "test/<%= app %>_web" => ~w/channels controllers views/
22 | }},
23 | {VBT.Credo.Check.Consistency.ModuleLayout, []},
24 | {VBT.Credo.Check.Readability.MultilineSimpleDo, []},
25 |
26 | # disabled checks
27 | {Credo.Check.Consistency.SpaceAroundOperators, false},
28 | {Credo.Check.Design.TagTODO, false},
29 |
30 | # obsolete checks
31 | {Credo.Check.Refactor.MapInto, false},
32 | {Credo.Check.Warning.LazyLogging, false}
33 | ]
34 | }
35 | ]
36 | }
37 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/lib/skafolder_tester_web/templates/page/index.html.eex:
--------------------------------------------------------------------------------
1 |
2 | <%= gettext "Welcome to %{name}!", name: "Phoenix" %>
3 | Peace of mind from prototype to production
4 |
5 |
6 |
7 |
8 | Resources
9 |
20 |
21 |
22 | Help
23 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: devstack devstack-build devstack-clean devstack-shell help
2 |
3 | DEFAULT_GOAL: help
4 |
5 | export LOCAL_USER_ID ?= $(shell id -u $$USER)
6 |
7 |
8 | # -----------------------
9 | # --- DOCKER DEVSTACK ---
10 | # -----------------------
11 |
12 | ## Builds the development Docker image
13 | devstack-build:
14 | @docker-compose build
15 |
16 | ## Stops all development containers
17 | devstack-clean:
18 | @docker-compose down -v
19 |
20 | ## Starts all development containers in the foreground
21 | devstack: devstack-build
22 | @docker-compose up
23 |
24 | ## Spawns an interactive Bash shell in development web container
25 | devstack-shell:
26 | @docker exec -e COLUMNS="`tput cols`" -e LINES="`tput lines`" -u ${LOCAL_USER_ID} -it $$(docker-compose ps -q web) /bin/bash -c "reset -w && /bin/bash"
27 |
28 | # ------------
29 | # --- HELP ---
30 | # ------------
31 |
32 | ## Shows the help menu
33 | help:
34 | @echo "Please use \`make ' where is one of\n\n"
35 | @awk '/^[a-zA-Z\-\_\/0-9]+:/ { \
36 | helpMessage = match(lastLine, /^## (.*)/); \
37 | if (helpMessage) { \
38 | helpCommand = substr($$1, 0, index($$1, ":")); \
39 | helpMessage = substr(lastLine, RSTART + 3, RLENGTH); \
40 | printf "%-30s %s\n", helpCommand, helpMessage; \
41 | } \
42 | } \
43 | { lastLine = $$0 }' $(MAKEFILE_LIST)
44 |
--------------------------------------------------------------------------------
/lib/vbt/absinthe/relay/type_resolver.ex:
--------------------------------------------------------------------------------
1 | defmodule VBT.Absinthe.Relay.TypeResolver do
2 | @moduledoc """
3 | Helper for defining mappings between Relay types and modules (typically Ecto schemas).
4 |
5 | iex> defmodule MyProjectWeb.ResolverHelper do
6 | ...> use VBT.Absinthe.Relay.TypeResolver, %{
7 | ...> MyProject.Schemas => %{
8 | ...> User => :user,
9 | ...> Organization => :organization
10 | ...> }
11 | ...> }
12 | ...> end
13 | iex> MyProjectWeb.ResolverHelper.module(:user)
14 | MyProject.Schemas.User
15 | iex> MyProjectWeb.ResolverHelper.type(MyProject.Schemas.User)
16 | :user
17 | """
18 |
19 | @doc false
20 | defmacro __using__(definition) do
21 | quote bind_quoted: [definition: definition] do
22 | for {scope, mappings} <- definition,
23 | {module, type} <- mappings do
24 | module = Module.concat(scope, module)
25 |
26 | @doc "Returns the module for the given type."
27 | @spec module(unquote(type)) :: unquote(module)
28 | def module(unquote(type)), do: unquote(module)
29 |
30 | @doc "Returns the type for the given module."
31 | @spec type(unquote(module)) :: unquote(type)
32 | def type(unquote(module)), do: unquote(type)
33 | end
34 | end
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/lib/skafolder_tester_web/channels/user_socket.ex:
--------------------------------------------------------------------------------
1 | # credo:disable-for-this-file Credo.Check.Readability.Specs
2 |
3 | defmodule SkafolderTesterWeb.UserSocket do
4 | use Phoenix.Socket
5 |
6 | ## Channels
7 | # channel "room:*", SkafolderTesterWeb.RoomChannel
8 |
9 | # Socket params are passed from the client and can
10 | # be used to verify and authenticate a user. After
11 | # verification, you can put default assigns into
12 | # the socket that will be set for all channels, ie
13 | #
14 | # {:ok, assign(socket, :user_id, verified_user_id)}
15 | #
16 | # To deny connection, return `:error`.
17 | #
18 | # See `Phoenix.Token` documentation for examples in
19 | # performing token verification on connect.
20 | @impl true
21 | def connect(_params, socket, _connect_info) do
22 | {:ok, socket}
23 | end
24 |
25 | # Socket id's are topics that allow you to identify all sockets for a given user:
26 | #
27 | # def id(socket), do: "user_socket:#{socket.assigns.user_id}"
28 | #
29 | # Would allow you to broadcast a "disconnect" event and terminate
30 | # all active sockets and channels for a given user:
31 | #
32 | # SkafolderTesterWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
33 | #
34 | # Returning `nil` makes this socket anonymous.
35 | @impl true
36 | def id(_socket), do: nil
37 | end
38 |
--------------------------------------------------------------------------------
/priv/test_repo/migrations/20200114120644_create_accounts.exs:
--------------------------------------------------------------------------------
1 | defmodule VBT.TestRepo.Migrations.CreateAccounts do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:accounts_serial_id, primary_key: false) do
6 | add :id, :serial, primary_key: true
7 | add :name, :string, null: false
8 | add :email, :string, null: false
9 | add :password_hash, :text, null: false
10 | end
11 |
12 | create unique_index(:accounts_serial_id, [:email])
13 |
14 | create table(:tokens_serial_id, primary_key: false) do
15 | add :id, :uuid, primary_key: true
16 | add :used_at, :utc_datetime
17 | add :expires_at, :utc_datetime, null: false
18 | add :account_id, references(:accounts_serial_id, type: :serial), null: false
19 | end
20 |
21 | create table(:accounts_uuid, primary_key: false) do
22 | add :id, :uuid, primary_key: true
23 | add :name, :string, null: false
24 | add :email, :string, null: false
25 | add :password_hash, :text, null: false
26 | end
27 |
28 | create unique_index(:accounts_uuid, [:email])
29 |
30 | create table(:tokens_uuid, primary_key: false) do
31 | add :id, :uuid, primary_key: true
32 | add :used_at, :utc_datetime
33 | add :expires_at, :utc_datetime, null: false
34 | add :account_id, references(:accounts_uuid, type: :uuid), null: false
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/lib/skafolder_tester_web/router.ex:
--------------------------------------------------------------------------------
1 | # credo:disable-for-this-file Credo.Check.Readability.Specs
2 |
3 | defmodule SkafolderTesterWeb.Router do
4 | use SkafolderTesterWeb, :router
5 |
6 | pipeline :browser do
7 | plug :accepts, ["html"]
8 | plug :fetch_session
9 | plug :fetch_flash
10 | plug :protect_from_forgery
11 | plug :put_secure_browser_headers
12 | end
13 |
14 | pipeline :api do
15 | plug :accepts, ["json"]
16 | end
17 |
18 | scope "/", SkafolderTesterWeb do
19 | pipe_through(:browser)
20 |
21 | get("/", PageController, :index)
22 | end
23 |
24 | # Other scopes may use custom stacks.
25 | # scope "/api", SkafolderTesterWeb do
26 | # pipe_through :api
27 | # end
28 |
29 | # Enables LiveDashboard only for development
30 | #
31 | # If you want to use the LiveDashboard in production, you should put
32 | # it behind authentication and allow only admins to access it.
33 | # If your application does not have an admins-only section yet,
34 | # you can use Plug.BasicAuth to set up some basic authentication
35 | # as long as you are also using SSL (which you should anyway).
36 | if Mix.env() in [:dev, :test] do
37 | import Phoenix.LiveDashboard.Router
38 |
39 | scope "/" do
40 | pipe_through(:browser)
41 | live_dashboard("/dashboard", metrics: SkafolderTesterWeb.Telemetry)
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/vbt_new/priv/templates/.credo.exs.eex:
--------------------------------------------------------------------------------
1 | %{
2 | configs: [
3 | %{
4 | name: "default",
5 | files: %{
6 | included: ["lib/", "src/", "test/", "web/", "apps/"],
7 | excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"]
8 | },
9 | requires: [],
10 | strict: true,
11 | color: true,
12 | checks: [
13 | # extra Credo checks
14 | {Credo.Check.Readability.AliasAs, []},
15 | {Credo.Check.Readability.SinglePipe, []},
16 | {Credo.Check.Readability.Specs, []},
17 | {Credo.Check.Readability.WithCustomTaggedTuple, []},
18 |
19 | # custom VBT checks
20 | {VBT.Credo.Check.Consistency.FileLocation,
21 | ignore_folder_namespace: %{
22 | "lib/<%= app %>_web" => ~w/channels controllers views/,
23 | "test/<%= app %>_web" => ~w/channels controllers views/
24 | }},
25 | {VBT.Credo.Check.Consistency.ModuleLayout, []},
26 | {VBT.Credo.Check.Graphql.MutationField, []},
27 | {VBT.Credo.Check.Readability.MultilineSimpleDo, []},
28 |
29 | # disabled checks
30 | {Credo.Check.Consistency.SpaceAroundOperators, false},
31 | {Credo.Check.Design.TagTODO, false},
32 | {Credo.Check.Readability.ModuleDoc, false},
33 |
34 | # obsolete checks
35 | {Credo.Check.Refactor.MapInto, false},
36 | {Credo.Check.Warning.LazyLogging, false}
37 | ]
38 | }
39 | ]
40 | }
41 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/.credo.exs:
--------------------------------------------------------------------------------
1 | %{
2 | configs: [
3 | %{
4 | name: "default",
5 | files: %{
6 | included: ["lib/", "src/", "test/", "web/", "apps/"],
7 | excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"]
8 | },
9 | requires: [],
10 | strict: true,
11 | color: true,
12 | checks: [
13 | # extra Credo checks
14 | {Credo.Check.Readability.AliasAs, []},
15 | {Credo.Check.Readability.SinglePipe, []},
16 | {Credo.Check.Readability.Specs, []},
17 | {Credo.Check.Readability.WithCustomTaggedTuple, []},
18 |
19 | # custom VBT checks
20 | {VBT.Credo.Check.Consistency.FileLocation,
21 | ignore_folder_namespace: %{
22 | "lib/skafolder_tester_web" => ~w/channels controllers views/,
23 | "test/skafolder_tester_web" => ~w/channels controllers views/
24 | }},
25 | {VBT.Credo.Check.Consistency.ModuleLayout, []},
26 | {VBT.Credo.Check.Graphql.MutationField, []},
27 | {VBT.Credo.Check.Readability.MultilineSimpleDo, []},
28 |
29 | # disabled checks
30 | {Credo.Check.Consistency.SpaceAroundOperators, false},
31 | {Credo.Check.Design.TagTODO, false},
32 | {Credo.Check.Readability.ModuleDoc, false},
33 |
34 | # obsolete checks
35 | {Credo.Check.Refactor.MapInto, false},
36 | {Credo.Check.Warning.LazyLogging, false}
37 | ]
38 | }
39 | ]
40 | }
41 |
--------------------------------------------------------------------------------
/lib/vbt/error.ex:
--------------------------------------------------------------------------------
1 | defmodule VBT.Error do
2 | @moduledoc """
3 | Module for defining a VBT business error.
4 |
5 | Business errors are used to return user-reportable errors to frontend clients. See
6 | `VBT.Absinthe.Relay.Schema` for details.
7 | """
8 |
9 | @doc false
10 | defmacro __using__(opts) do
11 | quote bind_quoted: [opts: opts] do
12 | defstruct [__vbt_error__: true] ++ Keyword.fetch!(opts, :fields)
13 | @type t :: %__MODULE__{}
14 | end
15 | end
16 | end
17 |
18 | defmodule VBT.BusinessError do
19 | @moduledoc "General purpose VBT business error."
20 | use VBT.Error, fields: [:error_code]
21 |
22 | @doc """
23 | Creates a VBT business error.
24 |
25 | The `scope` parameter can consist of multiple parts separated by the `.` character. At the
26 | very least supply a single part which is the operation name, or a standard VBT scope (e.g.
27 | `"registration"` or `"authentication"`).
28 |
29 | The prefix `com.vbt.` will be prepended to the given scope.
30 |
31 | The `reason` parameter represents the specific error reason (e.g. `"already_taken"`, or
32 | `"invalid_credentials"`).
33 |
34 | Example:
35 |
36 | iex> error = VBT.BusinessError.new("registration.login", "already_taken")
37 |
38 | iex> error.error_code
39 | "com.vbt.registration.login/already_taken"
40 | """
41 | @spec new(String.t(), String.t()) :: t
42 | def new(scope, reason), do: %__MODULE__{error_code: "com.vbt.#{scope}/#{reason}"}
43 | end
44 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/test/support/skafolder_tester_test/web/channel_case.ex:
--------------------------------------------------------------------------------
1 | # credo:disable-for-this-file Credo.Check.Design.AliasUsage
2 |
3 | defmodule SkafolderTesterTest.Web.ChannelCase do
4 | @moduledoc """
5 | This module defines the test case to be used by
6 | channel tests.
7 |
8 | Such tests rely on `Phoenix.ChannelTest` and also
9 | import other functionality to make it easier
10 | to build common data structures and query the data layer.
11 |
12 | Finally, if the test case interacts with the database,
13 | we enable the SQL sandbox, so changes done to the database
14 | are reverted at the end of every test. If you are using
15 | PostgreSQL, you can even run database tests asynchronously
16 | by setting `use SkafolderTesterTest.Web.ChannelCase, async: true`, although
17 | this option is not recommended for other databases.
18 | """
19 |
20 | use ExUnit.CaseTemplate
21 |
22 | using do
23 | quote do
24 | # Import conveniences for testing with channels
25 | import Phoenix.ChannelTest
26 | import SkafolderTesterTest.Web.ChannelCase
27 |
28 | # The default endpoint for testing
29 | @endpoint SkafolderTesterWeb.Endpoint
30 | end
31 | end
32 |
33 | setup tags do
34 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(SkafolderTester.Repo)
35 |
36 | unless tags[:async] do
37 | Ecto.Adapters.SQL.Sandbox.mode(SkafolderTester.Repo, {:shared, self()})
38 | end
39 |
40 | :ok
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/lib/vbt.ex:
--------------------------------------------------------------------------------
1 | defmodule VBT do
2 | @moduledoc "Common helper functions"
3 |
4 | # ------------------------------------------------------------------------
5 | # API
6 | # ------------------------------------------------------------------------
7 |
8 | @doc "Converts a boolean into `:ok | {:error, reason}`."
9 | @spec validate(boolean, error) :: :ok | {:error, error} when error: var
10 | def validate(condition, error), do: if(condition, do: :ok, else: {:error, error})
11 |
12 | @doc "Converts a boolean into `:ok | {:error, :unauthorized}`."
13 | @spec authorize(boolean) :: :ok | {:error, :unauthorized}
14 | def authorize(condition), do: validate(condition, :unauthorized)
15 |
16 | @doc """
17 | Performs recursive merge of two maps.
18 |
19 | Example:
20 |
21 | iex> map1 = %{a: 1, b: 2, c: %{d: 3}}
22 | iex> map2 = %{a: 4, c: %{e: 5}, f: 6}
23 | iex> VBT.deep_merge(map1, map2)
24 | %{a: 4, b: 2, c: %{d: 3, e: 5}, f: 6}
25 | """
26 | @spec deep_merge(map, map) :: map
27 | def deep_merge(left, right), do: Map.merge(left, right, &deep_resolve/3)
28 |
29 | # Key exists in both maps, and both values are maps as well.
30 | # These can be merged recursively.
31 | defp deep_resolve(_key, %{} = left, %{} = right), do: deep_merge(left, right)
32 |
33 | # Key exists in both maps, but at least one of the values is
34 | # NOT a map. We fall back to standard merge behavior, preferring
35 | # the value on the right.
36 | defp deep_resolve(_key, _left, right), do: right
37 | end
38 |
--------------------------------------------------------------------------------
/lib/vbt/graphql/types.ex:
--------------------------------------------------------------------------------
1 | defmodule VBT.Graphql.Types do
2 | @moduledoc "Custom VBT GraphQL types."
3 |
4 | use Absinthe.Schema.Notation
5 |
6 | object :business_error do
7 | description "VBT business error"
8 | field :error_code, non_null(:string)
9 | end
10 |
11 | scalar :datetime_usec, name: "DateTimeUsec" do
12 | description """
13 | Date and time with microseconds precision.
14 |
15 | In JSON format, the value is a valid ISO8601 datetime string. On the server side, the parsed
16 | value will be converted to UTC if there is an offset. The precision is normalized to
17 | microseconds.
18 |
19 | When encoding, the precision will be padded to microseconds if needed, and the encoded value
20 | will always be in the UTC time zone.
21 | """
22 |
23 | serialize &DateTime.to_iso8601(&1 && to_usec_precision(&1))
24 | parse &parse_datetime_usec/1
25 | end
26 |
27 | defp parse_datetime_usec(%Absinthe.Blueprint.Input.String{value: value}) do
28 | case DateTime.from_iso8601(value) do
29 | {:ok, datetime, _} -> {:ok, to_usec_precision(datetime)}
30 | {:error, _} -> :error
31 | end
32 | end
33 |
34 | defp parse_datetime_usec(%Absinthe.Blueprint.Input.Null{}), do: {:ok, nil}
35 | defp parse_datetime_usec(_), do: :error
36 |
37 | defp to_usec_precision(%{microsecond: {_value, 6}} = time_or_datetime),
38 | do: time_or_datetime
39 |
40 | defp to_usec_precision(%{microsecond: {value, _precision}} = time_or_datetime),
41 | do: %{time_or_datetime | microsecond: {value, 6}}
42 | end
43 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/lib/skafolder_tester_web/templates/layout/app.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | SkafolderTester · Phoenix Framework
8 | "/>
9 |
10 |
11 |
12 |
27 |
28 | <%= get_flash(@conn, :info) %>
29 | <%= get_flash(@conn, :error) %>
30 | <%= @inner_content %>
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/.github/workflows/test.yaml:
--------------------------------------------------------------------------------
1 | name: "Run tests"
2 |
3 | on:
4 | push:
5 | branches-ignore:
6 | - 'preview/*'
7 | - 'develop'
8 | - 'master'
9 | - 'release-*'
10 |
11 | jobs:
12 | test:
13 | name: Run tests
14 | runs-on: ubuntu-latest
15 |
16 | services:
17 | postgres:
18 | image: postgres:12.2
19 | env:
20 | POSTGRES_USER: "postgres"
21 | POSTGRES_PASSWORD: "postgres"
22 | ports:
23 | - 5432:5432
24 | # needed because the postgres container does not provide a healthcheck
25 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
26 | env:
27 | CACHE_VERSION: v1
28 | RELEASE_LEVEL: CI
29 | steps:
30 | - uses: actions/checkout@v2
31 |
32 | - uses: erlef/setup-beam@v1
33 | with:
34 | otp-version: '24.0'
35 | elixir-version: '1.12.2'
36 |
37 | - name: Restore cached deps
38 | uses: actions/cache@v2
39 | with:
40 | path: |
41 | deps
42 | _build
43 | key: deps-${{ env.CACHE_VERSION }}-${{ github.ref }}-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}
44 | restore-keys: |
45 | deps-${{ env.CACHE_VERSION }}-${{ github.ref }}-
46 | deps-${{ env.CACHE_VERSION }}-
47 |
48 | - name: Run CI checks
49 | uses: ./.github/workflows/actions/test
50 | with:
51 | ssh-private-key: ${{ secrets.VBT_DEPLOY_SSH_PRIVATE_KEY }}
52 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/test/support/skafolder_tester_test/web/conn_case.ex:
--------------------------------------------------------------------------------
1 | # credo:disable-for-this-file Credo.Check.Design.AliasUsage
2 | # credo:disable-for-this-file Credo.Check.Readability.AliasAs
3 |
4 | defmodule SkafolderTesterTest.Web.ConnCase do
5 | @moduledoc """
6 | This module defines the test case to be used by
7 | tests that require setting up a connection.
8 |
9 | Such tests rely on `Phoenix.ConnTest` and also
10 | import other functionality to make it easier
11 | to build common data structures and query the data layer.
12 |
13 | Finally, if the test case interacts with the database,
14 | we enable the SQL sandbox, so changes done to the database
15 | are reverted at the end of every test. If you are using
16 | PostgreSQL, you can even run database tests asynchronously
17 | by setting `use SkafolderTesterTest.Web.ConnCase, async: true`, although
18 | this option is not recommended for other databases.
19 | """
20 |
21 | use ExUnit.CaseTemplate
22 |
23 | using do
24 | quote do
25 | # Import conveniences for testing with connections
26 | import Plug.Conn
27 | import Phoenix.ConnTest
28 | import SkafolderTesterTest.Web.ConnCase
29 |
30 | alias SkafolderTesterWeb.Router.Helpers, as: Routes
31 |
32 | # The default endpoint for testing
33 | @endpoint SkafolderTesterWeb.Endpoint
34 | end
35 | end
36 |
37 | setup tags do
38 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(SkafolderTester.Repo)
39 |
40 | unless tags[:async] do
41 | Ecto.Adapters.SQL.Sandbox.mode(SkafolderTester.Repo, {:shared, self()})
42 | end
43 |
44 | {:ok, conn: Phoenix.ConnTest.build_conn()}
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/config/config.exs:
--------------------------------------------------------------------------------
1 | # This file is responsible for configuring your application
2 | # and its dependencies with the aid of the Mix.Config module.
3 | #
4 | # This configuration file is loaded before any dependency and
5 | # is restricted to this project.
6 |
7 | # General application configuration
8 | use Mix.Config
9 |
10 | config :sentry,
11 | dsn: {:system, "SENTRY_DSN"},
12 | environment_name: {:system, "RELEASE_LEVEL"},
13 | enable_source_code_context: true,
14 | root_source_code_path: File.cwd!(),
15 | included_environments: ~w(prod stage develop preview),
16 | release: SkafolderTester.MixProject.project()[:version]
17 |
18 | config :skafolder_tester, SkafolderTester.Repo,
19 | adapter: Ecto.Adapters.Postgres,
20 | migration_primary_key: [type: :binary_id],
21 | migration_timestamps: [type: :utc_datetime_usec],
22 | otp_app: :skafolder_tester
23 |
24 | config :skafolder_tester, ecto_repos: [SkafolderTester.Repo], generators: [binary_id: true]
25 |
26 | # Configures the endpoint
27 | config :skafolder_tester, SkafolderTesterWeb.Endpoint,
28 | render_errors: [view: SkafolderTesterWeb.ErrorView, accepts: ["html", "json"], layout: false],
29 | pubsub_server: SkafolderTester.PubSub,
30 | live_view: [signing_salt: "J3NfltcO"]
31 |
32 | # Configures Elixir's Logger
33 | config :logger, :console,
34 | format: "$time $metadata[$level] $message\n",
35 | metadata: [:request_id]
36 |
37 | # Use Jason for JSON parsing in Phoenix
38 | config :phoenix, :json_library, Jason
39 |
40 | # Import environment specific config. This must remain at the bottom
41 | # of this file so it overrides the configuration defined above.
42 | import_config "#{Mix.env()}.exs"
43 |
--------------------------------------------------------------------------------
/vbt_new/lib/mix/vbt/source_file.ex:
--------------------------------------------------------------------------------
1 | defmodule Mix.Vbt.SourceFile do
2 | @moduledoc false
3 |
4 | @type t :: %{name: String.t(), content: String.t(), format?: boolean, output: String.t()}
5 |
6 | @spec load!(String.t(), format?: boolean, output: String.t()) :: t
7 | def load!(name, opts \\ []) do
8 | %{
9 | name: name,
10 | content: File.read!(name),
11 | format?: Keyword.get(opts, :format?, true),
12 | output: Keyword.get(opts, :output, name)
13 | }
14 | end
15 |
16 | @spec store!(t) :: :ok
17 | def store!(file) do
18 | File.write!(file.output, format(file))
19 | if file.output != file.name, do: File.rm!(file.name)
20 | :ok
21 | end
22 |
23 | @spec add_to_module(t(), String.t()) :: t()
24 | def add_to_module(file, code) do
25 | content =
26 | String.replace(
27 | file.content,
28 |
29 | # Match the final non whitespace character before the last end.
30 | ~r/(^.*[^\s])(?=\s*end\s*$)/s,
31 |
32 | # Add new line and the desired code
33 | "\\1\n#{code} "
34 | )
35 |
36 | %{file | content: content}
37 | end
38 |
39 | @spec append(t, String.t()) :: t
40 | def append(file, extra_content), do: update_in(file.content, &(&1 <> extra_content))
41 |
42 | @spec prepend(t, String.t()) :: t
43 | def prepend(file, extra_content), do: update_in(file.content, &(extra_content <> &1))
44 |
45 | @spec format_code(String.t()) :: String.t()
46 | def format_code(content) do
47 | code =
48 | content
49 | |> Code.format_string!(locals_without_parens: [plug: :*, socket: :*])
50 | |> to_string()
51 |
52 | if String.ends_with?(code, "\n"), do: code, else: code <> "\n"
53 | end
54 |
55 | defp format(%{format?: false} = file), do: file.content
56 | defp format(file), do: format_code(file.content)
57 | end
58 |
--------------------------------------------------------------------------------
/vbt_new/priv/templates/.github/workflows/test.yaml.eex:
--------------------------------------------------------------------------------
1 | name: "Run tests"
2 |
3 | on:
4 | push:
5 | branches-ignore:
6 | - 'preview/*'
7 | - 'develop'
8 | - 'master'
9 | - 'release-*'
10 |
11 | jobs:
12 | test:
13 | name: Run tests
14 | runs-on: ubuntu-latest
15 |
16 | services:
17 | postgres:
18 | image: postgres:<%= "#{Mix.Vbt.tool_versions().postgres.major}.#{Mix.Vbt.tool_versions().postgres.minor}" %>
19 | env:
20 | POSTGRES_USER: "postgres"
21 | POSTGRES_PASSWORD: "postgres"
22 | ports:
23 | - 5432:5432
24 | # needed because the postgres container does not provide a healthcheck
25 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
26 | env:
27 | CACHE_VERSION: v1
28 | RELEASE_LEVEL: CI
29 | steps:
30 | - uses: actions/checkout@v2
31 |
32 | - uses: erlef/setup-beam@v1
33 | with:
34 | otp-version: '<%= "#{Mix.Vbt.tool_versions().erlang.major}.#{Mix.Vbt.tool_versions().erlang.minor}" %>'
35 | elixir-version: '<%= "#{Mix.Vbt.tool_versions().elixir.major}.#{Mix.Vbt.tool_versions().elixir.minor}.#{Mix.Vbt.tool_versions().elixir.patch}" %>'
36 |
37 | - name: Restore cached deps
38 | uses: actions/cache@v2
39 | with:
40 | path: |
41 | deps
42 | _build
43 | key: deps-${{ env.CACHE_VERSION }}-${{ github.ref }}-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}
44 | restore-keys: |
45 | deps-${{ env.CACHE_VERSION }}-${{ github.ref }}-
46 | deps-${{ env.CACHE_VERSION }}-
47 |
48 | - name: Run CI checks
49 | uses: ./.github/workflows/actions/test
50 | with:
51 | ssh-private-key: ${{ secrets.VBT_DEPLOY_SSH_PRIVATE_KEY }}
52 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/lib/skafolder_tester_web/views/error_helpers.ex:
--------------------------------------------------------------------------------
1 | # credo:disable-for-this-file Credo.Check.Readability.Specs
2 |
3 | defmodule SkafolderTesterWeb.ErrorHelpers do
4 | @moduledoc """
5 | Conveniences for translating and building error messages.
6 | """
7 |
8 | use Phoenix.HTML
9 |
10 | @doc """
11 | Generates tag for inlined form input errors.
12 | """
13 | def error_tag(form, field) do
14 | Enum.map(Keyword.get_values(form.errors, field), fn error ->
15 | content_tag(:span, translate_error(error),
16 | class: "invalid-feedback",
17 | phx_feedback_for: input_name(form, field)
18 | )
19 | end)
20 | end
21 |
22 | @doc """
23 | Translates an error message using gettext.
24 | """
25 | def translate_error({msg, opts}) do
26 | # When using gettext, we typically pass the strings we want
27 | # to translate as a static argument:
28 | #
29 | # # Translate "is invalid" in the "errors" domain
30 | # dgettext("errors", "is invalid")
31 | #
32 | # # Translate the number of files with plural rules
33 | # dngettext("errors", "1 file", "%{count} files", count)
34 | #
35 | # Because the error messages we show in our forms and APIs
36 | # are defined inside Ecto, we need to translate them dynamically.
37 | # This requires us to call the Gettext module passing our gettext
38 | # backend as first argument.
39 | #
40 | # Note we use the "errors" domain, which means translations
41 | # should be written to the errors.po file. The :count option is
42 | # set by Ecto and indicates we should also apply plural rules.
43 | if count = opts[:count] do
44 | Gettext.dngettext(SkafolderTesterWeb.Gettext, "errors", msg, msg, count, opts)
45 | else
46 | Gettext.dgettext(SkafolderTesterWeb.Gettext, "errors", msg, opts)
47 | end
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/lib/vbt/credo/check/consistency/module_layout.ex:
--------------------------------------------------------------------------------
1 | # credo:disable-for-this-file Credo.Check.Readability.Specs
2 | defmodule VBT.Credo.Check.Consistency.ModuleLayout do
3 | @moduledoc false
4 |
5 | use Credo.Check,
6 | category: :warning,
7 | base_priority: :high,
8 | explanations: [
9 | check: """
10 | Module parts should appear in the following order:
11 |
12 | 1. @shortdoc
13 | 2. @moduledoc
14 | 3. @behaviour
15 | 4. use
16 | 5. import
17 | 6. alias
18 | 7. require
19 | 8. custom module attributes
20 | 9. defstruct
21 | 10. @opaque
22 | 11. @type
23 | 12. @typep
24 | 13. @callback
25 | 14. @macrocallback
26 | 15. @optional_callbacks
27 | 16. public guards
28 | 17. public macros
29 | 18. public functions
30 | 19. behaviour callbacks
31 | 20. private functions
32 |
33 | This order has been adapted from https://github.com/christopheradams/elixir_style_guide#module-attribute-ordering.
34 | """
35 | ]
36 |
37 | alias Credo.Check.Readability.StrictModuleLayout
38 |
39 | @doc false
40 | def run(source_file, _params) do
41 | source_file
42 | |> StrictModuleLayout.run(
43 | order: ~w/
44 | shortdoc
45 | moduledoc
46 | behaviour
47 | use
48 | import
49 | alias
50 | require
51 | module_attribute
52 | defstruct
53 | opaque
54 | type
55 | typep
56 | callback
57 | macrocallback
58 | optional_callbacks
59 | private_macro
60 | public_guard
61 | public_macro
62 | public_fun
63 | callback_impl
64 | private_fun
65 | /a,
66 | ignore: [:private_macro, :private_guard]
67 | )
68 | |> Enum.map(&%Credo.Issue{&1 | check: __MODULE__})
69 | end
70 | end
71 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/test/support/skafolder_tester_test/data_case.ex:
--------------------------------------------------------------------------------
1 | # credo:disable-for-this-file Credo.Check.Readability.Specs
2 | # credo:disable-for-this-file Credo.Check.Design.AliasUsage
3 |
4 | defmodule SkafolderTesterTest.DataCase do
5 | @moduledoc """
6 | This module defines the setup for tests requiring
7 | access to the application's data layer.
8 |
9 | You may define functions here to be used as helpers in
10 | your tests.
11 |
12 | Finally, if the test case interacts with the database,
13 | we enable the SQL sandbox, so changes done to the database
14 | are reverted at the end of every test. If you are using
15 | PostgreSQL, you can even run database tests asynchronously
16 | by setting `use SkafolderTester.DataCase, async: true`, although
17 | this option is not recommended for other databases.
18 | """
19 |
20 | use ExUnit.CaseTemplate
21 |
22 | using do
23 | quote do
24 | alias SkafolderTester.Repo
25 |
26 | import Ecto
27 | import Ecto.Changeset
28 | import Ecto.Query
29 | import SkafolderTester.DataCase
30 | end
31 | end
32 |
33 | setup tags do
34 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(SkafolderTester.Repo)
35 |
36 | unless tags[:async] do
37 | Ecto.Adapters.SQL.Sandbox.mode(SkafolderTester.Repo, {:shared, self()})
38 | end
39 |
40 | :ok
41 | end
42 |
43 | @doc """
44 | A helper that transforms changeset errors into a map of messages.
45 |
46 | assert {:error, changeset} = Accounts.create_user(%{password: "short"})
47 | assert "password is too short" in errors_on(changeset).password
48 | assert %{password: ["password is too short"]} = errors_on(changeset)
49 |
50 | """
51 | def errors_on(changeset) do
52 | Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
53 | Regex.replace(~r"%{(\w+)}", message, fn _, key ->
54 | opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
55 | end)
56 | end)
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/test/vbt/credo/check/graphql/mutation_field_test.exs:
--------------------------------------------------------------------------------
1 | defmodule VBT.Credo.Check.Graphql.MutationFieldTest do
2 | use Credo.Test.Case
3 |
4 | alias VBT.Credo.Check.Graphql.MutationField
5 |
6 | test "reports error on non-payload mutation fields" do
7 | [issue] =
8 | """
9 | defmodule Test do
10 | use VBT.Absinthe.Relay.Schema
11 |
12 | mutation do
13 | payload field :foo do
14 | end
15 |
16 | field :bar do
17 | end
18 |
19 | payload field :baz do
20 | end
21 | end
22 |
23 | query do
24 | field :qux do
25 | end
26 | end
27 | end
28 | """
29 | |> to_source_file()
30 | |> run_check(MutationField)
31 | |> assert_issue()
32 |
33 | assert issue.message == "Mutation field :bar is not a payload field."
34 | end
35 |
36 | test "correctly handles nested modules" do
37 | [issue] =
38 | """
39 | defmodule Test do
40 | defmodule NotSchema do
41 | end
42 |
43 | defmodule Schema do
44 | use VBT.Absinthe.Relay.Schema
45 |
46 | mutation do
47 | field :foo do
48 | end
49 | end
50 | end
51 |
52 | defmodule AlsoNotSchema do
53 | end
54 | end
55 | """
56 | |> to_source_file()
57 | |> run_check(MutationField)
58 | |> assert_issue()
59 |
60 | assert issue.scope == "Test.Schema"
61 | end
62 |
63 | test "ignores non-schema modules" do
64 | """
65 | defmodule Test do
66 | mutation do
67 | field :foo do
68 | end
69 | end
70 |
71 | defmodule Schema do
72 | use VBT.Absinthe.Relay.Schema
73 |
74 | defmodule NotSchema do
75 | mutation do
76 | field :foo do
77 | end
78 | end
79 | end
80 | end
81 |
82 | mutation do
83 | field :bar do
84 | end
85 | end
86 | end
87 | """
88 | |> to_source_file()
89 | |> run_check(MutationField)
90 | |> refute_issues()
91 | end
92 | end
93 |
--------------------------------------------------------------------------------
/test/vbt/fixed_job_test.exs:
--------------------------------------------------------------------------------
1 | defmodule VBT.FixedJobTest do
2 | use ExUnit.Case, async: true
3 | import Periodic.Test
4 | alias VBT.FixedJob
5 |
6 | for property <- ~w/minute hour day_of_week day month year/a do
7 | @property property
8 | @expected_value if @property == :day_of_week, do: :monday, else: 1
9 | @unexpected_value if @property == :day_of_week, do: :tuesday, else: 2
10 |
11 | test "job is started if current time matches the `#{@property}` key" do
12 | scheduler = start_scheduler!(%{@property => @expected_value})
13 | FixedJob.set_time(scheduler, %{@property => @expected_value})
14 | assert sync_tick(scheduler) == {:ok, :normal}
15 | end
16 |
17 | test "job isn't started if current time doesn't match the `#{@property}` key" do
18 | scheduler = start_scheduler!(%{@property => @expected_value})
19 | FixedJob.set_time(scheduler, %{@property => @unexpected_value})
20 | assert sync_tick(scheduler) == {:error, :job_not_started}
21 | end
22 | end
23 |
24 | test "job is started if current time matches keys in filter" do
25 | scheduler = start_scheduler!(%{day: 1, minute: 1})
26 |
27 | FixedJob.set_time(scheduler, %{year: 1, day: 1, minute: 1})
28 | assert sync_tick(scheduler) == {:ok, :normal}
29 |
30 | FixedJob.set_time(scheduler, %{day: 1, hour: 2, minute: 1})
31 | assert sync_tick(scheduler) == {:ok, :normal}
32 | end
33 |
34 | test "job isn't started if current time doesn't match keys in filter" do
35 | scheduler = start_scheduler!(%{day: 1, minute: 1})
36 |
37 | FixedJob.set_time(scheduler, %{day: 2, minute: 1})
38 | assert sync_tick(scheduler) == {:error, :job_not_started}
39 |
40 | FixedJob.set_time(scheduler, %{day: 1, minute: 2})
41 | assert sync_tick(scheduler) == {:error, :job_not_started}
42 | end
43 |
44 | defp start_scheduler!(filter, opts \\ []) do
45 | test_process = self()
46 |
47 | opts =
48 | Keyword.merge(
49 | [mode: :manual, when: filter, run: fn -> send(test_process, :job_started) end],
50 | opts
51 | )
52 |
53 | start_supervised!({FixedJob, opts})
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/config/dev.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # Configure your database
4 | config :skafolder_tester, SkafolderTester.Repo, show_sensitive_data_on_connection_error: true
5 |
6 | # For development, we disable any cache and enable
7 | # debugging and code reloading.
8 | #
9 | # The watchers configuration can be used to run external
10 | # watchers to your application. For example, we use it
11 | # with webpack to recompile .js and .css sources.
12 | config :skafolder_tester, SkafolderTesterWeb.Endpoint,
13 | debug_errors: true,
14 | code_reloader: true,
15 | check_origin: false,
16 | watchers: []
17 |
18 | # ## SSL Support
19 | #
20 | # In order to use HTTPS in development, a self-signed
21 | # certificate can be generated by running the following
22 | # Mix task:
23 | #
24 | # mix phx.gen.cert
25 | #
26 | # Note that this task requires Erlang/OTP 20 or later.
27 | # Run `mix help phx.gen.cert` for more information.
28 | #
29 | # The `http:` config above can be replaced with:
30 | #
31 | # https: [
32 | # port: 4001,
33 | # cipher_suite: :strong,
34 | # keyfile: "priv/cert/selfsigned_key.pem",
35 | # certfile: "priv/cert/selfsigned.pem"
36 | # ],
37 | #
38 | # If desired, both `http:` and `https:` keys can be
39 | # configured to run both http and https servers on
40 | # different ports.
41 |
42 | # Watch static and templates for browser reloading.
43 | config :skafolder_tester, SkafolderTesterWeb.Endpoint,
44 | live_reload: [
45 | patterns: [
46 | ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
47 | ~r"priv/gettext/.*(po)$",
48 | ~r"lib/skafolder_tester_web/(live|views)/.*(ex)$",
49 | ~r"lib/skafolder_tester_web/templates/.*(eex)$"
50 | ]
51 | ]
52 |
53 | # Do not include metadata nor timestamps in development logs
54 | config :logger, :console, format: "[$level] $message\n"
55 |
56 | # Set a higher stacktrace during development. Avoid configuring such
57 | # in production as building large stacktraces may be expensive.
58 | config :phoenix, :stacktrace_depth, 20
59 |
60 | # Initialize plugs at runtime for faster development compilation
61 | config :phoenix, :plug_init_mode, :runtime
62 |
--------------------------------------------------------------------------------
/test/vbt/auth_test.exs:
--------------------------------------------------------------------------------
1 | defmodule VBT.AuthTest do
2 | use VBT.Graphql.Case, async: true, endpoint: VBT.GraphqlServer, api_path: "/"
3 | import Phoenix.ChannelTest
4 | @endpoint VBT.GraphqlServer
5 |
6 | describe "GraphQL authentication" do
7 | test "correctly decodes valid token" do
8 | token = authenticate!("some_login")
9 | assert {:ok, data} = current_user(token)
10 | assert data.current_user == "some_login"
11 | end
12 |
13 | test "rejects expired token" do
14 | token = authenticate!("some_login")
15 | assert {:error, response} = current_user(token, max_age: 0)
16 | assert "token_expired" in errors(response)
17 | end
18 |
19 | test "rejects invalid token" do
20 | assert {:error, response} = current_user("invalid token")
21 | assert "token_invalid" in errors(response)
22 | end
23 |
24 | test "rejects missing token" do
25 | assert {:error, response} = current_user(nil)
26 | assert "token_missing" in errors(response)
27 | end
28 | end
29 |
30 | describe "Phoenix socket authentication" do
31 | test "correctly decodes valid token" do
32 | token = authenticate!("some_login")
33 | assert {:ok, socket} = connect_to_socket(token)
34 | assert(socket.id == "user:some_login")
35 | end
36 |
37 | test "rejects expired token" do
38 | token = authenticate!("some_login")
39 | assert connect_to_socket(token, max_age: 0) == :error
40 | end
41 |
42 | test "rejects invalid token" do
43 | assert connect_to_socket("invalid token", max_age: 0) == :error
44 | end
45 |
46 | test "rejects empty token" do
47 | assert connect_to_socket(nil, max_age: 0) == :error
48 | end
49 | end
50 |
51 | defp authenticate!(login),
52 | do: call!(~s/query {auth_token(login: "#{login}")}/).auth_token
53 |
54 | defp current_user(token, opts \\ []),
55 | do: call(~s/query {current_user(max_age: #{opts[:max_age] || "null"})}/, auth: token)
56 |
57 | defp connect_to_socket(token, opts \\ []) do
58 | connect(
59 | VBT.GraphqlServer.Socket,
60 | Map.merge(%{"authorization" => "Bearer #{token}"}, Map.new(opts))
61 | )
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/test/vbt/absinthe/relay/schema_test.exs:
--------------------------------------------------------------------------------
1 | defmodule VBT.Absinthe.Relay.SchemaTest do
2 | use VBT.Graphql.Case, async: true, endpoint: __MODULE__.TestServer, api_path: "/"
3 |
4 | setup do
5 | Application.put_env(:vbt, __MODULE__.TestServer, [])
6 | start_supervised(__MODULE__.TestServer)
7 | :ok
8 | end
9 |
10 | test "correctly supplies a union result" do
11 | assert some_field(true) == {:ok, %{response: "some success"}}
12 | assert some_field(false) == {:ok, %{error_code: "com.vbt.some_field/some_error"}}
13 | end
14 |
15 | defp some_field(success?) do
16 | query = """
17 | mutation {
18 | some_field(input: {success: #{success?}}) {
19 | result {
20 | ... on SomeFieldPayloadSuccess { response }
21 | ... on BusinessError { error_code }
22 | }
23 | }
24 | }
25 | """
26 |
27 | with {:ok, response} <- call(query),
28 | do: {:ok, response.some_field.result}
29 | end
30 |
31 | defmodule TestServer do
32 | @moduledoc false
33 | use Phoenix.Endpoint, otp_app: :vbt
34 | plug Absinthe.Plug, schema: __MODULE__.Schema
35 |
36 | defmodule Schema do
37 | @moduledoc false
38 | use VBT.Absinthe.Relay.Schema
39 |
40 | query do
41 | end
42 |
43 | mutation do
44 | payload field :some_field do
45 | input do
46 | field :success, non_null(:boolean)
47 | end
48 |
49 | output do
50 | field :result, payload_type(:result)
51 |
52 | union payload_type(:result) do
53 | types [payload_type(:success), :business_error]
54 | resolve_type fn result, _ -> error_type(result) || payload_type(:success) end
55 | end
56 |
57 | object payload_type(:success) do
58 | field :response, non_null(:string)
59 | end
60 | end
61 |
62 | resolve payload_resolver(fn input, _ ->
63 | if input.success,
64 | do: {:ok, %{response: "some success"}},
65 | else: {:error, VBT.BusinessError.new("some_field", "some_error")}
66 | end)
67 | end
68 | end
69 | end
70 | end
71 | end
72 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/.github/workflows/prod.yaml:
--------------------------------------------------------------------------------
1 | name: "Test, build & deploy - prod env"
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | test:
10 | name: Run tests
11 | runs-on: ubuntu-latest
12 |
13 | services:
14 | postgres:
15 | image: postgres:11.7
16 | env:
17 | POSTGRES_USER: "postgres"
18 | POSTGRES_PASSWORD: "postgres"
19 | ports:
20 | - 5432:5432
21 | # needed because the postgres container does not provide a healthcheck
22 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
23 | env:
24 | CACHE_VERSION: v1
25 | RELEASE_LEVEL: CI
26 | steps:
27 | - uses: actions/checkout@v2
28 |
29 | - uses: erlef/setup-beam@v1
30 | with:
31 | otp-version: '24.0'
32 | elixir-version: '1.12.2'
33 |
34 | - name: Restore cached deps
35 | uses: actions/cache@v2
36 | with:
37 | path: |
38 | deps
39 | _build
40 | key: deps-${{ env.CACHE_VERSION }}-${{ github.ref }}-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}
41 | restore-keys: |
42 | deps-${{ env.CACHE_VERSION }}-${{ github.ref }}-
43 | deps-${{ env.CACHE_VERSION }}-
44 |
45 | - name: Run CI checks
46 | uses: ./.github/workflows/actions/test
47 | with:
48 | ssh-private-key: ${{ secrets.VBT_DEPLOY_SSH_PRIVATE_KEY }}
49 |
50 | build:
51 | needs: test
52 | name: Build, push, and deploy
53 | runs-on: ubuntu-latest
54 |
55 | steps:
56 | - uses: actions/checkout@v2
57 |
58 | - name: Deploy
59 | uses: ./.github/workflows/actions/deploy
60 | with:
61 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
62 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
63 | cluster-name: vbt-cluster
64 | github-token: ${{ secrets.GITHUB_TOKEN }}
65 | keybase-paperkey: ${{ secrets.KEYBASE_PAPERKEY }}
66 | keybase-username: ${{ secrets.KEYBASE_USERNAME }}
67 | release-level: prod
68 | ssh-private-key: ${{ secrets.VBT_DEPLOY_SSH_PRIVATE_KEY }}
69 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/.github/workflows/develop.yaml:
--------------------------------------------------------------------------------
1 | name: "Test, build & deploy - develop env"
2 |
3 | on:
4 | push:
5 | branches:
6 | - develop
7 |
8 | jobs:
9 | test:
10 | name: Run tests
11 | runs-on: ubuntu-latest
12 |
13 | services:
14 | postgres:
15 | image: postgres:11.7
16 | env:
17 | POSTGRES_USER: "postgres"
18 | POSTGRES_PASSWORD: "postgres"
19 | ports:
20 | - 5432:5432
21 | # needed because the postgres container does not provide a healthcheck
22 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
23 | env:
24 | CACHE_VERSION: v1
25 | RELEASE_LEVEL: CI
26 | steps:
27 | - uses: actions/checkout@v2
28 |
29 | - uses: erlef/setup-beam@v1
30 | with:
31 | otp-version: '24.0'
32 | elixir-version: '1.12.2'
33 |
34 | - name: Restore cached deps
35 | uses: actions/cache@v2
36 | with:
37 | path: |
38 | deps
39 | _build
40 | key: deps-${{ env.CACHE_VERSION }}-${{ github.ref }}-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}
41 | restore-keys: |
42 | deps-${{ env.CACHE_VERSION }}-${{ github.ref }}-
43 | deps-${{ env.CACHE_VERSION }}-
44 |
45 | - name: Run CI checks
46 | uses: ./.github/workflows/actions/test
47 | with:
48 | ssh-private-key: ${{ secrets.VBT_DEPLOY_SSH_PRIVATE_KEY }}
49 |
50 | build:
51 | needs: test
52 | name: Build, push, and deploy
53 | runs-on: ubuntu-latest
54 |
55 | steps:
56 | - uses: actions/checkout@v2
57 |
58 | - name: Deploy
59 | uses: ./.github/workflows/actions/deploy
60 | with:
61 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
62 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
63 | cluster-name: vbt-cluster
64 | github-token: ${{ secrets.GITHUB_TOKEN }}
65 | keybase-paperkey: ${{ secrets.KEYBASE_PAPERKEY }}
66 | keybase-username: ${{ secrets.KEYBASE_USERNAME }}
67 | release-level: develop
68 | ssh-private-key: ${{ secrets.VBT_DEPLOY_SSH_PRIVATE_KEY }}
69 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/.github/workflows/stage.yaml:
--------------------------------------------------------------------------------
1 | name: "Test, build & deploy - stage env"
2 |
3 | on:
4 | push:
5 | branches:
6 | - 'release-*'
7 |
8 | jobs:
9 | test:
10 | name: Run tests
11 | runs-on: ubuntu-latest
12 |
13 | services:
14 | postgres:
15 | image: postgres:11.7
16 | env:
17 | POSTGRES_USER: "postgres"
18 | POSTGRES_PASSWORD: "postgres"
19 | ports:
20 | - 5432:5432
21 | # needed because the postgres container does not provide a healthcheck
22 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
23 | env:
24 | CACHE_VERSION: v1
25 | RELEASE_LEVEL: CI
26 | steps:
27 | - uses: actions/checkout@v2
28 |
29 | - uses: erlef/setup-beam@v1
30 | with:
31 | otp-version: '24.0'
32 | elixir-version: '1.12.2'
33 |
34 | - name: Restore cached deps
35 | uses: actions/cache@v2
36 | with:
37 | path: |
38 | deps
39 | _build
40 | key: deps-${{ env.CACHE_VERSION }}-${{ github.ref }}-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}
41 | restore-keys: |
42 | deps-${{ env.CACHE_VERSION }}-${{ github.ref }}-
43 | deps-${{ env.CACHE_VERSION }}-
44 |
45 | - name: Run CI checks
46 | uses: ./.github/workflows/actions/test
47 | with:
48 | ssh-private-key: ${{ secrets.VBT_DEPLOY_SSH_PRIVATE_KEY }}
49 |
50 | build:
51 | needs: test
52 | name: Build, push, and deploy
53 | runs-on: ubuntu-latest
54 |
55 | steps:
56 | - uses: actions/checkout@v2
57 |
58 | - name: Deploy
59 | uses: ./.github/workflows/actions/deploy
60 | with:
61 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
62 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
63 | cluster-name: vbt-cluster
64 | github-token: ${{ secrets.GITHUB_TOKEN }}
65 | keybase-paperkey: ${{ secrets.KEYBASE_PAPERKEY }}
66 | keybase-username: ${{ secrets.KEYBASE_USERNAME }}
67 | release-level: stage
68 | ssh-private-key: ${{ secrets.VBT_DEPLOY_SSH_PRIVATE_KEY }}
69 |
--------------------------------------------------------------------------------
/test/vbt/telemetry/oban_test.exs:
--------------------------------------------------------------------------------
1 | defmodule VBT.Telemetry.ObanTest do
2 | # credo:disable-for-this-file Credo.Check.Readability.Specs
3 |
4 | use ExUnit.Case, async: false
5 | import ExUnit.CaptureLog
6 | alias __MODULE__.TestQueue
7 | alias Ecto.Adapters.SQL.Sandbox
8 |
9 | setup do
10 | Sandbox.checkout(VBT.TestRepo)
11 | end
12 |
13 | test "logs success" do
14 | on_exit(fn -> Logger.configure(level: :warn) end)
15 | Logger.configure(level: :debug)
16 |
17 | job = TestQueue.enqueue(fn -> :ok end)
18 | log = capture_log(fn -> TestQueue.drain_queue() end)
19 | assert log =~ ~s/processed job id=#{job.id} in queue "test_queue"/
20 | end
21 |
22 | test "logs exception" do
23 | log = run_queued_and_capture_error_log(fn -> raise("some error") end)
24 | assert log =~ ~s/some error/
25 | end
26 |
27 | test "logs erlang error" do
28 | log = run_queued_and_capture_error_log(fn -> :erlang.error("some error") end)
29 | assert log =~ ~s/some error/
30 | end
31 |
32 | test "logs exit" do
33 | log = run_queued_and_capture_error_log(fn -> exit("some error") end)
34 | assert log =~ ~s/some error/
35 | end
36 |
37 | test "logs throw" do
38 | log = run_queued_and_capture_error_log(fn -> throw("some error") end)
39 | assert log =~ ~s/some error/
40 | end
41 |
42 | test "logs error tuple" do
43 | log = run_queued_and_capture_error_log(fn -> {:error, "some error"} end)
44 | assert log =~ ~s/some error/
45 | end
46 |
47 | defp run_queued_and_capture_error_log(fun) do
48 | job = TestQueue.enqueue(fun)
49 | log = capture_log(fn -> TestQueue.drain_queue() end)
50 | assert log =~ ~s/failed processing job id=#{job.id} in queue "test_queue"/
51 | log
52 | end
53 |
54 | defmodule TestQueue do
55 | use Oban.Worker, queue: "test_queue"
56 |
57 | def enqueue(fun) do
58 | encoded_fun = fun |> :erlang.term_to_binary() |> Base.encode64(padding: false)
59 | Oban.insert!(new(%{arg: encoded_fun}))
60 | end
61 |
62 | def drain_queue, do: Oban.drain_queue(queue: "test_queue")
63 |
64 | @impl Oban.Worker
65 | def perform(%Oban.Job{args: %{"arg" => arg}}) do
66 | fun = arg |> Base.decode64!(padding: false) |> :erlang.binary_to_term()
67 | fun.()
68 | end
69 | end
70 | end
71 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/lib/skafolder_tester_web/telemetry.ex:
--------------------------------------------------------------------------------
1 | # credo:disable-for-this-file VBT.Credo.Check.Consistency.ModuleLayout
2 |
3 | # credo:disable-for-this-file Credo.Check.Readability.Specs
4 |
5 | defmodule SkafolderTesterWeb.Telemetry do
6 | use Supervisor
7 | import Telemetry.Metrics
8 |
9 | def start_link(arg) do
10 | Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
11 | end
12 |
13 | @impl true
14 | def init(_arg) do
15 | children = [
16 | # Telemetry poller will execute the given period measurements
17 | # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics
18 | {:telemetry_poller, measurements: periodic_measurements(), period: 10_000}
19 | # Add reporters as children of your supervision tree.
20 | # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()}
21 | ]
22 |
23 | Supervisor.init(children, strategy: :one_for_one)
24 | end
25 |
26 | def metrics do
27 | [
28 | # Phoenix Metrics
29 | summary("phoenix.endpoint.stop.duration",
30 | unit: {:native, :millisecond}
31 | ),
32 | summary("phoenix.router_dispatch.stop.duration",
33 | tags: [:route],
34 | unit: {:native, :millisecond}
35 | ),
36 |
37 | # Database Metrics
38 | summary("skafolder_tester.repo.query.total_time", unit: {:native, :millisecond}),
39 | summary("skafolder_tester.repo.query.decode_time", unit: {:native, :millisecond}),
40 | summary("skafolder_tester.repo.query.query_time", unit: {:native, :millisecond}),
41 | summary("skafolder_tester.repo.query.queue_time", unit: {:native, :millisecond}),
42 | summary("skafolder_tester.repo.query.idle_time", unit: {:native, :millisecond}),
43 |
44 | # VM Metrics
45 | summary("vm.memory.total", unit: {:byte, :kilobyte}),
46 | summary("vm.total_run_queue_lengths.total"),
47 | summary("vm.total_run_queue_lengths.cpu"),
48 | summary("vm.total_run_queue_lengths.io")
49 | ]
50 | end
51 |
52 | defp periodic_measurements do
53 | [
54 | # A module, function and arguments to be invoked periodically.
55 | # This function must call :telemetry.execute/3 and a metric must be added above.
56 | # {SkafolderTesterWeb, :count_users, []}
57 | ]
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/test/vbt/graphql/types_test.exs:
--------------------------------------------------------------------------------
1 | defmodule VBT.Graphql.TypesTest do
2 | use VBT.Graphql.Case, async: true, endpoint: VBT.GraphqlServer, api_path: "/"
3 |
4 | describe "datetime_usec" do
5 | test "decodes valid input" do
6 | assert {:ok, response} = datetime_usec("2020-01-02T01:02:03.456789Z")
7 | assert response.decoded == ~U[2020-01-02 01:02:03.456789Z]
8 | end
9 |
10 | test "encodes valid input" do
11 | assert {:ok, response} = datetime_usec("2020-01-02T01:02:03.456789Z")
12 | assert response.encoded == "2020-01-02T01:02:03.456789Z"
13 | end
14 |
15 | test "supports nil" do
16 | assert {:ok, response} = datetime_usec(nil)
17 | assert response.decoded == nil
18 | assert response.encoded == nil
19 | end
20 |
21 | test "returns error on invalid input" do
22 | assert {:error, %{errors: [error]}} = datetime_usec("invalid value")
23 | assert error.message =~ ~s/Argument "value" has invalid value/
24 | end
25 |
26 | test "normalizes to microseconds granularity while decoding" do
27 | {:ok, response} = datetime_usec("2020-01-02T01:02:03.456789Z")
28 | assert response.decoded.microsecond == {456_789, 6}
29 |
30 | {:ok, response} = datetime_usec("2020-01-02T01:02:03.456Z")
31 | assert response.decoded.microsecond == {456_000, 6}
32 |
33 | {:ok, response} = datetime_usec("2020-01-02T01:02:03Z")
34 | assert response.decoded.microsecond == {0, 6}
35 | end
36 |
37 | test "normalizes to microseconds granularity while encoding" do
38 | {:ok, response} = datetime_usec("2020-01-02T01:02:03.456789Z")
39 | assert response.encoded_msec == "2020-01-02T01:02:03.456000Z"
40 | assert response.encoded_sec == "2020-01-02T01:02:03.000000Z"
41 | end
42 |
43 | defp datetime_usec(value) do
44 | with {:ok, response} <-
45 | call(
46 | "query($value: DateTimeUsec) {
47 | datetime_usec(value: $value) {decoded encoded encoded_msec encoded_sec}
48 | }",
49 | variables: %{value: value}
50 | ) do
51 | {:ok,
52 | Map.update!(
53 | response.datetime_usec,
54 | :decoded,
55 | &(&1 |> Base.decode64!() |> :erlang.binary_to_term())
56 | )}
57 | end
58 | end
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/config/prod.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # For production, don't forget to configure the url host
4 | # to something meaningful, Phoenix uses this information
5 | # when generating URLs.
6 | #
7 | # Note we also include the path to a cache manifest
8 | # containing the digested version of static files. This
9 | # manifest is generated by the `mix phx.digest` task,
10 | # which you should run after static files are built and
11 | # before starting your production server.
12 | config :skafolder_tester, SkafolderTesterWeb.Endpoint,
13 | cache_static_manifest: "priv/static/cache_manifest.json",
14 | url: [scheme: "https", port: 443],
15 | force_ssl: [rewrite_on: [:x_forwarded_proto]],
16 | server: true
17 |
18 | # Do not print debug messages in production
19 | config :logger, level: :info
20 |
21 | # ## SSL Support
22 | #
23 | # To get SSL working, you will need to add the `https` key
24 | # to the previous section and set your `:url` port to 443:
25 | #
26 | # config :skafolder_tester, SkafolderTesterWeb.Endpoint,
27 | # ...
28 | # url: [host: "example.com", port: 443],
29 | # https: [
30 | # port: 443,
31 | # cipher_suite: :strong,
32 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
33 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH"),
34 | # transport_options: [socket_opts: [:inet6]]
35 | # ]
36 | #
37 | # The `cipher_suite` is set to `:strong` to support only the
38 | # latest and more secure SSL ciphers. This means old browsers
39 | # and clients may not be supported. You can set it to
40 | # `:compatible` for wider support.
41 | #
42 | # `:keyfile` and `:certfile` expect an absolute path to the key
43 | # and cert in disk or a relative path inside priv, for example
44 | # "priv/ssl/server.key". For all supported SSL configuration
45 | # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1
46 | #
47 | # We also recommend setting `force_ssl` in your endpoint, ensuring
48 | # no data is ever sent via http, always redirecting to https:
49 | #
50 | # config :skafolder_tester, SkafolderTesterWeb.Endpoint,
51 | # force_ssl: [hsts: true]
52 | #
53 | # Check `Plug.SSL` for all available options in `force_ssl`.
54 |
55 | # Finally import the config/prod.secret.exs which loads secrets
56 | # and configuration from environment variables.
57 |
--------------------------------------------------------------------------------
/lib/vbt/absinthe/schema.ex:
--------------------------------------------------------------------------------
1 | defmodule VBT.Absinthe.Schema do
2 | @moduledoc """
3 | Helper for building GraphQL schemas.
4 |
5 | Use this module in place of `use Absinthe.Schema` in your schema modules. When used, the
6 | following extensions are brought to the client module:
7 |
8 | 1. `use Absinthe.Schema`
9 | 2. `import_types VBT.Graphql.Types`
10 | 3. Installs the `VBT.Absinthe.Schema.NormalizeErrors` middleware to each field with a declared
11 | resolver.
12 |
13 | This module sets up a middleware by default. You can add your own middlewares as follows:
14 |
15 | defmodule MySchema do
16 | use VBT.Absinthe.Schema
17 |
18 | def middleware(middlewares, field, object) do
19 | middleware = [MyMiddleware1, MyMiddleware2 | middlewares]
20 | super(middlewares, field, object)
21 | end
22 |
23 | # ...
24 | end
25 | """
26 |
27 | @doc """
28 | Resolves the GraphQL type of a VBT business error, or returns `nil` if the provided argument is
29 | not a known VBT business error.
30 |
31 | This function can be useful when resolving union types. For example:
32 |
33 | union :login_result do
34 | types [:login_, :business_error]
35 | resolve_type fn result, _ -> error_type(result) || :login end
36 | end
37 | """
38 | @spec error_type(any) :: :business_error | nil
39 | def error_type(%VBT.BusinessError{}), do: :business_error
40 | def error_type(_unknown), do: nil
41 |
42 | @doc false
43 | defmacro __using__(_opts) do
44 | quote do
45 | use Absinthe.Schema
46 | import unquote(__MODULE__)
47 |
48 | # Conditionally defining the behaviour to support absinthe 1.4 and 1.5
49 | unless Enum.member?(Module.get_attribute(__MODULE__, :behaviour), Absinthe.Schema),
50 | do: @behaviour(Absinthe.Schema)
51 |
52 | import_types VBT.Graphql.Types
53 |
54 | @impl Absinthe.Schema
55 | def middleware(middlewares, _field, _object) do
56 | # We'll only add the normalizer middleware to the fields with a declared resolver.
57 | if Enum.any?(middlewares, &match?({{Absinthe.Resolution, :call}, _}, &1)),
58 | do: middlewares ++ [VBT.Absinthe.Schema.NormalizeErrors],
59 | else: middlewares
60 | end
61 |
62 | # allows clients to override the global schema middleware setup
63 | defoverridable middleware: 3
64 | end
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/test/vbt/graphql/case_test.exs:
--------------------------------------------------------------------------------
1 | defmodule VBT.Graphql.CaseTest do
2 | use VBT.Graphql.Case, async: true, endpoint: VBT.GraphqlServer, api_path: "/"
3 |
4 | describe "call" do
5 | test "returns data on success" do
6 | assert {:ok, data} = call(order_query(), variables: %{order_id: 1})
7 |
8 | assert data.order == %{
9 | id: 1,
10 | order_items: [
11 | %{product_name: "product 1", quantity: 1},
12 | %{product_name: "product 2", quantity: 2}
13 | ]
14 | }
15 | end
16 |
17 | test "returns entire response on failure" do
18 | assert {:error, %{data: data, errors: errors}} = call(failing_query())
19 | assert data == %{register_user: nil}
20 | assert [%{message: error1}, %{message: error2}] = errors
21 | assert error1 == "can't be blank"
22 | assert error2 == "invalid password"
23 | end
24 | end
25 |
26 | describe "call!" do
27 | test "returns data on success" do
28 | data = call!(order_query(), variables: %{order_id: 1})
29 |
30 | assert data.order == %{
31 | id: 1,
32 | order_items: [
33 | %{product_name: "product 1", quantity: 1},
34 | %{product_name: "product 2", quantity: 2}
35 | ]
36 | }
37 | end
38 |
39 | test "raises on failure" do
40 | error = assert_raise ExUnit.AssertionError, fn -> call!(failing_query()) end
41 | assert error.message =~ "GraphQL call failed"
42 | assert error.message =~ "invalid password"
43 | assert error.message =~ "can't be blank"
44 | end
45 | end
46 |
47 | describe "errors" do
48 | test "returns all error messages" do
49 | {:error, response} = call(failing_query())
50 | assert errors(response) == ["can't be blank", "invalid password"]
51 | end
52 | end
53 |
54 | describe "field_errors" do
55 | test "returns error messages for the desired field" do
56 | {:error, response} = call(failing_query())
57 | assert field_errors(response, "login") == ["can't be blank"]
58 | end
59 | end
60 |
61 | defp order_query do
62 | """
63 | query ($order_id: Int!) {
64 | order(id: $order_id) {
65 | id
66 | order_items {product_name quantity}
67 | }
68 | }
69 | """
70 | end
71 |
72 | defp failing_query, do: "mutation {register_user}"
73 | end
74 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/.github/workflows/actions/test/action.yaml:
--------------------------------------------------------------------------------
1 | name: 'Run tests'
2 | description: 'Run CI tests'
3 | inputs:
4 | ssh-private-key:
5 | description: 'SSH private key used to fetch private deps'
6 | required: true
7 |
8 | runs:
9 | using: "composite"
10 | steps:
11 | - run: echo "SSH_AUTH_SOCK=/tmp/ssh_agent.sock" >> $GITHUB_ENV
12 | shell: bash
13 |
14 | - name: Setup SSH Keys and known_hosts
15 | run: |
16 | mkdir -p ~/.ssh
17 | ssh-keyscan github.com >> ~/.ssh/known_hosts
18 | ssh-agent -a $SSH_AUTH_SOCK > /dev/null
19 | ssh-add - <<< "${{ inputs.ssh-private-key }}"
20 | shell: bash
21 |
22 | - name: Fetch deps
23 | run: mix deps.get
24 | shell: bash
25 |
26 | - name: Compile project
27 | run: |
28 | MIX_ENV=test mix compile --warnings-as-errors
29 | MIX_ENV=dev mix compile --warnings-as-errors
30 | MIX_ENV=prod mix compile --warnings-as-errors
31 | shell: bash
32 |
33 | - name: Check GraphQL schema for uncommitted changes
34 | run: mix compile.export_gql_schema --check-graphql-schema-updated
35 | shell: bash
36 |
37 | - name: Run linter checks
38 | run: mix credo list
39 | shell: bash
40 |
41 | - name: Check code format
42 | run: mix format --check-formatted
43 | shell: bash
44 |
45 | - name: Run dialyzer
46 | run: mix dialyzer
47 | shell: bash
48 |
49 | - name: "Reset database"
50 | run: MIX_ENV=test mix ecto.reset
51 | shell: bash
52 |
53 | - name: Run tests
54 | run: make test
55 | shell: bash
56 |
57 | - name: Check migrations reversibility
58 | run: MIX_ENV=test mix ecto.rollback --all
59 | shell: bash
60 |
61 | - name: Check OTP release
62 | run: |
63 | mix release --overwrite
64 |
65 | # generate config and load to env vars, so release can use the correct database
66 | set -a && source <(MIX_ENV=test mix run -e "IO.puts(SkafolderTesterConfig.template())" | egrep "#.*=" | sed "s/# //")
67 |
68 | _build/prod/rel/skafolder_tester/bin/migrate.sh
69 | _build/prod/rel/skafolder_tester/bin/seed.sh
70 | _build/prod/rel/skafolder_tester/bin/skafolder_tester eval "{:ok, _} = Application.ensure_all_started(:skafolder_tester)"
71 | _build/prod/rel/skafolder_tester/bin/rollback.sh --all
72 | shell: bash
73 |
--------------------------------------------------------------------------------
/vbt_new/priv/templates/.github/workflows/prod.yaml.eex:
--------------------------------------------------------------------------------
1 | name: "Test, build & deploy - prod env"
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | test:
10 | name: Run tests
11 | runs-on: ubuntu-latest
12 |
13 | services:
14 | postgres:
15 | image: postgres:11.7
16 | env:
17 | POSTGRES_USER: "postgres"
18 | POSTGRES_PASSWORD: "postgres"
19 | ports:
20 | - 5432:5432
21 | # needed because the postgres container does not provide a healthcheck
22 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
23 | env:
24 | CACHE_VERSION: v1
25 | RELEASE_LEVEL: CI
26 | steps:
27 | - uses: actions/checkout@v2
28 |
29 | - uses: erlef/setup-beam@v1
30 | with:
31 | otp-version: '<%= "#{Mix.Vbt.tool_versions().erlang.major}.#{Mix.Vbt.tool_versions().erlang.minor}" %>'
32 | elixir-version: '<%= "#{Mix.Vbt.tool_versions().elixir.major}.#{Mix.Vbt.tool_versions().elixir.minor}.#{Mix.Vbt.tool_versions().elixir.patch}" %>'
33 |
34 | - name: Restore cached deps
35 | uses: actions/cache@v2
36 | with:
37 | path: |
38 | deps
39 | _build
40 | key: deps-${{ env.CACHE_VERSION }}-${{ github.ref }}-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}
41 | restore-keys: |
42 | deps-${{ env.CACHE_VERSION }}-${{ github.ref }}-
43 | deps-${{ env.CACHE_VERSION }}-
44 |
45 | - name: Run CI checks
46 | uses: ./.github/workflows/actions/test
47 | with:
48 | ssh-private-key: ${{ secrets.VBT_DEPLOY_SSH_PRIVATE_KEY }}
49 |
50 | build:
51 | needs: test
52 | name: Build, push, and deploy
53 | runs-on: ubuntu-latest
54 |
55 | steps:
56 | - uses: actions/checkout@v2
57 |
58 | - name: Deploy
59 | uses: ./.github/workflows/actions/deploy
60 | with:
61 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
62 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
63 | cluster-name: <%= organization %>-cluster
64 | github-token: ${{ secrets.GITHUB_TOKEN }}
65 | keybase-paperkey: ${{ secrets.KEYBASE_PAPERKEY }}
66 | keybase-username: ${{ secrets.KEYBASE_USERNAME }}
67 | release-level: prod
68 | ssh-private-key: ${{ secrets.VBT_DEPLOY_SSH_PRIVATE_KEY }}
69 |
--------------------------------------------------------------------------------
/vbt_new/priv/templates/.github/workflows/develop.yaml.eex:
--------------------------------------------------------------------------------
1 | name: "Test, build & deploy - develop env"
2 |
3 | on:
4 | push:
5 | branches:
6 | - develop
7 |
8 | jobs:
9 | test:
10 | name: Run tests
11 | runs-on: ubuntu-latest
12 |
13 | services:
14 | postgres:
15 | image: postgres:11.7
16 | env:
17 | POSTGRES_USER: "postgres"
18 | POSTGRES_PASSWORD: "postgres"
19 | ports:
20 | - 5432:5432
21 | # needed because the postgres container does not provide a healthcheck
22 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
23 | env:
24 | CACHE_VERSION: v1
25 | RELEASE_LEVEL: CI
26 | steps:
27 | - uses: actions/checkout@v2
28 |
29 | - uses: erlef/setup-beam@v1
30 | with:
31 | otp-version: '<%= "#{Mix.Vbt.tool_versions().erlang.major}.#{Mix.Vbt.tool_versions().erlang.minor}" %>'
32 | elixir-version: '<%= "#{Mix.Vbt.tool_versions().elixir.major}.#{Mix.Vbt.tool_versions().elixir.minor}.#{Mix.Vbt.tool_versions().elixir.patch}" %>'
33 |
34 | - name: Restore cached deps
35 | uses: actions/cache@v2
36 | with:
37 | path: |
38 | deps
39 | _build
40 | key: deps-${{ env.CACHE_VERSION }}-${{ github.ref }}-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}
41 | restore-keys: |
42 | deps-${{ env.CACHE_VERSION }}-${{ github.ref }}-
43 | deps-${{ env.CACHE_VERSION }}-
44 |
45 | - name: Run CI checks
46 | uses: ./.github/workflows/actions/test
47 | with:
48 | ssh-private-key: ${{ secrets.VBT_DEPLOY_SSH_PRIVATE_KEY }}
49 |
50 | build:
51 | needs: test
52 | name: Build, push, and deploy
53 | runs-on: ubuntu-latest
54 |
55 | steps:
56 | - uses: actions/checkout@v2
57 |
58 | - name: Deploy
59 | uses: ./.github/workflows/actions/deploy
60 | with:
61 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
62 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
63 | cluster-name: <%= organization %>-cluster
64 | github-token: ${{ secrets.GITHUB_TOKEN }}
65 | keybase-paperkey: ${{ secrets.KEYBASE_PAPERKEY }}
66 | keybase-username: ${{ secrets.KEYBASE_USERNAME }}
67 | release-level: develop
68 | ssh-private-key: ${{ secrets.VBT_DEPLOY_SSH_PRIVATE_KEY }}
69 |
--------------------------------------------------------------------------------
/vbt_new/priv/templates/.github/workflows/stage.yaml.eex:
--------------------------------------------------------------------------------
1 | name: "Test, build & deploy - stage env"
2 |
3 | on:
4 | push:
5 | branches:
6 | - 'release-*'
7 |
8 | jobs:
9 | test:
10 | name: Run tests
11 | runs-on: ubuntu-latest
12 |
13 | services:
14 | postgres:
15 | image: postgres:11.7
16 | env:
17 | POSTGRES_USER: "postgres"
18 | POSTGRES_PASSWORD: "postgres"
19 | ports:
20 | - 5432:5432
21 | # needed because the postgres container does not provide a healthcheck
22 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
23 | env:
24 | CACHE_VERSION: v1
25 | RELEASE_LEVEL: CI
26 | steps:
27 | - uses: actions/checkout@v2
28 |
29 | - uses: erlef/setup-beam@v1
30 | with:
31 | otp-version: '<%= "#{Mix.Vbt.tool_versions().erlang.major}.#{Mix.Vbt.tool_versions().erlang.minor}" %>'
32 | elixir-version: '<%= "#{Mix.Vbt.tool_versions().elixir.major}.#{Mix.Vbt.tool_versions().elixir.minor}.#{Mix.Vbt.tool_versions().elixir.patch}" %>'
33 |
34 | - name: Restore cached deps
35 | uses: actions/cache@v2
36 | with:
37 | path: |
38 | deps
39 | _build
40 | key: deps-${{ env.CACHE_VERSION }}-${{ github.ref }}-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}
41 | restore-keys: |
42 | deps-${{ env.CACHE_VERSION }}-${{ github.ref }}-
43 | deps-${{ env.CACHE_VERSION }}-
44 |
45 | - name: Run CI checks
46 | uses: ./.github/workflows/actions/test
47 | with:
48 | ssh-private-key: ${{ secrets.VBT_DEPLOY_SSH_PRIVATE_KEY }}
49 |
50 | build:
51 | needs: test
52 | name: Build, push, and deploy
53 | runs-on: ubuntu-latest
54 |
55 | steps:
56 | - uses: actions/checkout@v2
57 |
58 | - name: Deploy
59 | uses: ./.github/workflows/actions/deploy
60 | with:
61 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
62 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
63 | cluster-name: <%= organization %>-cluster
64 | github-token: ${{ secrets.GITHUB_TOKEN }}
65 | keybase-paperkey: ${{ secrets.KEYBASE_PAPERKEY }}
66 | keybase-username: ${{ secrets.KEYBASE_USERNAME }}
67 | release-level: stage
68 | ssh-private-key: ${{ secrets.VBT_DEPLOY_SSH_PRIVATE_KEY }}
69 |
--------------------------------------------------------------------------------
/vbt_new/priv/templates/.github/workflows/actions/test/action.yaml.eex:
--------------------------------------------------------------------------------
1 | name: 'Run tests'
2 | description: 'Run CI tests'
3 | inputs:
4 | ssh-private-key:
5 | description: 'SSH private key used to fetch private deps'
6 | required: true
7 |
8 | runs:
9 | using: "composite"
10 | steps:
11 | - run: echo "SSH_AUTH_SOCK=/tmp/ssh_agent.sock" >> $GITHUB_ENV
12 | shell: bash
13 |
14 | - name: Setup SSH Keys and known_hosts
15 | run: |
16 | mkdir -p ~/.ssh
17 | ssh-keyscan github.com >> ~/.ssh/known_hosts
18 | ssh-agent -a $SSH_AUTH_SOCK > /dev/null
19 | ssh-add - <<< "${{ inputs.ssh-private-key }}"
20 | shell: bash
21 |
22 | - name: Fetch deps
23 | run: mix deps.get
24 | shell: bash
25 |
26 | - name: Compile project
27 | run: |
28 | MIX_ENV=test mix compile --warnings-as-errors
29 | MIX_ENV=dev mix compile --warnings-as-errors
30 | MIX_ENV=prod mix compile --warnings-as-errors
31 | shell: bash
32 |
33 | - name: Check GraphQL schema for uncommitted changes
34 | run: mix compile.export_gql_schema --check-graphql-schema-updated
35 | shell: bash
36 |
37 | - name: Run linter checks
38 | run: mix credo list
39 | shell: bash
40 |
41 | - name: Check code format
42 | run: mix format --check-formatted
43 | shell: bash
44 |
45 | - name: Run dialyzer
46 | run: mix dialyzer
47 | shell: bash
48 |
49 | - name: "Reset database"
50 | run: MIX_ENV=test mix ecto.reset
51 | shell: bash
52 |
53 | - name: Run tests
54 | run: make test
55 | shell: bash
56 |
57 | - name: Check migrations reversibility
58 | run: MIX_ENV=test mix ecto.rollback --all
59 | shell: bash
60 |
61 | - name: Check OTP release
62 | run: |
63 | mix release --overwrite
64 |
65 | # generate config and load to env vars, so release can use the correct database
66 | set -a && source <(MIX_ENV=test mix run -e "IO.puts(<%= Mix.Vbt.config_module_name() %>.template())" | egrep "#.*=" | sed "s/# //")
67 |
68 | _build/prod/rel/<%= Mix.Vbt.otp_app() %>/bin/migrate.sh
69 | _build/prod/rel/<%= Mix.Vbt.otp_app() %>/bin/seed.sh
70 | _build/prod/rel/<%= Mix.Vbt.otp_app() %>/bin/<%= Mix.Vbt.otp_app() %> eval "{:ok, _} = Application.ensure_all_started(:<%= Mix.Vbt.otp_app() %>)"
71 | _build/prod/rel/<%= Mix.Vbt.otp_app() %>/bin/rollback.sh --all
72 | shell: bash
73 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: ~w/dev test compile seed-db reset-db reseed-db
2 |
3 | # devstack
4 | .PHONY: devstack devstack-build devstack-clean devstack-shell devstack-run
5 |
6 | DEFAULT_GOAL: help
7 |
8 | # TODO read database name from Mix.Config
9 | PGDATABASE?=skafolder_tester_backend_dev
10 |
11 |
12 | export LOCAL_USER_ID ?= $(shell id -u $$USER)
13 | export DOCKER_BUILDKIT=1
14 |
15 | # -------------------
16 | # --- DEFINITIONS ---
17 | # -------------------
18 |
19 | require-%:
20 | @ if [ "${${*}}" = "" ]; then \
21 | echo "ERROR: Environment variable not set: \"$*\""; \
22 | exit 1; \
23 | fi
24 |
25 | # -----------------
26 | # --- MIX TASKS ---
27 | # -----------------
28 |
29 | dev:
30 | mix ecto.setup && iex -S mix phx.server
31 |
32 | test:
33 | mix test
34 |
35 | compile:
36 | @mix do deps.get, compile
37 |
38 | reset-db:
39 | @mix do ecto.reset
40 |
41 | seed-db:
42 | @mix seed
43 |
44 | reseed-db: reset-db seed-db
45 |
46 |
47 | # -----------------------
48 | # --- DOCKER DEVSTACK ---
49 | # -----------------------
50 |
51 | ## Builds the development Docker image
52 | devstack-build:
53 | @docker build --ssh default .\
54 | --target build \
55 | --tag skafolder_tester:latest
56 |
57 | ## Stops all development containers
58 | devstack-clean:
59 | @docker-compose down -v
60 |
61 | ## Starts all development containers in the foreground
62 | devstack: devstack-build
63 | @docker-compose up
64 |
65 | ## Spawns an interactive Bash shell in development web container
66 | devstack-shell:
67 | @docker exec -e COLUMNS="`tput cols`" -e LINES="`tput lines`" -u ${LOCAL_USER_ID} -it $$(docker-compose ps -q web) /bin/bash -c "reset -w && /bin/bash"
68 |
69 | ## Starts the development server inside docker
70 | devstack-run: devstack-build
71 | @docker-compose up -d &&\
72 | docker-compose exec web mix deps.get && \
73 | docker-compose exec web mix ecto.setup && \
74 | docker-compose exec web iex -S mix phx.server
75 |
76 |
77 | # ------------
78 | # --- HELP ---
79 | # ------------
80 |
81 | ## Shows the help menu
82 | help:
83 | @echo "Please use \`make ' where is one of\n\n"
84 | @awk '/^[a-zA-Z\-\_\/0-9]+:/ { \
85 | helpMessage = match(lastLine, /^## (.*)/); \
86 | if (helpMessage) { \
87 | helpCommand = substr($$1, 0, index($$1, ":")); \
88 | helpMessage = substr(lastLine, RSTART + 3, RLENGTH); \
89 | printf "%-30s %s\n", helpCommand, helpMessage; \
90 | } \
91 | } \
92 | { lastLine = $$0 }' $(MAKEFILE_LIST)
93 |
--------------------------------------------------------------------------------
/lib/vbt/credo/check/readability/multiline_simple_do.ex:
--------------------------------------------------------------------------------
1 | # credo:disable-for-this-file Credo.Check.Readability.Specs
2 | # credo:disable-for-this-file VBT.Credo.Check.Readability.MultilineSimpleDo
3 |
4 | defmodule VBT.Credo.Check.Readability.MultilineSimpleDo do
5 | @moduledoc false
6 |
7 | use Credo.Check,
8 | category: :warning,
9 | base_priority: :high,
10 | explanations: [
11 | check: """
12 | Avoid using multiline simple do expression.
13 |
14 | # preferred
15 |
16 | defp some_fun() do
17 | %{
18 | a: 1,
19 | b: 2,
20 | c: 3
21 | }
22 | end
23 |
24 | # NOT preferred
25 |
26 | defp some_fun(),
27 | do: %{
28 | a: 1,
29 | b: 2,
30 | c: 3
31 | }
32 | """
33 | ]
34 |
35 | def run(source_file, params \\ []) do
36 | source_file
37 | |> lines()
38 | |> multiline_simple_dos()
39 | |> Enum.map(&credo_error(&1, IssueMeta.for(source_file, params)))
40 | end
41 |
42 | defp lines(source_file) do
43 | source_file
44 | |> Credo.SourceFile.lines()
45 | |> Stream.map(&with_location/1)
46 | |> Stream.reject(&(&1.content =~ ~r/^#/))
47 | end
48 |
49 | defp with_location({row, content}) do
50 | column =
51 | case Regex.run(~r/^\s*./, content, return: :index) do
52 | [{0, column}] -> column
53 | nil -> 0
54 | end
55 |
56 | %{row: row, column: column, content: String.trim(content)}
57 | end
58 |
59 | defp multiline_simple_dos(lines) do
60 | lines
61 | |> Stream.chunk_every(3, 1, :discard)
62 | |> Stream.map(fn [previous_line, this_line, next_line] ->
63 | # Recognition algorithm:
64 | # 1. Previous line ends with `,`
65 | # 2. This line starts with `do:`
66 | # 3. Next line is not empty, `end`, `)`, or `else`
67 | if previous_line.content =~ ~r/^.*,$/ and
68 | String.starts_with?(this_line.content, "do:") and
69 | not (next_line.content == "" or
70 | next_line.content in ~w/end ) end) " """/ or
71 | String.starts_with?(next_line.content, "else")),
72 | do: this_line
73 | end)
74 | |> Stream.reject(&is_nil/1)
75 | end
76 |
77 | defp credo_error(line, issue_meta) do
78 | format_issue(
79 | issue_meta,
80 | message: "Replace multiline `do:` expression with `do...end`",
81 | line_no: line.row,
82 | column: line.column
83 | )
84 | end
85 | end
86 |
--------------------------------------------------------------------------------
/test/vbt/credo/check/readability/multiline_simple_do_test.exs:
--------------------------------------------------------------------------------
1 | defmodule VBT.Credo.Check.Readability.MultilineSimpleDoTest do
2 | # credo:disable-for-this-file VBT.Credo.Check.Readability.MultilineSimpleDo
3 | use Credo.Test.Case
4 |
5 | alias VBT.Credo.Check.Readability.MultilineSimpleDo
6 |
7 | test "reports no errors on valid usage" do
8 | """
9 | defmodule Test do
10 | def fun1, do: :ok
11 | def fun2, do: :ok
12 |
13 | def fun3,
14 | do: :ok
15 |
16 | def fun4 do
17 | if some_condition(),
18 | do: :ok,
19 | else: :error
20 | end
21 |
22 | def fun5 do
23 | if true do
24 | with :ok <- :ok,
25 | do: :ok
26 | else
27 | :ok
28 | end
29 | end
30 | end
31 | """
32 | |> to_source_file()
33 | |> run_check(MultilineSimpleDo)
34 | |> refute_issues()
35 | end
36 |
37 | test "reports error on multiline do:" do
38 | [issue] =
39 | """
40 | defmodule Test do
41 | def fun,
42 | do:
43 | :ok
44 | end
45 | """
46 | |> to_source_file()
47 | |> run_check(MultilineSimpleDo)
48 | |> assert_issue()
49 |
50 | assert issue.line_no == 3
51 | assert issue.column == 5
52 | end
53 |
54 | test "reports error with comments present:" do
55 | [issue] =
56 | """
57 | defmodule Test do
58 | def fun,
59 | # some comment
60 | do:
61 | # another comment
62 | :ok
63 | end
64 | """
65 | |> to_source_file()
66 | |> run_check(MultilineSimpleDo)
67 | |> assert_issue()
68 |
69 | assert issue.line_no == 4
70 | assert issue.column == 5
71 | end
72 |
73 | test "reports multiple errors" do
74 | assert [issue1, issue2] =
75 | """
76 | defmodule Test do
77 | def fun1,
78 | do:
79 | :ok
80 |
81 | def fun2, do: :ok
82 |
83 | def fun3,
84 | # some comment
85 | do:
86 | # another comment
87 | :ok
88 | end
89 | """
90 | |> to_source_file()
91 | |> run_check(MultilineSimpleDo)
92 | |> assert_issues()
93 |
94 | assert issue1.line_no == 3
95 | assert issue1.column == 5
96 |
97 | assert issue2.line_no == 10
98 | assert issue2.column == 5
99 | end
100 | end
101 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/priv/gettext/en/LC_MESSAGES/errors.po:
--------------------------------------------------------------------------------
1 | ## `msgid`s in this file come from POT (.pot) files.
2 | ##
3 | ## Do not add, change, or remove `msgid`s manually here as
4 | ## they're tied to the ones in the corresponding POT file
5 | ## (with the same domain).
6 | ##
7 | ## Use `mix gettext.extract --merge` or `mix gettext.merge`
8 | ## to merge POT files into PO files.
9 | msgid ""
10 | msgstr ""
11 | "Language: en\n"
12 |
13 | ## From Ecto.Changeset.cast/4
14 | msgid "can't be blank"
15 | msgstr ""
16 |
17 | ## From Ecto.Changeset.unique_constraint/3
18 | msgid "has already been taken"
19 | msgstr ""
20 |
21 | ## From Ecto.Changeset.put_change/3
22 | msgid "is invalid"
23 | msgstr ""
24 |
25 | ## From Ecto.Changeset.validate_acceptance/3
26 | msgid "must be accepted"
27 | msgstr ""
28 |
29 | ## From Ecto.Changeset.validate_format/3
30 | msgid "has invalid format"
31 | msgstr ""
32 |
33 | ## From Ecto.Changeset.validate_subset/3
34 | msgid "has an invalid entry"
35 | msgstr ""
36 |
37 | ## From Ecto.Changeset.validate_exclusion/3
38 | msgid "is reserved"
39 | msgstr ""
40 |
41 | ## From Ecto.Changeset.validate_confirmation/3
42 | msgid "does not match confirmation"
43 | msgstr ""
44 |
45 | ## From Ecto.Changeset.no_assoc_constraint/3
46 | msgid "is still associated with this entry"
47 | msgstr ""
48 |
49 | msgid "are still associated with this entry"
50 | msgstr ""
51 |
52 | ## From Ecto.Changeset.validate_length/3
53 | msgid "should be %{count} character(s)"
54 | msgid_plural "should be %{count} character(s)"
55 | msgstr[0] ""
56 | msgstr[1] ""
57 |
58 | msgid "should have %{count} item(s)"
59 | msgid_plural "should have %{count} item(s)"
60 | msgstr[0] ""
61 | msgstr[1] ""
62 |
63 | msgid "should be at least %{count} character(s)"
64 | msgid_plural "should be at least %{count} character(s)"
65 | msgstr[0] ""
66 | msgstr[1] ""
67 |
68 | msgid "should have at least %{count} item(s)"
69 | msgid_plural "should have at least %{count} item(s)"
70 | msgstr[0] ""
71 | msgstr[1] ""
72 |
73 | msgid "should be at most %{count} character(s)"
74 | msgid_plural "should be at most %{count} character(s)"
75 | msgstr[0] ""
76 | msgstr[1] ""
77 |
78 | msgid "should have at most %{count} item(s)"
79 | msgid_plural "should have at most %{count} item(s)"
80 | msgstr[0] ""
81 | msgstr[1] ""
82 |
83 | ## From Ecto.Changeset.validate_number/3
84 | msgid "must be less than %{number}"
85 | msgstr ""
86 |
87 | msgid "must be greater than %{number}"
88 | msgstr ""
89 |
90 | msgid "must be less than or equal to %{number}"
91 | msgstr ""
92 |
93 | msgid "must be greater than or equal to %{number}"
94 | msgstr ""
95 |
96 | msgid "must be equal to %{number}"
97 | msgstr ""
98 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/priv/gettext/errors.pot:
--------------------------------------------------------------------------------
1 | ## This is a PO Template file.
2 | ##
3 | ## `msgid`s here are often extracted from source code.
4 | ## Add new translations manually only if they're dynamic
5 | ## translations that can't be statically extracted.
6 | ##
7 | ## Run `mix gettext.extract` to bring this file up to
8 | ## date. Leave `msgstr`s empty as changing them here has no
9 | ## effect: edit them in PO (`.po`) files instead.
10 |
11 | ## From Ecto.Changeset.cast/4
12 | msgid "can't be blank"
13 | msgstr ""
14 |
15 | ## From Ecto.Changeset.unique_constraint/3
16 | msgid "has already been taken"
17 | msgstr ""
18 |
19 | ## From Ecto.Changeset.put_change/3
20 | msgid "is invalid"
21 | msgstr ""
22 |
23 | ## From Ecto.Changeset.validate_acceptance/3
24 | msgid "must be accepted"
25 | msgstr ""
26 |
27 | ## From Ecto.Changeset.validate_format/3
28 | msgid "has invalid format"
29 | msgstr ""
30 |
31 | ## From Ecto.Changeset.validate_subset/3
32 | msgid "has an invalid entry"
33 | msgstr ""
34 |
35 | ## From Ecto.Changeset.validate_exclusion/3
36 | msgid "is reserved"
37 | msgstr ""
38 |
39 | ## From Ecto.Changeset.validate_confirmation/3
40 | msgid "does not match confirmation"
41 | msgstr ""
42 |
43 | ## From Ecto.Changeset.no_assoc_constraint/3
44 | msgid "is still associated with this entry"
45 | msgstr ""
46 |
47 | msgid "are still associated with this entry"
48 | msgstr ""
49 |
50 | ## From Ecto.Changeset.validate_length/3
51 | msgid "should be %{count} character(s)"
52 | msgid_plural "should be %{count} character(s)"
53 | msgstr[0] ""
54 | msgstr[1] ""
55 |
56 | msgid "should have %{count} item(s)"
57 | msgid_plural "should have %{count} item(s)"
58 | msgstr[0] ""
59 | msgstr[1] ""
60 |
61 | msgid "should be at least %{count} character(s)"
62 | msgid_plural "should be at least %{count} character(s)"
63 | msgstr[0] ""
64 | msgstr[1] ""
65 |
66 | msgid "should have at least %{count} item(s)"
67 | msgid_plural "should have at least %{count} item(s)"
68 | msgstr[0] ""
69 | msgstr[1] ""
70 |
71 | msgid "should be at most %{count} character(s)"
72 | msgid_plural "should be at most %{count} character(s)"
73 | msgstr[0] ""
74 | msgstr[1] ""
75 |
76 | msgid "should have at most %{count} item(s)"
77 | msgid_plural "should have at most %{count} item(s)"
78 | msgstr[0] ""
79 | msgstr[1] ""
80 |
81 | ## From Ecto.Changeset.validate_number/3
82 | msgid "must be less than %{number}"
83 | msgstr ""
84 |
85 | msgid "must be greater than %{number}"
86 | msgstr ""
87 |
88 | msgid "must be less than or equal to %{number}"
89 | msgstr ""
90 |
91 | msgid "must be greater than or equal to %{number}"
92 | msgstr ""
93 |
94 | msgid "must be equal to %{number}"
95 | msgstr ""
96 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/lib/skafolder_tester_web/endpoint.ex:
--------------------------------------------------------------------------------
1 | # credo:disable-for-this-file Credo.Check.Readability.Specs
2 |
3 | defmodule SkafolderTesterWeb.Endpoint do
4 | use Sentry.PlugCapture
5 | use Phoenix.Endpoint, otp_app: :skafolder_tester
6 |
7 | # The session will be stored in the cookie and signed,
8 | # this means its contents can be read but not tampered with.
9 | # Set :encryption_salt if you would also like to encrypt it.
10 | @session_options [
11 | store: :cookie,
12 | key: "_skafolder_tester_key",
13 | signing_salt: "8ZpxzLRT"
14 | ]
15 |
16 | socket "/socket", SkafolderTesterWeb.UserSocket,
17 | websocket: true,
18 | longpoll: false
19 |
20 | socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
21 |
22 | # Serve at "/" the static files from "priv/static" directory.
23 | #
24 | # You should set gzip to true if you are running phx.digest
25 | # when deploying your static files in production.
26 | plug Plug.Static,
27 | at: "/",
28 | from: :skafolder_tester,
29 | gzip: false,
30 | only: ~w(css fonts images js favicon.ico robots.txt)
31 |
32 | # Code reloading can be explicitly enabled under the
33 | # :code_reloader configuration of your endpoint.
34 | if code_reloading? do
35 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
36 | plug Phoenix.LiveReloader
37 | plug Phoenix.CodeReloader
38 | plug Phoenix.Ecto.CheckRepoStatus, otp_app: :skafolder_tester
39 | end
40 |
41 | plug Phoenix.LiveDashboard.RequestLogger,
42 | param_key: "request_logger",
43 | cookie_key: "request_logger"
44 |
45 | plug Plug.RequestId
46 | plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
47 |
48 | plug Plug.Parsers,
49 | parsers: [:urlencoded, :multipart, :json],
50 | pass: ["*/*"],
51 | json_decoder: Phoenix.json_library()
52 |
53 | plug Sentry.PlugContext
54 |
55 | plug Plug.MethodOverride
56 | plug Plug.Head
57 | plug VBT.Kubernetes.Probe, "/healthz"
58 | plug Plug.Session, @session_options
59 |
60 | if Mix.env() == :test do
61 | plug SkafolderTesterTest.Web.TestPlug
62 | end
63 |
64 | plug SkafolderTesterWeb.Router
65 |
66 | @impl Phoenix.Endpoint
67 | def init(_type, config) do
68 | config =
69 | config
70 | |> Keyword.put(:secret_key_base, SkafolderTesterConfig.secret_key_base())
71 | |> Keyword.update(:url, url_config(), &Keyword.merge(&1, url_config()))
72 | |> Keyword.update(:http, http_config(), &(http_config() ++ (&1 || [])))
73 |
74 | {:ok, config}
75 | end
76 |
77 | defp url_config, do: [host: SkafolderTesterConfig.host()]
78 | defp http_config, do: [:inet6, port: SkafolderTesterConfig.port()]
79 | end
80 |
--------------------------------------------------------------------------------
/lib/vbt/aws/test.ex:
--------------------------------------------------------------------------------
1 | defmodule VBT.Aws.Test do
2 | @moduledoc """
3 | Helpers for testing AWS interaction.
4 |
5 | ## Usage
6 |
7 | # test_helper.exs
8 | VBT.Aws.Test.setup()
9 |
10 | # some_test.exs
11 | test "..." do
12 | VBT.Aws.Test.stub_request(response)
13 |
14 | assert perform_aws_operation(...) == {:ok, response}
15 |
16 | assert_received {:aws_request, req, config}
17 | # do something with req and config
18 | end
19 | """
20 |
21 | @doc """
22 | Sets up test mock which implements `ExAws.Behaviour`.
23 |
24 | Invoke this function in test_helper.exs to setup the mock. Then, in your tests you can use
25 | `stub_request/1`. If you need a finer grained control, you can also use `Mox`. See
26 | `stub_request/1` for details.
27 | """
28 | @spec setup :: :ok
29 | def setup do
30 | Application.put_env(:vbt, :ex_aws_client, VBT.TestAwsClient)
31 | mox().defmock(VBT.TestAwsClient, for: ExAws.Behaviour)
32 | :ok
33 | end
34 |
35 | @doc """
36 | Stubs the `:request` function of the module returned by `VBT.Aws.client/0`.
37 |
38 | The `response` argument can be either `t:VBT.Aws.response/0`, which represents the response
39 | of the operation, or a binary, in which case the response will be
40 | `{:ok, %{body: response, headers: [], status_code: 200}}`.
41 |
42 | This function will also send a message in the shape of `{:aws_request, req, config}` to
43 | the caller process. Therefore, you can use `ExUnit.Assertions.assert_receive/3` to retrieve the
44 | request, and make further assertions. Note that this will only work if `assert_receive/3` is
45 | invoked in the same process as `stub_request/0`. Consequently, this approach will work if
46 | this function is invoked from `test`, or `setup` blocks, but not from `setup_all`.
47 |
48 | This function is a lightweight wrapper around `Mox`. If you need more control, you can mock
49 | AWS client directly as follows:
50 |
51 | Mox.stub(VBT.Aws.client(), :request, fn req, config -> ... end)
52 | """
53 | @spec stub_request(VBT.Aws.response() | binary) :: :ok
54 | def stub_request(response) do
55 | test_pid = self()
56 |
57 | mox().stub(VBT.TestAwsClient, :request, fn req, config ->
58 | send(test_pid, {:aws_request, req, config})
59 |
60 | case response do
61 | body when is_binary(body) -> {:ok, %{body: body, headers: [], status_code: 200}}
62 | {:ok, _} = success -> success
63 | {:error, _} = error -> error
64 | end
65 | end)
66 |
67 | :ok
68 | end
69 |
70 | defp mox do
71 | # runtime checking the existence of mox to avoid compile-time warnings in dev/prod
72 | unless Code.ensure_loaded?(Mox),
73 | do: raise("mox library must be included in dependency")
74 |
75 | Mox
76 | end
77 | end
78 |
--------------------------------------------------------------------------------
/.github/workflows/vbt.yaml:
--------------------------------------------------------------------------------
1 | name: "vbt"
2 |
3 | on: push
4 |
5 | jobs:
6 | vbt_build:
7 | runs-on: ubuntu-latest
8 |
9 | services:
10 | postgres:
11 | image: postgres:11.7
12 | env:
13 | POSTGRES_USER: "postgres"
14 | POSTGRES_PASSWORD: "postgres"
15 | POSTGRES_DB: "vbt_test"
16 | ports:
17 | - 5432:5432
18 | # needed because the postgres container does not provide a healthcheck
19 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
20 | env:
21 | CACHE_VERSION: v11
22 | steps:
23 | - uses: actions/checkout@v2
24 |
25 | - uses: erlef/setup-beam@v1
26 | with:
27 | otp-version: 24.0
28 | elixir-version: 1.12.2
29 |
30 | - name: Restore cached deps
31 | uses: actions/cache@v1
32 | with:
33 | path: deps
34 | key: vbt-deps-${{ env.CACHE_VERSION }}-${{ github.ref }}-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}
35 | restore-keys: |
36 | vbt-deps-${{ env.CACHE_VERSION }}-${{ github.ref }}-
37 | vbt-deps-${{ env.CACHE_VERSION }}-
38 |
39 | - name: Restore cached build
40 | uses: actions/cache@v1
41 | with:
42 | path: _build
43 | key: vbt-build-${{ env.CACHE_VERSION }}-${{ github.ref }}-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}
44 | restore-keys: |
45 | vbt-build-${{ env.CACHE_VERSION }}-${{ github.ref }}-
46 | vbt-build-${{ env.CACHE_VERSION }}-
47 |
48 | - name: Fetch deps
49 | run: mix deps.get
50 |
51 | - name: Compile project
52 | run: |
53 | MIX_ENV=test mix compile --warnings-as-errors
54 | MIX_ENV=dev mix compile --warnings-as-errors
55 | MIX_ENV=prod mix compile --warnings-as-errors
56 |
57 | - name: Check code format
58 | run: mix format --check-formatted
59 |
60 | - name: Run linter checks
61 | run: mix credo list
62 |
63 | - name: Run tests
64 | run: mix test
65 |
66 | - name: Run dialyzer
67 | run: mix dialyzer
68 |
69 | - name: Build docs
70 | run: mix docs
71 |
72 | #- name: Configure AWS Credentials
73 | # uses: aws-actions/configure-aws-credentials@v1
74 | # with:
75 | # aws-access-key-id: AKIA5B73P2OV7SEEGVWQ
76 | # aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
77 | # aws-region: us-east-1
78 |
79 | #- name: "Release docs"
80 | # if: github.ref == 'refs/heads/master' || contains(github.event.head_commit.message, '# push docs')
81 | # run: aws s3 sync --acl public-read --region us-east-1 ./doc/ s3://vbt-common-docs.verybigthings.com/
82 |
--------------------------------------------------------------------------------
/vbt_new/priv/templates/Makefile.eex:
--------------------------------------------------------------------------------
1 | <%=
2 | common_targets =
3 | """
4 | .PHONY: ~w/dev test compile seed-db reset-db reseed-db
5 | """
6 |
7 | devstack_targets =
8 | if docker do
9 | """
10 | # devstack
11 | .PHONY: devstack devstack-build devstack-clean devstack-shell devstack-run
12 | """
13 | end
14 |
15 | [common_targets, devstack_targets]
16 | |> Stream.reject(&is_nil/1)
17 | |> Enum.join("\n")
18 | %>
19 | DEFAULT_GOAL: help
20 |
21 | # TODO read database name from Mix.Config
22 | PGDATABASE?=<%= app %>_backend_dev
23 |
24 |
25 | export LOCAL_USER_ID ?= $(shell id -u $$USER)
26 | export DOCKER_BUILDKIT=1
27 |
28 | # -------------------
29 | # --- DEFINITIONS ---
30 | # -------------------
31 |
32 | require-%:
33 | @ if [ "${${*}}" = "" ]; then \
34 | echo "ERROR: Environment variable not set: \"$*\""; \
35 | exit 1; \
36 | fi
37 |
38 | # -----------------
39 | # --- MIX TASKS ---
40 | # -----------------
41 |
42 | dev:
43 | mix ecto.setup && iex -S mix phx.server
44 |
45 | test:
46 | mix test
47 |
48 | compile:
49 | @mix do deps.get, compile
50 |
51 | reset-db:
52 | @mix do ecto.reset
53 |
54 | seed-db:
55 | @mix seed
56 |
57 | reseed-db: reset-db seed-db
58 |
59 | <%= if docker do %>
60 | # -----------------------
61 | # --- DOCKER DEVSTACK ---
62 | # -----------------------
63 |
64 | ## Builds the development Docker image
65 | devstack-build:
66 | @docker build --ssh default .\
67 | --target build \
68 | --tag <%= app %>:latest
69 |
70 | ## Stops all development containers
71 | devstack-clean:
72 | @docker-compose down -v
73 |
74 | ## Starts all development containers in the foreground
75 | devstack: devstack-build
76 | @docker-compose up
77 |
78 | ## Spawns an interactive Bash shell in development web container
79 | devstack-shell:
80 | @docker exec -e COLUMNS="`tput cols`" -e LINES="`tput lines`" -u ${LOCAL_USER_ID} -it $$(docker-compose ps -q web) /bin/bash -c "reset -w && /bin/bash"
81 |
82 | ## Starts the development server inside docker
83 | devstack-run: devstack-build
84 | @docker-compose up -d &&\
85 | docker-compose exec web mix deps.get && \
86 | docker-compose exec web mix ecto.setup && \
87 | docker-compose exec web iex -S mix phx.server
88 | <% end %>
89 |
90 | # ------------
91 | # --- HELP ---
92 | # ------------
93 |
94 | ## Shows the help menu
95 | help:
96 | @echo "Please use \`make ' where is one of\n\n"
97 | @awk '/^[a-zA-Z\-\_\/0-9]+:/ { \
98 | helpMessage = match(lastLine, /^## (.*)/); \
99 | if (helpMessage) { \
100 | helpCommand = substr($$1, 0, index($$1, ":")); \
101 | helpMessage = substr(lastLine, RSTART + 3, RLENGTH); \
102 | printf "%-30s %s\n", helpCommand, helpMessage; \
103 | } \
104 | } \
105 | { lastLine = $$0 }' $(MAKEFILE_LIST)
106 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/README.md:
--------------------------------------------------------------------------------
1 | # SkafolderTester
2 |
3 | TODO: add project overview here
4 |
5 | ## Getting started
6 |
7 | It's advised to use the [asdf version manager](https://asdf-vm.com/) and the following plugins:
8 |
9 | - [Elixir](https://github.com/asdf-vm/asdf-elixir)
10 | - [Erlang](https://github.com/asdf-vm/asdf-erlang)
11 | - [Postgresql](https://github.com/asdf-vm/asdf-erlang)
12 |
13 | Once all of these are installed, invoke `asdf install` at the root of the project to install the required tools.
14 |
15 | Usage of asdf is not mandatory, but then you have to manage your own tool versions. Consult the .tool-versions files for the required tools. It is also possible to use asdf for some tools (e.g. Elixir and Erlang), while using the system-wide installation for others (e.g. PostgreSQL). In this case, don't install the corresponding asdf plugin.
16 |
17 | In the PostgreSQL instance the password for the `postgres` user should be set to `postgres`.
18 |
19 | ## First test
20 |
21 | 1. Fetch the deps with `mix deps.get`
22 | 2. Recreate the test database with `MIX_ENV=test mix ecto.reset`
23 | 3. Verify that all the tests are passing with `mix test`
24 |
25 | ## Running locally
26 |
27 | 1. Create the dev database with `mix ecto.reset`
28 | 2. Invoke `iex -S mix phx.server`
29 |
30 | The system will start and listen on the port 4000.
31 |
32 | ## Running inside a docker container
33 |
34 | Various make targets are present which can start the dockerized backend environment (so called devstack):
35 |
36 | - `make devstack` - builds the images and starts the required devstack containers.
37 | - `make devstack-shell` - connects to the main devstack container. Inside this container you can invoke regular commands, such as `mix test`.
38 | - `make devstack-clean` - stops all the containers for this project.
39 |
40 | The main container will also map the port 4000 to the host. Consequently, you can't start the local development server while the devstack containers are running (or vice versa). Other tasks can run safely, so for example you can run local tests while the devstack containers are running.
41 |
42 | ## Running the production version locally
43 |
44 | In some cases you may want run the production version locally. This can be useful for some testing inside the environment which is more similar to production. For example, a dev version might use a mock mail adapter to send e-mails, while in the OTP release the real mail adapter is used. The suggested way to do this is to build and run the OTP release locally:
45 |
46 | 1. run `mix release` to build the OTP release
47 | 2. run `MIX_ENV=dev mix operator_template > /tmp/skafolder_tester.env`
48 | 3. open /tmp/skafolder_tester.env and uncomment all the default settings, tweaking the ones which you need (e.g. access tokens for external services)
49 | 4. invoke `(. /tmp/skafolder_tester.env && _build/prod/rel/skafolder_tester/bin/skafolder_tester start)`
50 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/test/skafolder_tester_web/endpoint_test.exs:
--------------------------------------------------------------------------------
1 | defmodule SkafolderTesterWeb.EndpointTest do
2 | use SkafolderTesterTest.Web.ConnCase, async: false
3 | alias SkafolderTesterTest.Web.TestPlug
4 |
5 | describe "alive" do
6 | test "returns a 200 status code in response" do
7 | assert %{resp_body: "", status: 200} = get(build_conn(), "/healthz")
8 | end
9 | end
10 |
11 | describe "exception details" do
12 | for release_level <- ~w/develop stage preview/ do
13 | test "are sent to frontend on #{release_level}" do
14 | error =
15 | cause_server_exception(release_level: unquote(release_level), message: "some error")
16 |
17 | assert error =~ "some error"
18 | assert error =~ Path.basename(__ENV__.file)
19 | end
20 | end
21 |
22 | for release_level <- ~w/prod CI/ do
23 | test "are not sent to frontend on #{release_level}" do
24 | error =
25 | cause_server_exception(release_level: unquote(release_level), message: "some error")
26 |
27 | assert error == "Internal Server Error"
28 | end
29 | end
30 |
31 | for release_level <- ~w/develop preview prod/ do
32 | test "are sent to sentry on #{release_level}" do
33 | capture_sentry_events()
34 | cause_server_exception(release_level: unquote(release_level), message: "some error")
35 | assert_receive {:sentry_report, body}
36 | assert Map.fetch!(Jason.decode!(body), "message") =~ "(RuntimeError some error)"
37 | end
38 | end
39 | end
40 |
41 | defp capture_sentry_events do
42 | test_pid = self()
43 | bypass = Bypass.open()
44 |
45 | Bypass.expect(bypass, fn conn ->
46 | {:ok, body, conn} = Plug.Conn.read_body(conn)
47 | send(test_pid, {:sentry_report, body})
48 | Plug.Conn.resp(conn, 200, ~s<{"id": "340"}>)
49 | end)
50 |
51 | Application.put_env(:sentry, :dsn, "http://public:secret@localhost:#{bypass.port}/1")
52 | Application.put_env(:sentry, :send_result, :sync)
53 | end
54 |
55 | defp cause_server_exception(opts) do
56 | prev_release_level = set_release_level(Keyword.get(opts, :release_level))
57 |
58 | try do
59 | {500, _headers, body} = assert_error_sent(500, fn -> dispatch(opts) end)
60 |
61 | body
62 | |> Jason.decode!()
63 | |> Map.fetch!("errors")
64 | |> hd()
65 | |> Map.fetch!("message")
66 | after
67 | set_release_level(prev_release_level)
68 | end
69 | end
70 |
71 | defp dispatch(opts),
72 | do: TestPlug.dispatch(fn _conn -> raise Keyword.fetch!(opts, :message) end)
73 |
74 | defp set_release_level(value) do
75 | current_release_level = System.get_env("RELEASE_LEVEL")
76 |
77 | if is_nil(value),
78 | do: System.delete_env("RELEASE_LEVEL"),
79 | else: System.put_env("RELEASE_LEVEL", value)
80 |
81 | current_release_level
82 | end
83 | end
84 |
--------------------------------------------------------------------------------
/vbt_new/priv/templates/test/web/endpoint_test.exs.eex:
--------------------------------------------------------------------------------
1 | defmodule <%= Mix.Vbt.web_module_name() %>.EndpointTest do
2 | use <%= Mix.Vbt.test_module_name() %>.Web.ConnCase, async: false
3 | alias <%= Mix.Vbt.test_module_name() %>.Web.TestPlug
4 |
5 | describe "alive" do
6 | test "returns a 200 status code in response" do
7 | assert %{resp_body: "", status: 200} = get(build_conn(), "/healthz")
8 | end
9 | end
10 |
11 | describe "exception details" do
12 | for release_level <- ~w/develop stage preview/ do
13 | test "are sent to frontend on #{release_level}" do
14 | error =
15 | cause_server_exception(release_level: unquote(release_level), message: "some error")
16 |
17 | assert error =~ "some error"
18 | assert error =~ Path.basename(__ENV__.file)
19 | end
20 | end
21 |
22 | for release_level <- ~w/prod CI/ do
23 | test "are not sent to frontend on #{release_level}" do
24 | error =
25 | cause_server_exception(release_level: unquote(release_level), message: "some error")
26 |
27 | assert error == "Internal Server Error"
28 | end
29 | end
30 |
31 | for release_level <- ~w/develop preview prod/ do
32 | test "are sent to sentry on #{release_level}" do
33 | capture_sentry_events()
34 | cause_server_exception(release_level: unquote(release_level), message: "some error")
35 | assert_receive {:sentry_report, body}
36 | assert Map.fetch!(Jason.decode!(body), "message") =~ "(RuntimeError some error)"
37 | end
38 | end
39 | end
40 |
41 | defp capture_sentry_events do
42 | test_pid = self()
43 | bypass = Bypass.open()
44 |
45 | Bypass.expect(bypass, fn conn ->
46 | {:ok, body, conn} = Plug.Conn.read_body(conn)
47 | send(test_pid, {:sentry_report, body})
48 | Plug.Conn.resp(conn, 200, ~s<{"id": "340"}>)
49 | end)
50 |
51 | Application.put_env(:sentry, :dsn, "http://public:secret@localhost:#{bypass.port}/1")
52 | Application.put_env(:sentry, :send_result, :sync)
53 | end
54 |
55 | defp cause_server_exception(opts) do
56 | prev_release_level = set_release_level(Keyword.get(opts, :release_level))
57 |
58 | try do
59 | {500, _headers, body} = assert_error_sent 500, fn -> dispatch(opts) end
60 |
61 | body
62 | |> Jason.decode!()
63 | |> Map.fetch!("errors")
64 | |> hd()
65 | |> Map.fetch!("message")
66 | after
67 | set_release_level(prev_release_level)
68 | end
69 | end
70 |
71 | defp dispatch(opts),
72 | do: TestPlug.dispatch(fn _conn -> raise Keyword.fetch!(opts, :message) end)
73 |
74 | defp set_release_level(value) do
75 | current_release_level = System.get_env("RELEASE_LEVEL")
76 |
77 | if is_nil(value),
78 | do: System.delete_env("RELEASE_LEVEL"),
79 | else: System.put_env("RELEASE_LEVEL", value)
80 |
81 | current_release_level
82 | end
83 | end
84 |
--------------------------------------------------------------------------------
/vbt_new/priv/templates/README.md.eex:
--------------------------------------------------------------------------------
1 | # <%= Mix.Vbt.context_module_name() %>
2 |
3 | TODO: add project overview here
4 |
5 | ## Getting started
6 |
7 | It's advised to use the [asdf version manager](https://asdf-vm.com/) and the following plugins:
8 |
9 | - [Elixir](https://github.com/asdf-vm/asdf-elixir)
10 | - [Erlang](https://github.com/asdf-vm/asdf-erlang)
11 | - [Postgresql](https://github.com/asdf-vm/asdf-erlang)
12 |
13 | Once all of these are installed, invoke `asdf install` at the root of the project to install the required tools.
14 |
15 | Usage of asdf is not mandatory, but then you have to manage your own tool versions. Consult the .tool-versions files for the required tools. It is also possible to use asdf for some tools (e.g. Elixir and Erlang), while using the system-wide installation for others (e.g. PostgreSQL). In this case, don't install the corresponding asdf plugin.
16 |
17 | In the PostgreSQL instance the password for the `postgres` user should be set to `postgres`.
18 |
19 | ## First test
20 |
21 | 1. Fetch the deps with `mix deps.get`
22 | 2. Recreate the test database with `MIX_ENV=test mix ecto.reset`
23 | 3. Verify that all the tests are passing with `mix test`
24 |
25 | ## Running locally
26 |
27 | 1. Create the dev database with `mix ecto.reset`
28 | 2. Invoke `iex -S mix phx.server`
29 |
30 | The system will start and listen on the port 4000.
31 |
32 | ## Running inside a docker container
33 |
34 | Various make targets are present which can start the dockerized backend environment (so called devstack):
35 |
36 | - `make devstack` - builds the images and starts the required devstack containers.
37 | - `make devstack-shell` - connects to the main devstack container. Inside this container you can invoke regular commands, such as `mix test`.
38 | - `make devstack-clean` - stops all the containers for this project.
39 |
40 | The main container will also map the port 4000 to the host. Consequently, you can't start the local development server while the devstack containers are running (or vice versa). Other tasks can run safely, so for example you can run local tests while the devstack containers are running.
41 |
42 | ## Running the production version locally
43 |
44 | In some cases you may want run the production version locally. This can be useful for some testing inside the environment which is more similar to production. For example, a dev version might use a mock mail adapter to send e-mails, while in the OTP release the real mail adapter is used. The suggested way to do this is to build and run the OTP release locally:
45 |
46 | 1. run `mix release` to build the OTP release
47 | 2. run `MIX_ENV=dev mix operator_template > /tmp/<%= Mix.Vbt.otp_app() %>.env`
48 | 3. open /tmp/<%= Mix.Vbt.otp_app() %>.env and uncomment all the default settings, tweaking the ones which you need (e.g. access tokens for external services)
49 | 4. invoke `(. /tmp/<%= Mix.Vbt.otp_app() %>.env && _build/prod/rel/<%= Mix.Vbt.otp_app() %>/bin/<%= Mix.Vbt.otp_app() %> start)`
50 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule VBT.MixProject do
2 | use Mix.Project
3 |
4 | def project do
5 | [
6 | app: :vbt,
7 | version: "0.1.0",
8 | elixir: "~> 1.12",
9 | start_permanent: Mix.env() == :prod,
10 | deps: deps(),
11 | aliases: aliases(),
12 | preferred_cli_env: preferred_cli_env(),
13 | dialyzer: dialyzer(),
14 | elixirc_paths: elixirc_paths(Mix.env()),
15 | compilers: [:phoenix] ++ Mix.compilers(),
16 | source_url: "https://github.com/VeryBigThings/elixir_common_private/",
17 | docs: docs()
18 | ]
19 | end
20 |
21 | def application do
22 | additional_apps = if Mix.env() == :test, do: [:postgrex, :ecto], else: []
23 |
24 | [
25 | extra_applications: [:logger | additional_apps],
26 | mod: {VBT.Application, []}
27 | ]
28 | end
29 |
30 | defp deps do
31 | [
32 | {:absinthe_phoenix, "~> 2.0"},
33 | {:absinthe_relay, "~> 1.5"},
34 | {:bamboo, "~> 2.2"},
35 | {:bamboo_phoenix, "~> 1.0.0"},
36 | {:bcrypt_elixir, "~> 2.3"},
37 | {:credo, "~> 1.5", runtime: false},
38 | {:dialyxir, "~> 1.1", runtime: false},
39 | {:ecto_enum, "~> 1.4"},
40 | {:ecto_sql, "~> 3.7"},
41 | {:ex_aws_s3, "~> 2.3"},
42 | {:ex_crypto, "~> 0.10.0"},
43 | {:ex_doc, "~> 0.25.1", only: :dev, runtime: false},
44 | {:mox, "~> 1.0", only: :test},
45 | {:oban, "~> 2.8"},
46 | {:parent, "~> 0.12.0"},
47 | {:phoenix_html, "~> 2.13"},
48 | {:phoenix_live_view, "~> 0.14", optional: true},
49 | {:phoenix, "~> 1.5.12"},
50 | {:plug_cowboy, "~> 2.5"},
51 | {:provider, github: "VeryBigThings/provider"},
52 | {:sentry, "~> 8.0"},
53 | {:stream_data, "~> 0.5.0", only: [:test, :dev]}
54 | ]
55 | end
56 |
57 | defp aliases do
58 | [
59 | credo: ~w/compile credo/,
60 | "ecto.reset": ~w/ecto.drop ecto.create/,
61 | test: ["ecto.create --quiet", "ecto.migrate", "test"]
62 | ]
63 | end
64 |
65 | defp preferred_cli_env do
66 | [
67 | credo: :test,
68 | dialyzer: :test,
69 | "ecto.reset": :test,
70 | "ecto.migrate": :test,
71 | "ecto.rollback": :test,
72 | "ecto.gen.migration": :test
73 | ]
74 | end
75 |
76 | defp dialyzer() do
77 | [
78 | plt_add_apps: ~w/mix eex ecto credo bamboo ex_unit phoenix_pubsub phoenix_live_view/a
79 | ]
80 | end
81 |
82 | defp elixirc_paths(:test), do: ["lib", "test/support"]
83 | defp elixirc_paths(_), do: ["lib"]
84 |
85 | defp docs do
86 | [
87 | main: VBT,
88 | groups_for_modules: [
89 | "GraphQL & Absinthe": ~r/VBT\.((Absinthe)|(Graphql)).*/,
90 | Ecto: ~r/VBT\.((Ecto)|(Repo)).*/,
91 | "Auth & accounts": ~r/VBT\.((Auth)|(Accounts)).*/,
92 | "External services": ~r/VBT\.((Aws)|(Kubernetes)).*/,
93 | Credo: ~r/VBT\.Credo.*/,
94 | "Business errors": ~r/VBT\.[^\.]*Error/
95 | ]
96 | ]
97 | end
98 | end
99 |
--------------------------------------------------------------------------------
/lib/vbt/ecto.ex:
--------------------------------------------------------------------------------
1 | defmodule VBT.Ecto do
2 | @moduledoc "Helpers for working with Ecto."
3 |
4 | alias Ecto.Multi
5 |
6 | @type multi_result ::
7 | {:ok, changes}
8 | | {:error, failed_operation :: Multi.name(), reason :: any, changes}
9 |
10 | @type changes :: %{Multi.name() => any}
11 |
12 | @doc """
13 | Returns the result of a multi operation or the operation error.
14 |
15 | iex> result = (
16 | ...> Ecto.Multi.new()
17 | ...> |> Ecto.Multi.run(:foo, fn _, _ -> {:ok, 1} end)
18 | ...> |> Ecto.Multi.run(:bar, fn _, _ -> {:ok, 2} end)
19 | ...> |> Ecto.Multi.run(:baz, fn _, _ -> {:ok, 3} end)
20 | ...> |> VBT.TestRepo.transaction()
21 | ...> )
22 | iex> VBT.Ecto.multi_operation_result(result, :foo)
23 | {:ok, 1}
24 |
25 | iex> result = (
26 | ...> Ecto.Multi.new()
27 | ...> |> Ecto.Multi.run(:foo, fn _, _ -> {:ok, 1} end)
28 | ...> |> Ecto.Multi.run(:bar, fn _, _ -> {:error, "bar error"} end)
29 | ...> |> VBT.TestRepo.transaction()
30 | ...> )
31 | iex> VBT.Ecto.multi_operation_result(result, :foo)
32 | {:error, "bar error"}
33 | """
34 | @spec multi_operation_result(multi_result, Multi.name()) :: {:ok, any} | {:error, any}
35 | def multi_operation_result(multi_result, operation),
36 | do: map_multi_result(multi_result, &Map.fetch!(&1, operation))
37 |
38 | @doc """
39 | Maps the result of a multi operation, or returns an error.
40 |
41 | iex> result = (
42 | ...> Ecto.Multi.new()
43 | ...> |> Ecto.Multi.run(:foo, fn _, _ -> {:ok, 1} end)
44 | ...> |> Ecto.Multi.run(:bar, fn _, _ -> {:ok, 2} end)
45 | ...> |> Ecto.Multi.run(:baz, fn _, _ -> {:ok, 3} end)
46 | ...> |> VBT.TestRepo.transaction()
47 | ...> )
48 | iex> VBT.Ecto.map_multi_result(result)
49 | {:ok, %{foo: 1, bar: 2, baz: 3}}
50 |
51 | iex> result = (
52 | ...> Ecto.Multi.new()
53 | ...> |> Ecto.Multi.run(:foo, fn _, _ -> {:ok, 1} end)
54 | ...> |> Ecto.Multi.run(:bar, fn _, _ -> {:ok, 2} end)
55 | ...> |> Ecto.Multi.run(:baz, fn _, _ -> {:ok, 3} end)
56 | ...> |> VBT.TestRepo.transaction()
57 | ...> )
58 | iex> VBT.Ecto.map_multi_result(result, &Map.take(&1, [:foo, :bar]))
59 | {:ok, %{foo: 1, bar: 2}}
60 |
61 | iex> result = (
62 | ...> Ecto.Multi.new()
63 | ...> |> Ecto.Multi.run(:foo, fn _, _ -> {:ok, 1} end)
64 | ...> |> Ecto.Multi.run(:bar, fn _, _ -> {:error, "bar error"} end)
65 | ...> |> VBT.TestRepo.transaction()
66 | ...> )
67 | iex> VBT.Ecto.map_multi_result(result)
68 | {:error, "bar error"}
69 | """
70 | @spec map_multi_result(multi_result, (changes -> result)) :: {:ok, result} | {:error, any}
71 | when result: var
72 | def map_multi_result(multi_result, success_mapper \\ & &1)
73 |
74 | def map_multi_result({:ok, result}, success_mapper),
75 | do: {:ok, success_mapper.(result)}
76 |
77 | def map_multi_result({:error, _operation, error, _changes}, _success_mapper),
78 | do: {:error, error}
79 | end
80 |
--------------------------------------------------------------------------------
/lib/vbt/absinthe/schema/normalize_errors.ex:
--------------------------------------------------------------------------------
1 | defmodule VBT.Absinthe.Schema.NormalizeErrors do
2 | @moduledoc """
3 | Middleware which converts `Ecto.Changeset` errors into absinthe compatible errors.
4 |
5 | The simplest way to use this middleware is via `VBT.Absinthe.Schema`, which will automatically
6 | install this middleware to every field. Alternatively, you can install middleware manually, using
7 | standard absinthe mechanisms.
8 |
9 | Once the middleware is installed, you can safely return `{:error, Ecto.Changeset.t}` from your
10 | resolvers.
11 | """
12 |
13 | @behaviour Absinthe.Middleware
14 |
15 | alias Absinthe.Adapter.LanguageConventions
16 |
17 | @impl Absinthe.Middleware
18 | # credo:disable-for-next-line Credo.Check.Readability.Specs
19 | def call(resolution, _arg) do
20 | if resolution.state == :resolved do
21 | errors =
22 | Enum.flat_map(
23 | resolution.errors,
24 | fn
25 | %Ecto.Changeset{} = changeset -> changeset_errors(changeset)
26 | other -> [other]
27 | end
28 | )
29 |
30 | %Absinthe.Resolution{resolution | errors: errors}
31 | else
32 | # Field is not yet resolved, so we'll execute this middleware at the very end
33 | %Absinthe.Resolution{resolution | middleware: resolution.middleware ++ [__MODULE__]}
34 | end
35 | end
36 |
37 | @doc false
38 | # credo:disable-for-next-line Credo.Check.Readability.Specs
39 | def changeset_errors(changeset, opts \\ []) do
40 | changeset
41 | |> Ecto.Changeset.traverse_errors(& &1)
42 | |> format_errors(Keyword.merge(default_opts(), opts))
43 | end
44 |
45 | defp default_opts, do: [format_value: &format_value/2]
46 |
47 | defp format_errors(errors, opts) do
48 | errors
49 | |> Enum.map(fn {field_name, errors_per_field} ->
50 | external_field_name = to_external_name(field_name)
51 | Enum.map(errors_per_field, &handle_error(external_field_name, &1, opts))
52 | end)
53 | |> List.flatten()
54 | end
55 |
56 | defp to_external_name(field) do
57 | field
58 | |> Atom.to_string()
59 | |> LanguageConventions.to_external_name(:field)
60 | end
61 |
62 | defp handle_error(field, {msg, values}, opts) when is_binary(msg) do
63 | %{message: error_message(msg, values, opts), extensions: %{field: field}}
64 | end
65 |
66 | defp handle_error(_field, {field_name, nested}, opts) do
67 | Enum.map(nested, &handle_error(field_name, &1, opts))
68 | end
69 |
70 | defp handle_error(_field, errors, opts) do
71 | format_errors(errors, opts)
72 | end
73 |
74 | defp error_message("is invalid" = template, _values, _opts), do: template
75 |
76 | defp error_message(template, values, opts) do
77 | values
78 | |> Stream.filter(fn {key, _value} -> String.contains?(template, "%{#{key}}") end)
79 | |> Enum.reduce(template, fn {key, value}, acc ->
80 | String.replace(acc, "%{#{key}}", value_formatter(opts).(key, value))
81 | end)
82 | end
83 |
84 | defp value_formatter(opts), do: Keyword.fetch!(opts, :format_value)
85 |
86 | defp format_value(_, value), do: to_string(value)
87 | end
88 |
--------------------------------------------------------------------------------
/test/vbt/absinthe/instrumentation_test.exs:
--------------------------------------------------------------------------------
1 | defmodule VBT.Absinthe.InstrumentationTest do
2 | use ExUnit.Case, async: false
3 | alias VBT.Absinthe.Instrumentation
4 |
5 | test "logs an operation if its duration exceeds the given threshold" do
6 | set_threshold(0)
7 |
8 | log =
9 | ExUnit.CaptureLog.capture_log(fn ->
10 | Absinthe.run(
11 | """
12 | query {
13 | object1: object(id: 1) {id children {id children {id}}}
14 | object2: object(id: 2) {id children {id children {id}}}
15 | }
16 | """,
17 | __MODULE__.Schema
18 | )
19 | end)
20 |
21 | log = String.replace(log, ~r/\d+ms/, "0ms")
22 |
23 | assert log =~
24 | "[warn] spent 0ms in query { object1: object(id: 1) {id children {id children {id}}} object2: object(id: 2) {id children {id children {id}}} }"
25 |
26 | assert log =~ "0ms (1 calls) in object1"
27 | assert log =~ "0ms (1 calls) in object1.children"
28 | assert log =~ "0ms (1 calls) in object1.children.[i].children"
29 | assert log =~ "0ms (1 calls) in object2"
30 | assert log =~ "0ms (1 calls) in object2.children"
31 | assert log =~ "0ms (2 calls) in object2.children.[i].children"
32 | end
33 |
34 | test "doesn't log an operation if its duration is below the given threshold" do
35 | set_threshold(:timer.hours(24))
36 |
37 | log =
38 | ExUnit.CaptureLog.capture_log(fn ->
39 | Absinthe.run(
40 | """
41 | query {
42 | object1: object(id: 1) {id children {id children {id}}}
43 | object2: object(id: 2) {id children {id children {id}}}
44 | }
45 | """,
46 | __MODULE__.Schema
47 | )
48 | end)
49 |
50 | assert log == ""
51 | end
52 |
53 | test "correctly logs multiple queries made from the same process" do
54 | set_threshold(0)
55 |
56 | log =
57 | ExUnit.CaptureLog.capture_log(fn ->
58 | Absinthe.run("query {object(id: 1) {id}}", __MODULE__.Schema)
59 | Absinthe.run("query {object(id: 2) {id}}", __MODULE__.Schema)
60 | end)
61 |
62 | assert [_prefix, warning1, warning2] =
63 | log |> String.replace(~r/\d+ms/, "0ms") |> String.split("[warn]")
64 |
65 | assert warning1 =~ "spent 0ms in query {object(id: 1) {id}}"
66 | assert warning2 =~ "spent 0ms in query {object(id: 2) {id}}"
67 | end
68 |
69 | defp set_threshold(duration_ms) do
70 | Instrumentation.set_long_operation_threshold(duration_ms)
71 | on_exit(fn -> Instrumentation.set_long_operation_threshold(:infinity) end)
72 | end
73 |
74 | defmodule Schema do
75 | @moduledoc false
76 | use VBT.Absinthe.Schema
77 |
78 | query do
79 | field :object, :result do
80 | arg :id, non_null(:integer)
81 | resolve fn arg, _res -> {:ok, object(arg.id)} end
82 | end
83 | end
84 |
85 | object :result do
86 | field :id, non_null(:integer)
87 |
88 | field :children, list_of(:result),
89 | resolve: fn parent, _arg, _res -> {:ok, Enum.map(1..parent.id, &object/1)} end
90 | end
91 |
92 | defp object(id), do: %{id: id, children: nil}
93 | end
94 | end
95 |
--------------------------------------------------------------------------------
/lib/vbt/credo/check/graphql/mutation_field.ex:
--------------------------------------------------------------------------------
1 | # credo:disable-for-this-file Credo.Check.Readability.Specs
2 |
3 | defmodule VBT.Credo.Check.Graphql.MutationField do
4 | @moduledoc false
5 |
6 | use Credo.Check,
7 | category: :warning,
8 | base_priority: :high,
9 | explanations: [
10 | check: """
11 | Mutation field in a relay schema should be a payload field.
12 |
13 | # preferred
14 |
15 | payload field :some_field, ...
16 |
17 |
18 | # NOT preferred
19 |
20 | field :some_field, ...
21 | """
22 | ]
23 |
24 | alias alias Credo.Code
25 |
26 | def run(source_file, params \\ []) do
27 | {_, state} =
28 | source_file
29 | |> Code.ast()
30 | |> Macro.traverse(
31 | %{module_parts: [], schema?: [], mutation?: false, errors: []},
32 | &pre_traverse/2,
33 | &post_traverse/2
34 | )
35 |
36 | state.errors
37 | |> Enum.reverse()
38 | |> Enum.map(&error(IssueMeta.for(source_file, params), &1))
39 | end
40 |
41 | defp error(issue_meta, error) do
42 | format_issue(
43 | issue_meta,
44 | message: "Mutation field #{error.field_name} is not a payload field.",
45 | trigger: inspect(error.module),
46 | line_no: error.location[:line],
47 | column: error.location[:column]
48 | )
49 | end
50 |
51 | defp pre_traverse({:defmodule, _, args} = ast, state) do
52 | state =
53 | state
54 | |> Map.update!(:module_parts, &[module_name(args) | &1])
55 | |> Map.update!(:schema?, &[false | &1])
56 |
57 | {ast, state}
58 | end
59 |
60 | defp pre_traverse(
61 | {:use, _, [{:__aliases__, _, [:VBT, :Absinthe, :Relay, :Schema]} | _]} = ast,
62 | %{schema?: [_ | _]} = state
63 | ),
64 | do: {ast, Map.update!(state, :schema?, &[true | tl(&1)])}
65 |
66 | defp pre_traverse({:mutation, _, _} = ast, %{schema?: [true | _]} = state),
67 | do: {ast, %{state | mutation?: true}}
68 |
69 | defp pre_traverse({:payload, _, [{:field, _, _}, _]}, state),
70 | do: {[], state}
71 |
72 | defp pre_traverse({:field, meta, args}, %{mutation?: true} = state) do
73 | error = %{
74 | module: Module.concat(Enum.reverse(state.module_parts)),
75 | field_name: inspect(hd(args)),
76 | location: Keyword.take(meta, ~w/line column/a)
77 | }
78 |
79 | {[], Map.update!(state, :errors, &[error | &1])}
80 | end
81 |
82 | defp pre_traverse(other, state), do: {other, state}
83 |
84 | defp post_traverse({:defmodule, _, _} = ast, state),
85 | do: {ast, state |> Map.update!(:module_parts, &tl/1) |> Map.update!(:schema?, &tl/1)}
86 |
87 | defp post_traverse({:mutation, _, _} = ast, %{schema?: [true | _]} = state),
88 | do: {ast, %{state | mutation?: false}}
89 |
90 | defp post_traverse(other, state), do: {other, state}
91 |
92 | defp module_name([{:__aliases__, _, name_parts} | _]) do
93 | name_parts
94 | |> Enum.map(fn
95 | atom when is_atom(atom) -> atom
96 | _other -> Unknown
97 | end)
98 | |> Module.concat()
99 | end
100 |
101 | defp module_name(_other), do: Unknown
102 | end
103 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/lib/skafolder_tester_web.ex:
--------------------------------------------------------------------------------
1 | # credo:disable-for-this-file VBT.Credo.Check.Consistency.ModuleLayout
2 | # credo:disable-for-this-file Credo.Check.Readability.Specs
3 | # credo:disable-for-this-file Credo.Check.Readability.AliasAs
4 |
5 | defmodule SkafolderTesterWeb do
6 | @moduledoc """
7 | The entrypoint for defining your web interface, such
8 | as controllers, views, channels and so on.
9 |
10 | This can be used in your application as:
11 |
12 | use SkafolderTesterWeb, :controller
13 | use SkafolderTesterWeb, :view
14 |
15 | The definitions below will be executed for every view,
16 | controller, etc, so keep them short and clean, focused
17 | on imports, uses and aliases.
18 |
19 | Do NOT define functions inside the quoted expressions
20 | below. Instead, define any helper function in modules
21 | and import those modules here.
22 | """
23 |
24 | use Boundary,
25 | deps: [SkafolderTester, SkafolderTesterConfig, SkafolderTesterSchemas],
26 | exports: [Endpoint]
27 |
28 | @spec start_link :: Supervisor.on_start()
29 | def start_link do
30 | Supervisor.start_link(
31 | [
32 | SkafolderTesterWeb.Telemetry,
33 | SkafolderTesterWeb.Endpoint
34 | ],
35 | strategy: :one_for_one,
36 | name: __MODULE__
37 | )
38 | end
39 |
40 | @spec child_spec(any) :: Supervisor.child_spec()
41 | def child_spec(_arg) do
42 | %{
43 | id: __MODULE__,
44 | type: :supervisor,
45 | start: {__MODULE__, :start_link, []}
46 | }
47 | end
48 |
49 | def controller do
50 | quote do
51 | use Phoenix.Controller, namespace: SkafolderTesterWeb
52 |
53 | import Plug.Conn
54 | import SkafolderTesterWeb.Gettext
55 | alias SkafolderTesterWeb.Router.Helpers, as: Routes
56 | end
57 | end
58 |
59 | def view do
60 | quote do
61 | use Phoenix.View,
62 | root: "lib/skafolder_tester_web/templates",
63 | namespace: SkafolderTesterWeb
64 |
65 | # Import convenience functions from controllers
66 | import Phoenix.Controller,
67 | only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1]
68 |
69 | # Include shared imports and aliases for views
70 | unquote(view_helpers())
71 | end
72 | end
73 |
74 | def router do
75 | quote do
76 | use Phoenix.Router
77 |
78 | import Plug.Conn
79 | import Phoenix.Controller
80 | end
81 | end
82 |
83 | def channel do
84 | quote do
85 | use Phoenix.Channel
86 | import SkafolderTesterWeb.Gettext
87 | end
88 | end
89 |
90 | defp view_helpers do
91 | quote do
92 | # Use all HTML functionality (forms, tags, etc)
93 | use Phoenix.HTML
94 |
95 | # Import basic rendering functionality (render, render_layout, etc)
96 | import Phoenix.View
97 |
98 | import SkafolderTesterWeb.ErrorHelpers
99 | import SkafolderTesterWeb.Gettext
100 | alias SkafolderTesterWeb.Router.Helpers, as: Routes
101 | end
102 | end
103 |
104 | @doc """
105 | When used, dispatch to the appropriate controller/view/etc.
106 | """
107 | defmacro __using__(which) when is_atom(which) do
108 | apply(__MODULE__, which, [])
109 | end
110 | end
111 |
--------------------------------------------------------------------------------
/test/support/vbt/graphql_server.ex:
--------------------------------------------------------------------------------
1 | defmodule VBT.GraphqlServer do
2 | @moduledoc false
3 |
4 | # credo:disable-for-this-file Credo.Check.Readability.Specs
5 |
6 | use Phoenix.Endpoint, otp_app: :vbt
7 |
8 | socket "/socket", __MODULE__.Socket,
9 | websocket: true,
10 | longpoll: false
11 |
12 | plug VBT.Auth
13 | plug Absinthe.Plug, schema: __MODULE__.Schema
14 |
15 | defmodule Schema do
16 | @moduledoc false
17 | use VBT.Absinthe.Schema
18 |
19 | query do
20 | field :order, :order do
21 | arg :id, non_null(:integer)
22 |
23 | resolve fn arg, _ ->
24 | order = %{id: arg.id, order_items: [order_item(1), order_item(2)]}
25 | {:ok, order}
26 | end
27 | end
28 |
29 | field :auth_token, :string do
30 | arg :login, non_null(:string)
31 | resolve fn arg, _ -> {:ok, VBT.Auth.sign(VBT.GraphqlServer, "some_salt", arg.login)} end
32 | end
33 |
34 | field :current_user, :string do
35 | arg :max_age, :integer
36 |
37 | resolve fn arg, resolution ->
38 | VBT.Auth.verify(resolution, "some_salt", arg.max_age || 100)
39 | end
40 | end
41 |
42 | field :datetime_usec, :datetime_usec_result do
43 | arg :value, :datetime_usec
44 |
45 | resolve fn arg, _ ->
46 | decoded = arg.value |> :erlang.term_to_binary() |> Base.encode64()
47 |
48 | {:ok,
49 | %{
50 | decoded: decoded,
51 | encoded: arg.value,
52 | encoded_msec: arg.value && DateTime.truncate(arg.value, :millisecond),
53 | encoded_sec: arg.value && DateTime.truncate(arg.value, :second)
54 | }}
55 | end
56 | end
57 | end
58 |
59 | mutation do
60 | field :register_user, :string do
61 | resolve fn _, _ ->
62 | types = %{login: :string, password: :string}
63 |
64 | changeset =
65 | {%{}, types}
66 | |> Ecto.Changeset.cast(%{}, Map.keys(types))
67 | |> Ecto.Changeset.validate_required([:login])
68 | |> Ecto.Changeset.add_error(:password, "invalid password")
69 |
70 | {:error, changeset}
71 | end
72 | end
73 | end
74 |
75 | object :order do
76 | field :id, non_null(:integer)
77 | field :order_items, non_null(list_of(:order_item))
78 | end
79 |
80 | object :order_item do
81 | field :product_name, non_null(:string)
82 | field :quantity, non_null(:integer)
83 | end
84 |
85 | object :datetime_usec_result do
86 | field :decoded, :string
87 | field :encoded, :datetime_usec
88 | field :encoded_msec, :datetime_usec
89 | field :encoded_sec, :datetime_usec
90 | end
91 |
92 | defp order_item(id), do: %{product_name: "product #{id}", quantity: id}
93 | end
94 |
95 | defmodule Socket do
96 | @moduledoc false
97 | use Phoenix.Socket
98 |
99 | def connect(args, socket) do
100 | case VBT.Auth.verify(socket, "some_salt", Map.get(args, "max_age", 100), args) do
101 | {:ok, login} -> {:ok, assign(socket, %{login: login})}
102 | {:error, _reason} -> :error
103 | end
104 | end
105 |
106 | def id(socket), do: "user:#{socket.assigns.login}"
107 | end
108 | end
109 |
--------------------------------------------------------------------------------
/.github/workflows/vbt_new.yaml:
--------------------------------------------------------------------------------
1 | name: "vbt_new"
2 |
3 | on: push
4 |
5 | jobs:
6 | vbt_new_build:
7 | runs-on: ubuntu-latest
8 |
9 | services:
10 | postgres:
11 | image: postgres:11.7
12 | env:
13 | POSTGRES_USER: "postgres"
14 | POSTGRES_PASSWORD: "postgres"
15 | POSTGRES_DB: "vbt_test"
16 | ports:
17 | - 5432:5432
18 | # needed because the postgres container does not provide a healthcheck
19 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
20 | env:
21 | CACHE_VERSION: v11
22 | steps:
23 | - uses: actions/checkout@v2
24 |
25 | - uses: erlef/setup-beam@v1
26 | with:
27 | otp-version: 24.0
28 | elixir-version: 1.12.2
29 |
30 | - name: Restore cached deps
31 | uses: actions/cache@v1
32 | with:
33 | path: vbt_new/deps
34 | key: vbt_new_deps-${{ env.CACHE_VERSION }}-${{ github.ref }}-${{ hashFiles(format('{0}{1}', github.workspace, '/vbt_new/mix.lock')) }}
35 | restore-keys: |
36 | vbt_new_deps-${{ env.CACHE_VERSION }}-${{ github.ref }}-
37 | vbt_new_deps-${{ env.CACHE_VERSION }}-
38 |
39 | - name: Restore cached build
40 | uses: actions/cache@v1
41 | with:
42 | path: vbt_new/_build
43 | key: vbt_new_build-${{ env.CACHE_VERSION }}-${{ github.ref }}-${{ hashFiles(format('{0}{1}', github.workspace, '/vbt_new/mix.lock')) }}
44 | restore-keys: |
45 | vbt_new_build-${{ env.CACHE_VERSION }}-${{ github.ref }}-
46 | vbt_new_build-${{ env.CACHE_VERSION }}-
47 |
48 | - name: Restore cached tmp
49 | uses: actions/cache@v1
50 | with:
51 | path: vbt_new/tmp
52 | key: vbt_new_tmp-${{ env.CACHE_VERSION }}-${{ github.ref }}-${{ hashFiles(format('{0}{1}', github.workspace, '/vbt_new/test_projects/expected_state/mix.lock')) }}
53 | restore-keys: |
54 | vbt_new_tmp-${{ env.CACHE_VERSION }}-${{ github.ref }}-
55 | vbt_new_tmp-${{ env.CACHE_VERSION }}-
56 |
57 | - name: Fetch deps
58 | run: cd vbt_new && mix deps.get
59 |
60 | - name: Compile project
61 | run: |
62 | cd vbt_new
63 | MIX_ENV=test mix compile --warnings-as-errors
64 | MIX_ENV=dev mix compile --warnings-as-errors
65 | MIX_ENV=prod mix compile --warnings-as-errors
66 |
67 | - name: Check code format
68 | run: cd vbt_new && mix format --check-formatted
69 |
70 | - name: Run linter checks
71 | run: cd vbt_new && mix credo list
72 |
73 | - name: Run tests
74 | run: cd vbt_new && mix test
75 |
76 | - name: Run dialyzer
77 | run: cd vbt_new && mix dialyzer
78 |
79 | - name: Build archive
80 | run: cd vbt_new && mix archive.build
81 |
82 | #- name: Configure AWS Credentials
83 | # uses: aws-actions/configure-aws-credentials@v1
84 | # with:
85 | # aws-access-key-id: AKIA5B73P2OV7SEEGVWQ
86 | # aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
87 | # aws-region: us-east-1
88 |
89 | #- name: Deploy archive
90 | # if: github.ref == 'refs/heads/master'
91 | # run: aws s3 cp --acl public-read --region us-east-1 ./vbt_new/vbt_new-0.1.0.ez s3://vbt-common-docs.verybigthings.com/vbt_new.ez
92 |
--------------------------------------------------------------------------------
/lib/vbt/uri.ex:
--------------------------------------------------------------------------------
1 | defmodule VBT.URI do
2 | @moduledoc """
3 | Encoding and decoding of VBT URIs.
4 |
5 | A VBT URI is a special format of URI where path, query, and fragment parts are encoded into the
6 | fragment. For example, a standard URI http://some.host/some/path?foo=1#some_fragment is
7 | represented as http://some.host/#!some/path?foo=1#some_fragment.
8 |
9 | This special URI format is introduced to support deep links with applications deployed to S3.
10 | By encoding everything into fragment, we make sure that every link always points to the root
11 | (index.html) on the S3 server.
12 | """
13 |
14 | # ------------------------------------------------------------------------
15 | # API
16 | # ------------------------------------------------------------------------
17 |
18 | @doc """
19 | Returns the string representation of the given URI, encoded in VBT format.
20 |
21 | iex> uri = %URI{
22 | ...> scheme: "http",
23 | ...> host: "foo.bar",
24 | ...> port: 4000,
25 | ...> path: "/some/path",
26 | ...> query: "foo=1&bar=2",
27 | ...> fragment: "some_fragment"
28 | ...> }
29 | iex> VBT.URI.to_string(uri)
30 | "http://foo.bar:4000/#!some/path?foo=1&bar=2#some_fragment"
31 | """
32 | @spec to_string(URI.t()) :: String.t()
33 | def to_string(uri) do
34 | if not is_nil(uri.path) and not String.starts_with?(uri.path, "/"),
35 | do: raise(ArgumentError, message: "the input path must start with /")
36 |
37 | URI.to_string(%URI{uri | path: "/", query: nil, fragment: encode_fragment(uri)})
38 | end
39 |
40 | @doc """
41 | Parses a well-formed VBT URI string into its components.
42 |
43 | iex> VBT.URI.parse("http://foo.bar:4000/#!some/path?foo=1&bar=2#some_fragment")
44 | %URI{
45 | scheme: "http",
46 | host: "foo.bar",
47 | port: 4000,
48 | path: "/some/path",
49 | query: "foo=1&bar=2",
50 | fragment: "some_fragment",
51 | userinfo: nil,
52 | authority: "foo.bar:4000",
53 | }
54 |
55 | If the input is not a valid VBT URI, this function will raise an argument error.
56 | """
57 | @spec parse(String.t()) :: URI.t()
58 | def parse(uri_string) do
59 | standard_uri = URI.parse(uri_string)
60 |
61 | if standard_uri.path != "/" or standard_uri.query != nil or
62 | not String.starts_with?(standard_uri.fragment || "", "!"),
63 | do: raise(ArgumentError, message: "invalid uri")
64 |
65 | vbt_uri = decode_fragment(standard_uri.fragment)
66 |
67 | %URI{
68 | scheme: standard_uri.scheme,
69 | userinfo: standard_uri.userinfo,
70 | host: standard_uri.host,
71 | port: standard_uri.port,
72 | authority: standard_uri.authority,
73 | path: vbt_uri.path,
74 | query: vbt_uri.query,
75 | fragment: vbt_uri.fragment
76 | }
77 | end
78 |
79 | # ------------------------------------------------------------------------
80 | # Private
81 | # ------------------------------------------------------------------------
82 |
83 | defp encode_fragment(uri) do
84 | path = encode_path(uri.path)
85 | "!" <> URI.to_string(%URI{path: path, query: uri.query, fragment: uri.fragment})
86 | end
87 |
88 | defp encode_path(nil), do: nil
89 | defp encode_path("/" <> path), do: path
90 |
91 | defp decode_fragment("!" <> fragment) do
92 | %URI{path: path, query: query, fragment: fragment} = URI.parse(fragment)
93 | %URI{path: decode_path(path), query: query, fragment: fragment}
94 | end
95 |
96 | defp decode_path(nil), do: nil
97 | defp decode_path(path), do: "/#{path}"
98 | end
99 |
--------------------------------------------------------------------------------
/vbt_new/test_projects/expected_state/lib/skafolder_tester_app/release.ex:
--------------------------------------------------------------------------------
1 | # credo:disable-for-this-file Credo.Check.Readability.Specs
2 |
3 | defmodule SkafolderTesterApp.Release do
4 | @moduledoc false
5 |
6 | @start_apps [
7 | :crypto,
8 | :ssl,
9 | :postgrex,
10 | :ecto,
11 | :ecto_sql
12 | ]
13 |
14 | @app Keyword.fetch!(Mix.Project.config(), :app)
15 |
16 | def check_config, do: SkafolderTesterConfig.validate!()
17 |
18 | def migrate(args \\ []) do
19 | start_services()
20 | run_migrations(args)
21 | after
22 | stop_services()
23 | end
24 |
25 | def rollback(args \\ []) do
26 | start_services()
27 | run_rollbacks(args)
28 | after
29 | stop_services()
30 | end
31 |
32 | def seed(args) do
33 | {opts, []} = OptionParser.parse!(args, strict: [file: :string])
34 | start_services()
35 | run_migrations()
36 | run_seeds(Keyword.get(opts, :file, "seeds.exs"))
37 | after
38 | stop_services()
39 | end
40 |
41 | defp start_services do
42 | IO.puts("Starting dependencies..")
43 | Enum.each(@start_apps, &Application.ensure_all_started/1)
44 |
45 | IO.puts("Starting repos..")
46 | :ok = Application.load(@app)
47 | Enum.each(repos(), & &1.start_link(pool_size: 5))
48 | end
49 |
50 | defp stop_services do
51 | IO.puts("Stopping...")
52 | System.stop()
53 | end
54 |
55 | defp run_migrations(args \\ []) do
56 | Enum.each(repos(), fn repo ->
57 | app = Keyword.get(repo.config(), :otp_app)
58 | IO.puts("Running migrations for #{app}")
59 | run_migrations_based_on_args(repo, :up, args)
60 | end)
61 | end
62 |
63 | defp run_rollbacks(args) do
64 | Enum.each(repos(), fn repo ->
65 | app = Keyword.get(repo.config(), :otp_app)
66 | IO.puts("Running rollback for #{app}")
67 | run_migrations_based_on_args(repo, :down, args)
68 | end)
69 | end
70 |
71 | defp run_migrations_based_on_args(repo, direction, args) do
72 | case args do
73 | ["--step", n] -> run_migrations_for(repo, direction, step: String.to_integer(n))
74 | ["-n", n] -> run_migrations_for(repo, direction, step: String.to_integer(n))
75 | ["--to", to] -> run_migrations_for(repo, direction, to: String.to_integer(to))
76 | ["--all"] -> run_migrations_for(repo, direction, all: true)
77 | [] -> run_migrations_for(repo, direction)
78 | end
79 | end
80 |
81 | defp run_migrations_for(repo, :up), do: run_migrations_for(repo, :up, all: true)
82 | defp run_migrations_for(repo, :down), do: run_migrations_for(repo, :down, step: 1)
83 |
84 | defp run_migrations_for(repo, direction, opts) do
85 | migrations_path = priv_path_for(repo, "migrations")
86 | Ecto.Migrator.run(repo, [migrations_path], direction, opts)
87 | end
88 |
89 | defp run_seeds(seed_file), do: Enum.each(repos(), &run_seeds_for(&1, seed_file))
90 |
91 | defp run_seeds_for(repo, seed_file) do
92 | # Run the seed script if it exists
93 | seed_script = priv_path_for(repo, seed_file)
94 |
95 | if File.exists?(seed_script) do
96 | IO.puts("Running seed script #{seed_script}..")
97 | Code.eval_file(seed_script)
98 | else
99 | IO.puts("Seed script #{seed_script} does not exist..")
100 | end
101 | end
102 |
103 | defp priv_path_for(repo, filename) do
104 | app_dir = Application.app_dir(Keyword.fetch!(repo.config, :otp_app))
105 |
106 | repo_underscore =
107 | repo
108 | |> Module.split()
109 | |> List.last()
110 | |> Macro.underscore()
111 |
112 | Path.join([app_dir, "priv", repo_underscore, filename])
113 | end
114 |
115 | defp repos, do: Application.fetch_env!(@app, :ecto_repos)
116 | end
117 |
--------------------------------------------------------------------------------
/vbt_new/priv/templates/lib/otp_app_app/release.ex.eex:
--------------------------------------------------------------------------------
1 | # credo:disable-for-this-file Credo.Check.Readability.Specs
2 |
3 | defmodule <%= Mix.Vbt.app_module_name() %>.Release do
4 | @moduledoc false
5 |
6 | @start_apps [
7 | :crypto,
8 | :ssl,
9 | :postgrex,
10 | :ecto,
11 | :ecto_sql
12 | ]
13 |
14 | @app Keyword.fetch!(Mix.Project.config(), :app)
15 |
16 | def check_config, do: <%= Mix.Vbt.config_module_name() %>.validate!()
17 |
18 | def migrate(args \\ []) do
19 | start_services()
20 | run_migrations(args)
21 | after
22 | stop_services()
23 | end
24 |
25 | def rollback(args \\ []) do
26 | start_services()
27 | run_rollbacks(args)
28 | after
29 | stop_services()
30 | end
31 |
32 | def seed(args) do
33 | {opts, []} = OptionParser.parse!(args, strict: [file: :string])
34 | start_services()
35 | run_migrations()
36 | run_seeds(Keyword.get(opts, :file, "seeds.exs"))
37 | after
38 | stop_services()
39 | end
40 |
41 | defp start_services do
42 | IO.puts("Starting dependencies..")
43 | Enum.each(@start_apps, &Application.ensure_all_started/1)
44 |
45 | IO.puts("Starting repos..")
46 | :ok = Application.load(@app)
47 | Enum.each(repos(), & &1.start_link(pool_size: 5))
48 | end
49 |
50 | defp stop_services do
51 | IO.puts("Stopping...")
52 | System.stop()
53 | end
54 |
55 | defp run_migrations(args \\ []) do
56 | Enum.each(repos(), fn repo ->
57 | app = Keyword.get(repo.config(), :otp_app)
58 | IO.puts("Running migrations for #{app}")
59 | run_migrations_based_on_args(repo, :up, args)
60 | end)
61 | end
62 |
63 | defp run_rollbacks(args) do
64 | Enum.each(repos(), fn repo ->
65 | app = Keyword.get(repo.config(), :otp_app)
66 | IO.puts("Running rollback for #{app}")
67 | run_migrations_based_on_args(repo, :down, args)
68 | end)
69 | end
70 |
71 | defp run_migrations_based_on_args(repo, direction, args) do
72 | case args do
73 | ["--step", n] -> run_migrations_for(repo, direction, step: String.to_integer(n))
74 | ["-n", n] -> run_migrations_for(repo, direction, step: String.to_integer(n))
75 | ["--to", to] -> run_migrations_for(repo, direction, to: String.to_integer(to))
76 | ["--all"] -> run_migrations_for(repo, direction, all: true)
77 | [] -> run_migrations_for(repo, direction)
78 | end
79 | end
80 |
81 | defp run_migrations_for(repo, :up), do: run_migrations_for(repo, :up, all: true)
82 | defp run_migrations_for(repo, :down), do: run_migrations_for(repo, :down, step: 1)
83 |
84 | defp run_migrations_for(repo, direction, opts) do
85 | migrations_path = priv_path_for(repo, "migrations")
86 | Ecto.Migrator.run(repo, [migrations_path], direction, opts)
87 | end
88 |
89 | defp run_seeds(seed_file), do: Enum.each(repos(), &run_seeds_for(&1, seed_file))
90 |
91 | defp run_seeds_for(repo, seed_file) do
92 | # Run the seed script if it exists
93 | seed_script = priv_path_for(repo, seed_file)
94 |
95 | if File.exists?(seed_script) do
96 | IO.puts("Running seed script #{seed_script}..")
97 | Code.eval_file(seed_script)
98 | else
99 | IO.puts("Seed script #{seed_script} does not exist..")
100 | end
101 | end
102 |
103 | defp priv_path_for(repo, filename) do
104 | app_dir = Application.app_dir(Keyword.fetch!(repo.config, :otp_app))
105 |
106 | repo_underscore =
107 | repo
108 | |> Module.split()
109 | |> List.last()
110 | |> Macro.underscore()
111 |
112 | Path.join([app_dir, "priv", repo_underscore, filename])
113 | end
114 |
115 | defp repos, do: Application.fetch_env!(@app, :ecto_repos)
116 | end
117 |
--------------------------------------------------------------------------------
/vbt_new/lib/mix/vbt/config_file.ex:
--------------------------------------------------------------------------------
1 | defmodule Mix.Vbt.ConfigFile do
2 | @moduledoc false
3 | alias Mix.Vbt
4 | alias Mix.Vbt.SourceFile
5 |
6 | @type updater_fun :: (Keyword.t() -> Keyword.t())
7 |
8 | @spec app :: atom
9 | def app, do: Keyword.fetch!(Mix.Project.config(), :app)
10 |
11 | @spec prepend(SourceFile.t(), String.t()) :: SourceFile.t()
12 | def prepend(file, code) do
13 | update_in(
14 | file.content,
15 | &String.replace(&1, ~r/(?<=use Mix.Config\n)\n/s, "\n#{code}\n\n")
16 | )
17 | end
18 |
19 | @spec update_endpoint_config(SourceFile.t(), updater_fun) :: SourceFile.t()
20 | def update_endpoint_config(file, updater),
21 | do: update_kw_config(file, Vbt.endpoint_module(), updater)
22 |
23 | @spec update_repo_config(SourceFile.t(), updater_fun) :: SourceFile.t()
24 | def update_repo_config(file, updater),
25 | do: update_kw_config(file, Vbt.repo_module(), updater)
26 |
27 | @spec update_config(SourceFile.t(), atom, updater_fun) :: SourceFile.t()
28 | def update_config(file, app \\ Vbt.otp_app(), updater) do
29 | do_update_config(
30 | file,
31 | updater,
32 | ~r/(\n\s*config\s+#{inspect(app)},\n)(?.*?)(?=\n\n)/s
33 | )
34 | end
35 |
36 | defp update_kw_config(file, key, updater) do
37 | # match `config :my_app, SomeKey,` (i.e. the part up until and including the comma character
38 | # and the remaining whitespaces)
39 | config_start_regex = ~r/(\n\s*config\s+#{inspect(Vbt.otp_app())},\s+#{inspect(key)},\s*?)/
40 |
41 | # config opts are all characters up until (but excluding) two newline characters
42 | config_opts_regex = ~r/(?[^\s].*?)(?=\n\n)/
43 |
44 | do_update_config(
45 | file,
46 | updater,
47 | ~r/#{Regex.source(config_start_regex)}#{Regex.source(config_opts_regex)}/s
48 | )
49 | end
50 |
51 | defp do_update_config(file, updater, regex),
52 | do: update_in(file.content, &replace(&1, updater, regex))
53 |
54 | defp replace(content, updater, regex) do
55 | # Recursive replacing of the content according to the regex.
56 | # Normally we could use String.replace for this, but this won't work in the cases where
57 | # there are multiple config entries for the same key. This code performs iterative
58 | # replaces of all matched occurences in the given content.
59 | case Regex.named_captures(regex, content, return: :index) do
60 | nil ->
61 | content
62 |
63 | %{"opts" => {from, len}} ->
64 | {prefix, suffix} = String.split_at(content, from + len)
65 | {prefix, opts} = String.split_at(prefix, from)
66 |
67 | {opts, _} = Code.eval_string("[#{opts}]")
68 |
69 | updated_opts = updater.(opts)
70 | updated_suffix = replace(suffix, updater, regex)
71 |
72 | if opts == updated_opts and suffix == updated_suffix do
73 | # Small optimization to avoid changing the content if nothing has changed.
74 | # Without this, we might end up making some minor changes which are semantically
75 | # equivalent, but visually different (e.g. replacing ~w// with [...]).
76 | content
77 | else
78 | updated_opts =
79 | updated_opts
80 | |> inspect(limit: :infinity)
81 | # We accidentally expand `{:cd, Path.expand("../assets", __DIR__)}` since config is
82 | # evaled above. This is a hacky way of returning this to the original form.
83 | |> String.replace(
84 | ~s["#{Path.expand("../assets")}"],
85 | ~s[Path.expand("../assets", __DIR__)]
86 | )
87 | |> String.replace(~r/^\[/, "")
88 | |> String.replace(~r/\]$/, "")
89 |
90 | prefix <> updated_opts <> updated_suffix
91 | end
92 | end
93 | end
94 | end
95 |
--------------------------------------------------------------------------------