16 |
--------------------------------------------------------------------------------
/apps/alb_monitor/lib/alb_monitor.ex:
--------------------------------------------------------------------------------
1 | defmodule ALBMonitor do
2 | @moduledoc """
3 | When the app is running on AWS, monitors the Application Load Balancer for the instance and
4 | proactively shuts down the app when it begins draining connections, to ensure long-lived event
5 | stream connections are cleanly closed.
6 | """
7 |
8 | use Application
9 |
10 | def start(_type, _args) do
11 | children = [
12 | ALBMonitor.Monitor
13 | ]
14 |
15 | opts = [strategy: :one_for_one, name: ALBMonitor.Supervisor]
16 | Supervisor.start_link(children, opts)
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/apps/parse/lib/parse/facility/property.ex:
--------------------------------------------------------------------------------
1 | defmodule Parse.Facility.Property do
2 | @moduledoc """
3 |
4 | Parser for facility_properties.txt
5 |
6 | """
7 | use Parse.Simple
8 |
9 | alias Model.Facility.Property
10 |
11 | def parse_row(row) do
12 | %Property{
13 | name: copy(row["property_id"]),
14 | facility_id: copy(row["facility_id"]),
15 | value: decode_value(row["value"])
16 | }
17 | end
18 |
19 | defp decode_value(value) do
20 | case Integer.parse(value) do
21 | {integer, ""} -> integer
22 | _ -> value
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/apps/state/test/state/stop/worker_test.exs:
--------------------------------------------------------------------------------
1 | defmodule State.Stop.WorkerTest do
2 | use ExUnit.Case, async: true
3 | alias Model.Stop
4 |
5 | setup do
6 | worker_id = :test
7 | {:ok, _} = State.Stop.Worker.start_link(worker_id)
8 |
9 | {:ok, %{worker_id: worker_id}}
10 | end
11 |
12 | test "it can add a stop and query it", %{worker_id: worker_id} do
13 | stop = %Stop{id: "1", name: "stop", latitude: 1, longitude: -2}
14 | State.Stop.Worker.new_state(worker_id, [stop])
15 |
16 | assert State.Stop.Worker.around(worker_id, 1.001, -2.002) == ["1"]
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/apps/api_web/lib/api_web/templates/client_portal/user/configure_2fa.html.heex:
--------------------------------------------------------------------------------
1 |
2 |
Configure 2-Factor
3 |
4 | <%= if @enabled do %>
5 |
TOTP 2-Factor authentication is enabled for your account.
6 | {link("Disable 2-Factor authentication", to: user_path(@conn, :unenroll_2fa))}
7 | <% else %>
8 |
2-Factor is not enabled for your account.
9 |
10 | <%= form_for %{}, user_path(@conn, :enable_2fa), [method: :post], fn _f -> %>
11 | {submit("Enable 2-Factor", class: "btn btn-primary")}
12 | <% end %>
13 | <% end %>
14 |
15 |
--------------------------------------------------------------------------------
/apps/api_web/lib/api_web/templates/client_portal/key/edit.html.heex:
--------------------------------------------------------------------------------
1 | Edit Key
2 |
3 |
Key Details
4 |
5 |
6 | User E-mail: {@user.email}
7 | Key: {@key.key}
8 |
9 |
10 |
11 |
12 | {render("form.html",
13 | changeset: @changeset,
14 | action: key_path(@conn, :update, @key),
15 | method: :put,
16 | api_versions: @api_versions
17 | )}
18 |
19 | {link("Back", to: portal_path(@conn, :index))}
20 |
--------------------------------------------------------------------------------
/apps/api_web/lib/api_web/controllers/status_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.StatusController do
2 | use ApiWeb.Web, :controller
3 |
4 | def index(conn, _params) do
5 | {feed_version, feed_start_date, feed_end_date} = State.Metadata.feed_metadata()
6 | updated_timestamps = State.Metadata.updated_timestamps()
7 |
8 | data = %{
9 | feed: %{
10 | version: feed_version,
11 | start_date: feed_start_date,
12 | end_date: feed_end_date
13 | },
14 | timestamps: updated_timestamps
15 | }
16 |
17 | render(conn, "index.json-api", data: data)
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/apps/api_web/lib/api_web/controllers/portal/portal_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.ClientPortal.PortalController do
2 | @moduledoc false
3 | use ApiWeb.Web, :controller
4 |
5 | def landing(conn, _params) do
6 | conn
7 | |> assign(:pre_container_template, "_hero.html")
8 | |> render("landing.html")
9 | end
10 |
11 | def index(conn, _params) do
12 | keys = ApiAccounts.list_keys_for_user(conn.assigns.user)
13 |
14 | render(
15 | conn,
16 | "index.html",
17 | keys: keys,
18 | api_versions: Application.get_env(:api_web, :versions)[:versions]
19 | )
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/apps/model/lib/model/agency.ex:
--------------------------------------------------------------------------------
1 | defmodule Model.Agency do
2 | @moduledoc """
3 | Agency represents a branded agency operating transit services.
4 | """
5 |
6 | use Recordable, [
7 | :id,
8 | :agency_name
9 | ]
10 |
11 | @type id :: String.t()
12 |
13 | @typedoc """
14 | * `:id` - Unique ID
15 | * `:agency_name` - Full name of the agency. See
16 | [GTFS `agency.txt` `agency_name`](https://github.com/google/transit/blob/master/gtfs/spec/en/reference.md#agencytxt)
17 | """
18 | @type t :: %__MODULE__{
19 | id: id,
20 | agency_name: String.t()
21 | }
22 | end
23 |
--------------------------------------------------------------------------------
/apps/api_web/lib/api_web/plugs/redirect_already_authenticated.ex:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.Plugs.RedirectAlreadyAuthenticated do
2 | @moduledoc """
3 | Redirects to Client Portal index page when user is already authenticated.
4 | """
5 | import Plug.Conn
6 | import Phoenix.Controller, only: [redirect: 2]
7 |
8 | def init(opts), do: opts
9 |
10 | def call(conn, _) do
11 | case conn.assigns[:user] do
12 | %ApiAccounts.User{} ->
13 | conn
14 | |> redirect(to: ApiWeb.Router.Helpers.portal_path(conn, :index))
15 | |> halt()
16 |
17 | _ ->
18 | conn
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/apps/api_web/lib/api_web/templates/admin/accounts/key/edit.html.heex:
--------------------------------------------------------------------------------
1 | Edit Key
2 |
3 |
Key Details
4 |
5 |
6 | User E-mail: {@user.email}
7 | Key: {@key.key}
8 |
9 |
10 |
11 |
12 | {render("form.html",
13 | changeset: @changeset,
14 | action: admin_key_path(@conn, :update, @user, @key),
15 | method: :put,
16 | api_versions: @api_versions
17 | )}
18 |
19 | {link("Back", to: admin_user_path(@conn, :show, @user))}
20 |
--------------------------------------------------------------------------------
/apps/api_web/lib/api_web/views/occupancy_view.ex:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.OccupancyView do
2 | use ApiWeb.Web, :api_view
3 |
4 | attributes([:id, :status, :percentage])
5 |
6 | @spec id(Model.CommuterRailOccupancy.t()) :: String.t()
7 | def id(%{trip_name: trip_name}, _conn) do
8 | "occupancy-" <> trip_name
9 | end
10 |
11 | @spec status(Model.CommuterRailOccupancy.t()) :: String.t()
12 | def status(%{status: :many_seats_available}), do: "MANY_SEATS_AVAILABLE"
13 | def status(%{status: :few_seats_available}), do: "FEW_SEATS_AVAILABLE"
14 | def status(%{status: :full}), do: "FULL"
15 | def status(_), do: ""
16 | end
17 |
--------------------------------------------------------------------------------
/apps/api_web/lib/api_web/views/service_view.ex:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.ServiceView do
2 | use ApiWeb.Web, :api_view
3 |
4 | location(:service_location)
5 |
6 | def service_location(service, conn), do: service_path(conn, :show, service.id)
7 |
8 | attributes([
9 | :start_date,
10 | :end_date,
11 | :valid_days,
12 | :description,
13 | :schedule_name,
14 | :schedule_type,
15 | :schedule_typicality,
16 | :rating_start_date,
17 | :rating_end_date,
18 | :rating_description,
19 | :added_dates,
20 | :added_dates_notes,
21 | :removed_dates,
22 | :removed_dates_notes
23 | ])
24 | end
25 |
--------------------------------------------------------------------------------
/apps/fetch/lib/fetch/app.ex:
--------------------------------------------------------------------------------
1 | defmodule Fetch.App do
2 | @moduledoc """
3 |
4 | Application for the various fetching servers. If configured with a
5 | `test: true` key, then does not start the fetchers.
6 |
7 | """
8 | use Application
9 |
10 | def start(_type, _args) do
11 | opts = Application.fetch_env!(:fetch, Fetch)
12 |
13 | children = [
14 | {Registry, keys: :unique, name: Fetch.Registry},
15 | :hackney_pool.child_spec(:fetch_pool, []),
16 | {Fetch, opts}
17 | ]
18 |
19 | opts = [strategy: :one_for_one, name: Fetch.App]
20 | Supervisor.start_link(children, opts)
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/apps/api_web/test/api_web/controller_helpers_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.ControllerHelpersTest do
2 | @moduledoc false
3 | use ApiWeb.ConnCase, async: true
4 | import ApiWeb.ControllerHelpers
5 |
6 | describe "conn_service_date/1" do
7 | test "returns a service date and a conn", %{conn: conn} do
8 | assert {%Plug.Conn{}, %Date{}} = conn_service_date(conn)
9 | end
10 |
11 | test "caches the initial date", %{conn: conn} do
12 | initial_conn = conn
13 | {conn, date} = conn_service_date(conn)
14 | assert conn != initial_conn
15 | assert {conn, date} == conn_service_date(conn)
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/apps/model/lib/model/wgs84.ex:
--------------------------------------------------------------------------------
1 | defmodule Model.WGS84 do
2 | @moduledoc """
3 | A [WGS-84](https://en.wikipedia.org/wiki/World_Geodetic_System#A_new_World_Geodetic_System:_WGS.C2.A084) latitude and
4 | longitude
5 | """
6 |
7 | @typedoc """
8 | Degrees East, in the [WGS-84](https://en.wikipedia.org/wiki/World_Geodetic_System#A_new_World_Geodetic_System:_WGS.C2.A084)
9 | coordinate system.
10 | """
11 | @type latitude :: float
12 |
13 | @typedoc """
14 | Degrees East, in the [WGS-84](https://en.wikipedia.org/wiki/World_Geodetic_System#Longitudes_on_WGS.C2.A084)
15 | coordinate system.
16 | """
17 | @type longitude :: float
18 | end
19 |
--------------------------------------------------------------------------------
/apps/api_web/test/api_web/rate_limiter/memcache_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.RateLimiter.MemcacheTest do
2 | use ExUnit.Case
3 | import ApiWeb.RateLimiter.Memcache
4 |
5 | @moduletag :memcache
6 |
7 | describe "rate_limited?/2" do
8 | test "correctly determines whether the user is rate limited, and returns the correct number of requests" do
9 | {:ok, _} = start_link(clear_interval: 1000)
10 | user_id = "#{System.monotonic_time()}"
11 |
12 | assert {:remaining, 1} = rate_limited?(user_id, 2)
13 | assert {:remaining, 0} = rate_limited?(user_id, 2)
14 | assert :rate_limited == rate_limited?(user_id, 2)
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/rel/env.sh.eex:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # Sets and enables heart (recommended only in daemon mode)
4 | # case $RELEASE_COMMAND in
5 | # daemon*)
6 | # HEART_COMMAND="$RELEASE_ROOT/bin/$RELEASE_NAME $RELEASE_COMMAND"
7 | # export HEART_COMMAND
8 | # export ELIXIR_ERL_OPTIONS="-heart"
9 | # ;;
10 | # *)
11 | # ;;
12 | # esac
13 |
14 | # Set the release to work across nodes. If using the long name format like
15 | # the one below (my_app@127.0.0.1), you need to also uncomment the
16 | # RELEASE_DISTRIBUTION variable below. Must be "sname", "name" or "none".
17 |
18 | DIRNAME=$(dirname $0)
19 |
20 | export RELEASE_DISTRIBUTION=sname
21 | export RELEASE_NODE=api
22 |
--------------------------------------------------------------------------------
/apps/parse/lib/parse/helpers.ex:
--------------------------------------------------------------------------------
1 | defmodule Parse.Helpers do
2 | @moduledoc "Helper functions for parsing"
3 |
4 | @doc "Copies a binary, otherwise returns the term unchanged"
5 | @spec copy(term) :: term
6 | def copy(binary) when is_binary(binary) do
7 | :binary.copy(binary)
8 | end
9 |
10 | def copy(other), do: other
11 |
12 | @doc "Copies a binary, but treats the empty string as a nil value"
13 | @spec optional_copy(term) :: term
14 | def optional_copy("") do
15 | # empty string is a default value and should be treated as a not-provided
16 | # value
17 | nil
18 | end
19 |
20 | def optional_copy(value) do
21 | copy(value)
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/apps/state_mediator/config/test.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | config :state_mediator, State.FakeModuleA, source: {:system, "FAKE_VAR_A", "default_a"}
4 |
5 | config :state_mediator, State.FakeModuleB, source: {:system, "FAKE_VAR_B", "default_b"}
6 |
7 | config :state_mediator, State.FakeModuleC, source: "default_c"
8 |
9 | config :state_mediator, :commuter_rail_crowding,
10 | enabled: "true",
11 | s3_bucket: "mbta-gtfs-commuter-rail-prod",
12 | s3_object: "crowding-trends.json",
13 | source: "s3"
14 |
15 | # Record the original working directory so that when it changes during the
16 | # test run, we can still find the MBTA_GTFS_FILE.
17 | config :state_mediator, :cwd, File.cwd!()
18 |
--------------------------------------------------------------------------------
/semaphore/build_push.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | # Required configuration:
5 | # * APP
6 | # * DOCKER_REPO
7 |
8 | # log into docker hub if credentials are in the environment
9 | if [ -n "$DOCKER_USERNAME" ]; then
10 | echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin
11 | fi
12 |
13 | # build docker image and tag it with git hash and aws environment
14 | githash=$(git rev-parse --short HEAD)
15 | docker build --pull -t $APP:latest .
16 | docker tag $APP:latest $DOCKER_REPO:git-$githash
17 | docker tag $APP:latest $DOCKER_REPO:latest
18 |
19 | # push images to ECS image repo
20 | docker push $DOCKER_REPO:git-$githash
21 | docker push $DOCKER_REPO:latest
22 |
--------------------------------------------------------------------------------
/apps/parse/lib/parse/multi_route_trips.ex:
--------------------------------------------------------------------------------
1 | defmodule Parse.MultiRouteTrips do
2 | @moduledoc """
3 | Parses `multi_route_trips.txt` CSV from GTFS zip
4 |
5 | added_route_id,trip_id
6 | 12,hybrid
7 | 34,hybrid
8 | CR-Lowell,"gene's"
9 |
10 | """
11 |
12 | use Parse.Simple
13 | alias Model.MultiRouteTrip
14 |
15 | @doc """
16 | Parses (non-header) row of `multi_route_trips.txt`
17 | """
18 | @spec parse_row(row :: %{optional(String.t()) => String.t()}) :: MultiRouteTrip.t()
19 | def parse_row(row) do
20 | %MultiRouteTrip{
21 | added_route_id: copy(row["added_route_id"]),
22 | trip_id: copy(row["trip_id"])
23 | }
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/apps/parse/lib/parse/timezone.ex:
--------------------------------------------------------------------------------
1 | defmodule Parse.Timezone do
2 | @moduledoc """
3 | Maintains the local timezone name, offset, and abbreviation.
4 |
5 | The parsers use this to convert a UNIX timestamp into the appropriate local DateTime in an efficient manner.
6 | """
7 | @doc """
8 | Given a unix timestamp, converts it to the appropriate local timezone.
9 |
10 | iex> unix_to_local(1522509910)
11 | #DateTime<2018-03-31 11:25:10-04:00 EDT America/New_York>
12 | """
13 | def unix_to_local(unix_timestamp) when is_integer(unix_timestamp) do
14 | {:ok, datetime} = FastLocalDatetime.unix_to_datetime(unix_timestamp, "America/New_York")
15 | datetime
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/apps/api_web/test/support/fixtures.ex:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.Fixtures do
2 | @moduledoc false
3 |
4 | @test_password "password"
5 | @valid_user_attrs %{
6 | email: "authorized@example.com",
7 | password: @test_password
8 | }
9 |
10 | def fixture(:totp_user) do
11 | time = DateTime.utc_now() |> DateTime.add(-35, :second)
12 | {:ok, user} = ApiAccounts.create_user(@valid_user_attrs)
13 | {:ok, user} = ApiAccounts.generate_totp_secret(user)
14 |
15 | {:ok, user} =
16 | ApiAccounts.enable_totp(
17 | user,
18 | NimbleTOTP.verification_code(user.totp_secret_bin, time: time),
19 | time: time
20 | )
21 |
22 | user
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/apps/api_web/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | excludes = [:integration]
2 |
3 | # try to connect to memcache: if it fails, don't run those tests
4 | excludes =
5 | case :gen_tcp.connect(~c"localhost", 11_211, [:inet], 100) do
6 | {:ok, sock} ->
7 | :gen_tcp.close(sock)
8 | excludes
9 |
10 | {:error, _} ->
11 | [:memcache | excludes]
12 | end
13 |
14 | ExUnit.start(exclude: excludes)
15 |
16 | defmodule ApiWeb.Test.ProcessHelper do
17 | use ExUnit.Case
18 |
19 | def assert_stopped(pid) do
20 | if Process.alive?(pid) do
21 | ref = Process.monitor(pid)
22 | assert_receive {:DOWN, ^ref, :process, ^pid, _}
23 | else
24 | :ok
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/apps/model/lib/model/facility/property.ex:
--------------------------------------------------------------------------------
1 | defmodule Model.Facility.Property do
2 | @moduledoc """
3 | A property of a facility. The names are not unique, even within a facility_id.
4 | """
5 |
6 | use Recordable, [
7 | :facility_id,
8 | :name,
9 | :value,
10 | :updated_at
11 | ]
12 |
13 | @typedoc """
14 | * `:name` - Name of the property
15 | * `:facility_id` - The `Model.Facility.id` this property applies to.
16 | * `:value` - Value of the property
17 | """
18 | @type t :: %__MODULE__{
19 | name: String.t(),
20 | facility_id: Model.Facility.id(),
21 | value: term,
22 | updated_at: DateTime.t() | nil
23 | }
24 | end
25 |
--------------------------------------------------------------------------------
/apps/api_web/test/api_web/plugs/clear_metadata_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.Plugs.ClearMetadataTest do
2 | @moduledoc false
3 | use ApiWeb.ConnCase
4 | alias ApiWeb.Plugs.ClearMetadata
5 |
6 | @opts ClearMetadata.init(~w(metadata_value)a)
7 |
8 | test "clears given metadata values", %{conn: conn} do
9 | Logger.metadata(metadata_value: :value)
10 | assert conn == ClearMetadata.call(conn, @opts)
11 | assert Logger.metadata() == []
12 | end
13 |
14 | test "does not clear metadata values not given", %{conn: conn} do
15 | Logger.metadata(metadata_keep: :value)
16 | assert conn == ClearMetadata.call(conn, @opts)
17 | assert Logger.metadata() == [metadata_keep: :value]
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/apps/health/lib/health/checker.ex:
--------------------------------------------------------------------------------
1 | defmodule Health.Checker do
2 | @moduledoc """
3 | Aggregator for multiple health checks.
4 |
5 | We can return both data about the checks (current/0) as well as a boolean
6 | as to whether we're healthy or not (healthy?/0).
7 | """
8 | @checkers Application.compile_env(:health, :checkers)
9 |
10 | def current do
11 | :current
12 | |> each_checker
13 | |> Enum.reduce([], &Keyword.merge/2)
14 | end
15 |
16 | def healthy? do
17 | :healthy?
18 | |> each_checker
19 | |> Enum.all?()
20 | end
21 |
22 | defp each_checker(fun_name) do
23 | for checker <- @checkers do
24 | apply(checker, fun_name, [])
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/apps/parse/lib/parse/agency.ex:
--------------------------------------------------------------------------------
1 | defmodule Parse.Agency do
2 | @moduledoc """
3 | Parses `agency.txt` CSV from GTFS zip
4 |
5 | agency_id,agency_name,agency_url,agency_timezone,agency_lang,agency_phone
6 | 1,MBTA,http://www.mbta.com,America/New_York,EN,617-222-3200
7 | """
8 |
9 | use Parse.Simple
10 | alias Model.Agency
11 |
12 | @doc """
13 | Parses (non-header) row of `agency.txt`
14 |
15 | ## Columns
16 |
17 | * `"agency_id"` - `Model.Agency.t` - `id`
18 | * `"agency_name"` - `Model.Agency.t` - `agency_name`
19 | """
20 | def parse_row(row) do
21 | %Agency{
22 | id: copy(row["agency_id"]),
23 | agency_name: copy(row["agency_name"])
24 | }
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/apps/parse/lib/parse/calendar_dates.ex:
--------------------------------------------------------------------------------
1 | defmodule Parse.CalendarDates do
2 | @moduledoc """
3 | Parser for GTFS calendar_dates.txt
4 | """
5 | @behaviour Parse
6 | defstruct [:service_id, :date, :added, :holiday_name]
7 |
8 | def parse(blob) do
9 | blob
10 | |> BinaryLineSplit.stream!()
11 | |> SimpleCSV.decode()
12 | |> Enum.map(&parse_row/1)
13 | end
14 |
15 | defp parse_row(row) do
16 | %__MODULE__{
17 | service_id: :binary.copy(row["service_id"]),
18 | date: row["date"] |> Timex.parse!("{YYYY}{0M}{0D}") |> NaiveDateTime.to_date(),
19 | added: row["exception_type"] == "1",
20 | holiday_name: :binary.copy(row["holiday_name"])
21 | }
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | # App artifacts
3 | /_build
4 | /tmp
5 | /data
6 | /db
7 | /deps
8 | /*.ez
9 | /cache
10 | /Mnesia.*/
11 | # Generate on crash by the VM
12 | erl_crash.dump
13 | /rel/api
14 | *.secret.exs
15 |
16 | apps/api/priv/static/swagger.json
17 |
18 | # Elastic Beanstalk Files
19 | aws/
20 | .elasticbeanstalk/*
21 | !.elasticbeanstalk/*.cfg.yml
22 | !.elasticbeanstalk/*.global.yml
23 | /api-build.zip
24 | apps/state/bench/snapshots
25 | cover
26 |
27 | # ex_doc
28 | /doc
29 | /apps/api_web/priv/static/swagger.json
30 |
31 | .vscode/
32 | .elixir_ls/
33 |
34 | # Python artifacts (for locust)
35 | __pycache__
36 |
37 | # local db
38 | shared-local-instance.db
39 | /bin
40 | dynamodb-local-metadata.json
41 |
--------------------------------------------------------------------------------
/apps/parse/test/parse/agency_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Parse.AgencyTest do
2 | use ExUnit.Case, async: true
3 |
4 | import Parse.Agency
5 | alias Model.Agency
6 |
7 | describe "parse_row/1" do
8 | test "parses a route CSV map into an %Agency{}" do
9 | row = %{
10 | "agency_id" => "1",
11 | "agency_name" => "MBTA",
12 | "agency_url" => "http://www.mbta.com",
13 | "agency_timezone" => "America/New_York",
14 | "agency_lang" => "EN",
15 | "agency_phone" => "617-222-3200"
16 | }
17 |
18 | expected = %Agency{
19 | id: "1",
20 | agency_name: "MBTA"
21 | }
22 |
23 | assert parse_row(row) == expected
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/apps/state_mediator/test/state_mediator_test.exs:
--------------------------------------------------------------------------------
1 | defmodule StateMediatorTest do
2 | use ExUnit.Case
3 |
4 | describe "source_url/1" do
5 | test "returns default config value when no environment variable set" do
6 | assert StateMediator.source_url(State.FakeModuleA) == "default_a"
7 | end
8 |
9 | test "returns environment value when set set" do
10 | expected = "config_b"
11 | :os.putenv(~c"FAKE_VAR_B", String.to_charlist(expected))
12 | assert StateMediator.source_url(State.FakeModuleB) == expected
13 | end
14 |
15 | test "returns config value when explicitly set" do
16 | assert StateMediator.source_url(State.FakeModuleC) == "default_c"
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/apps/api_accounts/.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 | *.secret.exs
23 |
24 | # for the DynamoDB local instance used in testing
25 | bin/
26 | shared-local-instance.db
27 |
--------------------------------------------------------------------------------
/apps/health/lib/health.ex:
--------------------------------------------------------------------------------
1 | defmodule Health do
2 | @moduledoc """
3 | Monitors health of the rest of the OTP applications in the umbrella project
4 | """
5 |
6 | use Application
7 |
8 | # See http://elixir-lang.org/docs/stable/elixir/Application.html
9 | # for more information on OTP Applications
10 | def start(_type, _args) do
11 | # Define workers and child supervisors to be supervised
12 | children = [
13 | Health.Checkers.State
14 | ]
15 |
16 | # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
17 | # for other strategies and supported options
18 | opts = [strategy: :one_for_one, name: Health.Supervisor]
19 | Supervisor.start_link(children, opts)
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/apps/api_web/README.md:
--------------------------------------------------------------------------------
1 | # Api
2 |
3 | **TODO: Add description**
4 |
5 | ## Installation
6 |
7 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed as:
8 |
9 | 1. Add `api` to your list of dependencies in `mix.exs`:
10 |
11 | ```elixir
12 | def deps do
13 | [{:api, "~> 0.1.0"}]
14 | end
15 | ```
16 |
17 | 2. Ensure `api` is started before your application:
18 |
19 | ```elixir
20 | def application do
21 | [applications: [:api]]
22 | end
23 | ```
24 |
25 |
26 | ## Swagger and Tests
27 |
28 | The Swagger API documentation is used to validate incoming requests to all
29 | requests that use the :api pipeline (see
30 | `apps/api_web/lib/api_web/router.ex`).
31 |
--------------------------------------------------------------------------------
/apps/api_web/lib/api_web/plugs/clear_metadata.ex:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.Plugs.ClearMetadata do
2 | @moduledoc """
3 | Clear Logger metadata at the start of a new request.
4 |
5 | Bandit is more agressive about re-using processes than Cowboy was, which means we
6 | can't rely on the old behavior of the Logger metadata being automatically cleared
7 | when the process is terminated.
8 |
9 | This takes a list of metadata keys to clear.
10 | """
11 | @behaviour Plug
12 |
13 | @impl Plug
14 | def init(keys_to_clear) do
15 | for key <- keys_to_clear do
16 | {key, nil}
17 | end
18 | end
19 |
20 | @impl Plug
21 | def call(conn, metadata) do
22 | _ = Logger.metadata(metadata)
23 |
24 | conn
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/apps/api_web/lib/api_web/templates/client_portal/user/show.html.heex:
--------------------------------------------------------------------------------
1 |
2 |
Account Information
3 |
4 |
Email
5 |
{@user.email}
6 |
7 |
Phone
8 |
9 | <%= if @user.phone do %>
10 | {@user.phone}
11 | <% else %>
12 | Not set
13 | <% end %>
14 |
15 |
16 |
17 | {link("Edit Information", to: user_path(@conn, :edit))}
18 |
19 |
20 |
21 | {link("Update Password", to: user_path(@conn, :edit_password))}
22 |
23 |
24 |
25 | {link("Configure 2-Factor authentication", to: user_path(@conn, :configure_2fa))}
26 |
27 |
28 |
--------------------------------------------------------------------------------
/apps/api_web/test/api_web/plugs/require_user_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.Plugs.RequireUserTest do
2 | use ApiWeb.ConnCase, async: true
3 |
4 | test "init" do
5 | opts = []
6 | assert ApiWeb.Plugs.RequireUser.init(opts) == opts
7 | end
8 |
9 | test "proceeds if user present", %{conn: conn} do
10 | conn =
11 | conn
12 | |> assign(:user, %ApiAccounts.User{})
13 | |> ApiWeb.Plugs.RequireUser.call([])
14 |
15 | refute conn.status
16 | refute conn.halted
17 | end
18 |
19 | test "redirect to login if user isn't present", %{conn: conn} do
20 | conn = ApiWeb.Plugs.RequireUser.call(conn, [])
21 | assert redirected_to(conn) == session_path(conn, :new)
22 | assert conn.halted
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/apps/api_web/lib/api_web/templates/admin/accounts/key/index.html.heex:
--------------------------------------------------------------------------------
1 | Find user by key
2 | {render("search.html",
3 | conn: @conn,
4 | action: admin_key_path(@conn, :find_user_by_key),
5 | method: :post
6 | )}
7 |
8 | Pending Key Approvals
9 |
10 |
11 |
12 | User Email
13 | Requested Date
14 |
15 |
16 |
17 | <%= for {key, user} <- @key_requests do %>
18 |
19 |
20 | {link(user.email, to: admin_user_path(@conn, :show, user), target: "_blank")}
21 |
22 | {key.requested_date}
23 |
24 | <% end %>
25 |
26 |
27 |
--------------------------------------------------------------------------------
/apps/model/lib/model/commuter_rail_occupancy.ex:
--------------------------------------------------------------------------------
1 | defmodule Model.CommuterRailOccupancy do
2 | @moduledoc """
3 | An expected or predicted level of occupancy for a given commuter rail trip.
4 | Stores the data we receive from Keolis, indexed by train name.
5 | Naming inspired by [GTFS-Occupancies proposal](https://github.com/google/transit/pull/240).
6 | """
7 |
8 | use Recordable, [
9 | :trip_name,
10 | :status,
11 | :percentage
12 | ]
13 |
14 | @type status ::
15 | :many_seats_available
16 | | :few_seats_available
17 | | :full
18 |
19 | @type t :: %__MODULE__{
20 | trip_name: String.t(),
21 | status: status(),
22 | percentage: non_neg_integer()
23 | }
24 | end
25 |
--------------------------------------------------------------------------------
/.github/workflows/asana.yml:
--------------------------------------------------------------------------------
1 | name: Asana integration for GitHub PRs
2 | on:
3 | workflow_dispatch:
4 | pull_request:
5 | types: [review_requested, closed, opened, reopened, converted_to_draft, edited, ready_for_review]
6 | pull_request_review:
7 | types: [submitted]
8 |
9 | jobs:
10 | call-workflow:
11 | uses: mbta/workflows/.github/workflows/asana.yml@main
12 | with:
13 | development-section: "In Development"
14 | review-section: "Pending Review"
15 | merged-section: "Merged / Not Deployed"
16 | trigger-phrase: "\\*\\*Asana Ticket:\\*\\*"
17 | attach-pr: true
18 | secrets:
19 | asana-token: ${{ secrets.ASANA_PERSONAL_ACCESS_TOKEN }}
20 | github-secret: ${{ secrets.ASANA_GITHUB_INTEGRATION_SECRET }}
21 |
--------------------------------------------------------------------------------
/apps/api_web/lib/api_web/view_helpers.ex:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.ViewHelpers do
2 | @moduledoc """
3 | Shared functions for HTML views.
4 | """
5 | use Phoenix.Component
6 |
7 | @doc """
8 | Generates a form-group div for a field.
9 |
10 | If the field has an error, the appropriate error class is added to the
11 | form group div.
12 | """
13 | def form_group(%{form: form, field: field} = assigns) do
14 | if Map.get(form.source.errors, field, []) == [] do
15 | ~H"""
16 |
17 | {render_slot(@inner_block)}
18 |
19 | """
20 | else
21 | ~H"""
22 |
23 | {render_slot(@inner_block)}
24 |
25 | """
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/apps/api_web/lib/api_web/plugs/require_admin.ex:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.Plugs.RequireAdmin do
2 | @moduledoc """
3 | Plug enforcing a user to have the administrator role.
4 | """
5 | import Plug.Conn
6 | import Phoenix.Controller, only: [render: 3, put_view: 2]
7 |
8 | def init(opts), do: opts
9 |
10 | def call(conn, _opts) do
11 | conn
12 | |> fetch_user()
13 | |> authenticate(conn)
14 | end
15 |
16 | defp fetch_user(conn) do
17 | conn.assigns[:user]
18 | end
19 |
20 | defp authenticate(%ApiAccounts.User{role: "administrator"}, conn), do: conn
21 |
22 | defp authenticate(_, conn) do
23 | conn
24 | |> put_status(:not_found)
25 | |> put_view(ApiWeb.ErrorView)
26 | |> render("404.html", [])
27 | |> halt()
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/apps/parse/test/parse/directions_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Parse.DirectionsTest do
2 | use ExUnit.Case, async: true
3 |
4 | import Parse.Directions
5 |
6 | describe "parse_row/1" do
7 | test "parses a route CSV map into a valid structure" do
8 | row = %{
9 | "route_id" => "708",
10 | "direction_id" => "0",
11 | "direction" => "Outbound",
12 | "direction_destination" => "Beth Israel Deaconess or Boston Medical Center"
13 | }
14 |
15 | expected = %Parse.Directions{
16 | route_id: "708",
17 | direction_id: "0",
18 | direction: "Outbound",
19 | direction_destination: "Beth Israel Deaconess or Boston Medical Center"
20 | }
21 |
22 | assert parse_row(row) == expected
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/apps/api_accounts/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 | import Config
4 |
5 | config :ex_aws,
6 | dynamodb: [
7 | port: "8000",
8 | scheme: "http://",
9 | host: "localhost"
10 | ],
11 | json_codec: Jason
12 |
13 | config :ex_aws, :hackney_opts,
14 | recv_timeout: 30_000,
15 | pool: :ex_aws_pool
16 |
17 | config :email_checker,
18 | default_dns: {8, 8, 8, 8},
19 | smtp_retries: 1,
20 | timeout_milliseconds: 6000,
21 | validations: [EmailChecker.Check.Format, EmailChecker.Check.MX]
22 |
23 | config :api_accounts, ApiAccounts.Mailer, adapter: Bamboo.SesAdapter
24 |
25 | config :api_accounts, migrate_on_start: false
26 |
27 | import_config "#{config_env()}.exs"
28 |
--------------------------------------------------------------------------------
/apps/fetch/test/fetch_test.exs:
--------------------------------------------------------------------------------
1 | defmodule FetchTest do
2 | use ExUnit.Case, async: true
3 | import Plug.Conn
4 |
5 | setup do
6 | lasso = Lasso.open()
7 | url = "http://localhost:#{lasso.port}"
8 | {:ok, %{lasso: lasso, url: url}}
9 | end
10 |
11 | test "can fetch a URL", %{lasso: lasso, url: url} do
12 | Lasso.expect(lasso, "GET", "/", fn conn ->
13 | etag = "etag"
14 |
15 | case get_req_header(conn, "if-none-match") do
16 | [^etag] ->
17 | resp(conn, 304, "")
18 |
19 | [] ->
20 | conn
21 | |> put_resp_header("ETag", etag)
22 | |> resp(200, "body")
23 | end
24 | end)
25 |
26 | assert Fetch.fetch_url(url) == {:ok, "body"}
27 | assert Fetch.fetch_url(url) == :unmodified
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/apps/health/lib/health/checkers/run_queue.ex:
--------------------------------------------------------------------------------
1 | defmodule Health.Checkers.RunQueue do
2 | @moduledoc """
3 | Health check for monitoring the Erlang [Run
4 | Queue](http://erlang.org/doc/man/erlang.html#statistics-1).
5 |
6 | This check always returns healthy as we don't want to kill tasks based on the run queue length.
7 | Instead it logs the maximum run queue length across all schedulers for monitoring purposes.
8 | """
9 | require Logger
10 |
11 | def current do
12 | [run_queue: max_queue_length()]
13 | end
14 |
15 | def healthy? do
16 | max_length = max_queue_length()
17 | _ = Logger.info("run_queue_check max_run_queue_length=#{max_length}")
18 | true
19 | end
20 |
21 | defp max_queue_length do
22 | Enum.max(:erlang.statistics(:run_queue_lengths))
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/apps/health/test/health/checkers/ports_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Health.Checkers.PortsTest do
2 | use ExUnit.Case
3 | alias Health.Checkers.Ports
4 |
5 | describe "current/0" do
6 | test "returns an integer number of ports" do
7 | kw = Ports.current()
8 | assert kw[:ports] >= 0
9 | end
10 | end
11 |
12 | describe "healthy?" do
13 | test "true if the number of ports is low" do
14 | assert Ports.healthy?()
15 | end
16 |
17 | test "false if the number of ports is higher than the configuration" do
18 | old = Application.get_env(:health, Ports)
19 |
20 | on_exit(fn ->
21 | Application.put_env(:health, Ports, old)
22 | end)
23 |
24 | Application.put_env(:health, Ports, max_ports: -1)
25 |
26 | refute Ports.healthy?()
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/apps/api_web/lib/api_web/plugs/experimental_features.ex:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.Plugs.ExperimentalFeatures do
2 | @moduledoc """
3 | Allows a requestor to opt into experimental features in the API.
4 |
5 | By including the `x-enable-experimental-features: true` header, a user
6 | can opt into data and features that might change without prior warning
7 | or without a backwards-compatible fallback.
8 |
9 | This places a `:experimental_features_enabled?` in the conn's assigns.
10 | """
11 | @behaviour Plug
12 | import Plug.Conn
13 |
14 | @impl Plug
15 | def init(options) do
16 | options
17 | end
18 |
19 | @impl Plug
20 | def call(conn, _) do
21 | enabled? = get_req_header(conn, "x-enable-experimental-features") == ["true"]
22 | assign(conn, :experimental_features_enabled?, enabled?)
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/apps/api_web/test/api_web/plugs/experimental_features_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.Plugs.ExperimentalFeaturesTest do
2 | import Phoenix.ConnTest
3 | use ApiWeb.ConnCase
4 |
5 | test "init" do
6 | assert ApiWeb.Plugs.ExperimentalFeatures.init([]) == []
7 | end
8 |
9 | describe ":experimental_features_enabled?" do
10 | test "set to true if x-enable-experimental-features header included", %{conn: conn} do
11 | conn = put_req_header(conn, "x-enable-experimental-features", "true")
12 | conn = get(conn, "/stops/")
13 | assert conn.assigns.experimental_features_enabled?
14 | end
15 |
16 | test "set to false if x-enable-experimental-features header absent", %{conn: conn} do
17 | conn = get(conn, "/stops/")
18 | refute conn.assigns.experimental_features_enabled?
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/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 | import Config
4 |
5 | # By default, the umbrella project as well as each child
6 | # application will require this configuration file, ensuring
7 | # they all use the same configuration. While one could
8 | # configure all applications here, we prefer to delegate
9 | # back to each application for organization purposes.
10 | for config <- "../apps/*/config/config.exs" |> Path.expand(__DIR__) |> Path.wildcard() do
11 | import_config config
12 | end
13 |
14 | # Sample configuration (overrides the imported configuration above):
15 | #
16 | # config :logger, :console,
17 | # level: :info,
18 | # format: "$date $time [$level] $metadata$message\n",
19 | # metadata: [:user_id]
20 |
--------------------------------------------------------------------------------
/apps/api_web/lib/api_web/plugs/require_2factor.ex:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.Plugs.Require2Factor do
2 | @moduledoc """
3 | Plug enforcing a user to have 2fa enabled
4 | """
5 |
6 | import Phoenix.Controller
7 |
8 | def init(opts), do: opts
9 |
10 | def call(conn, _opts) do
11 | conn
12 | |> fetch_user()
13 | |> authenticate(conn)
14 | end
15 |
16 | defp fetch_user(conn) do
17 | conn.assigns[:user]
18 | end
19 |
20 | defp authenticate(%ApiAccounts.User{totp_enabled: true}, conn), do: conn
21 |
22 | defp authenticate(_, conn) do
23 | conn
24 | |> put_flash(
25 | :error,
26 | "Account does not have 2-Factor Authentication enabled. Please enable before performing administrative tasks."
27 | )
28 | |> redirect(to: ApiWeb.Router.Helpers.user_path(conn, :configure_2fa))
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/apps/parse/test/parse/feed_info_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Parse.FeedInfoTest do
2 | use ExUnit.Case, async: true
3 | import Parse.FeedInfo
4 | alias Model.Feed
5 |
6 | @blob """
7 | "feed_publisher_name","feed_publisher_url","feed_lang","feed_start_date","feed_end_date","feed_version"
8 | "MBTA","http://www.mbta.com","EN",20170228,20170623,"Spring 2017 version 2D, 3/2/17"
9 | """
10 |
11 | describe "parse/1" do
12 | test "parses a CSV into a list of %Feed{} structs" do
13 | assert parse(@blob) ==
14 | [
15 | %Feed{
16 | name: "MBTA",
17 | start_date: ~D[2017-02-28],
18 | end_date: ~D[2017-06-23],
19 | version: "Spring 2017 version 2D, 3/2/17"
20 | }
21 | ]
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/apps/api_web/lib/api_web/views/error_helpers.ex:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.ErrorHelpers do
2 | @moduledoc """
3 | Conveniences for translating and building error messages.
4 | """
5 | use PhoenixHTML4Compat
6 |
7 | @doc """
8 | Translates an error message using gettext.
9 | """
10 | def translate_error({msg, _opts}) do
11 | msg
12 | end
13 |
14 | @doc """
15 | Generates tag for inlined form input errors.
16 | """
17 | def error_tag(form, field, humanized \\ nil) do
18 | Enum.map(Map.get(form.source.errors, field, []), fn error ->
19 | humanized_field = humanized || Phoenix.Naming.humanize(field)
20 | translated_error = translate_error({error, []})
21 | error_message = "#{humanized_field} #{translated_error}."
22 | content_tag(:span, error_message, class: "help-block")
23 | end)
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/apps/events/lib/events/gather.ex:
--------------------------------------------------------------------------------
1 | defmodule Events.Gather do
2 | @moduledoc """
3 | Gathers multiple event `keys` so that only when all `keys` are `received` is `callback` called, so that event
4 | callbacks don't need to handle subsets of events.
5 | """
6 |
7 | defstruct [:keys, :callback, received: %{}]
8 |
9 | def new(keys, callback) when is_list(keys) and is_function(callback, 1) do
10 | %__MODULE__{keys: MapSet.new(keys), callback: callback}
11 | end
12 |
13 | def update(%__MODULE__{keys: keys, received: received, callback: callback} = state, key, value) do
14 | received = Map.put(received, key, value)
15 |
16 | if received |> Map.keys() |> MapSet.new() == keys do
17 | callback.(received)
18 | %{state | received: %{}}
19 | else
20 | %{state | received: received}
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/apps/api_web/lib/api_web/controllers/health_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.HealthController do
2 | use ApiWeb.Web, :controller
3 |
4 | require Logger
5 |
6 | def index(conn, _params) do
7 | health = Health.Checker.current()
8 |
9 | status =
10 | if Health.Checker.healthy?() do
11 | :ok
12 | else
13 | :service_unavailable
14 | end
15 |
16 | body =
17 | health
18 | |> Enum.into(%{})
19 | |> Jason.encode!()
20 |
21 | _ = log_health_check(health, status)
22 |
23 | conn
24 | |> put_resp_content_type("application/json")
25 | |> send_resp(status, body)
26 | end
27 |
28 | defp log_health_check(health, :service_unavailable) do
29 | Logger.info("health_check healthy=false #{inspect(health)}")
30 | end
31 |
32 | defp log_health_check(_, _), do: :ignored
33 | end
34 |
--------------------------------------------------------------------------------
/apps/api_web/test/api_web/plugs/deadline_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.Plugs.DeadlineTest do
2 | @moduledoc false
3 | use ApiWeb.ConnCase, async: true
4 | import ApiWeb.Plugs.Deadline
5 |
6 | setup %{conn: conn} do
7 | conn = call(conn, init([]))
8 | {:ok, %{conn: conn}}
9 | end
10 |
11 | describe "check!/1" do
12 | test "returns :ok if the deadline is met", %{conn: conn} do
13 | assert :ok =
14 | conn
15 | |> set(5_000)
16 | |> check!
17 | end
18 |
19 | test "returns :ok if no deadline was set", %{conn: conn} do
20 | assert :ok = check!(conn)
21 | end
22 |
23 | test "raises an exception if the deadline is missed", %{conn: conn} do
24 | conn = set(conn, -1)
25 |
26 | assert_raise ApiWeb.Plugs.Deadline.Error, fn ->
27 | check!(conn)
28 | end
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/apps/parse/lib/parse/time.ex:
--------------------------------------------------------------------------------
1 | defmodule Parse.Time do
2 | @moduledoc """
3 | Helpers for times and dates
4 | """
5 |
6 | @spec now() :: DateTime.t()
7 | def now do
8 | now_unix = System.system_time(:second)
9 | {:ok, dt} = FastLocalDatetime.unix_to_datetime(now_unix, "America/New_York")
10 | dt
11 | end
12 |
13 | @spec service_date() :: Date.t()
14 | @spec service_date(DateTime.t()) :: Date.t()
15 | def service_date(current_time \\ DateTime.utc_now())
16 |
17 | def service_date(%{year: _} = current_time) do
18 | current_unix = DateTime.to_unix(current_time)
19 | back_three_hours = current_unix - 10_800
20 | {:ok, dt} = FastLocalDatetime.unix_to_datetime(back_three_hours, "America/New_York")
21 | DateTime.to_date(dt)
22 | end
23 |
24 | def service_date(%Timex.AmbiguousDateTime{before: before}) do
25 | service_date(before)
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/apps/state/test/state/line_test.exs:
--------------------------------------------------------------------------------
1 | defmodule State.LineTest do
2 | use ExUnit.Case
3 | alias Model.Line
4 |
5 | setup do
6 | State.Line.new_state([])
7 | :ok
8 | end
9 |
10 | test "returns nil for unknown line" do
11 | assert State.Line.by_id("1") == nil
12 | end
13 |
14 | test "it can add a line and query it" do
15 | line = %Line{
16 | id: "1",
17 | short_name: "1st Line",
18 | long_name: "First Line",
19 | color: "00843D",
20 | text_color: "FFFFFF",
21 | sort_order: 1
22 | }
23 |
24 | State.Line.new_state([line])
25 |
26 | assert State.Line.by_id("1") == %Line{
27 | id: "1",
28 | short_name: "1st Line",
29 | long_name: "First Line",
30 | color: "00843D",
31 | text_color: "FFFFFF",
32 | sort_order: 1
33 | }
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/rel/.ebextensions/02nginx.config:
--------------------------------------------------------------------------------
1 | files:
2 | "/etc/nginx/nginx.conf":
3 | mode: "00644"
4 | owner: "root"
5 | group: "root"
6 | content: |
7 | # Elastic Beanstalk Nginx Configuration File
8 | user nginx;
9 | worker_processes auto;
10 | error_log /var/log/nginx/error.log;
11 | pid /var/run/nginx.pid;
12 | events {
13 | worker_connections 200000;
14 | use epoll;
15 | multi_accept on;
16 | }
17 | http {
18 | include /etc/nginx/mime.types;
19 | default_type application/octet-stream;
20 | access_log /var/log/nginx/access.log;
21 | log_format healthd '$msec"$uri"$status"$request_time"$upstream_response_time"$http_x_forwarded_for';
22 | include /etc/nginx/conf.d/*.conf;
23 | include /etc/nginx/sites-enabled/*;
24 | }
25 |
--------------------------------------------------------------------------------
/apps/api_web/lib/api_web/sentry_event_filter.ex:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.SentryEventFilter do
2 | @moduledoc """
3 | Provides a filter for exceptions coming from 404 errors
4 | """
5 |
6 | # Sentry allows this callback to both modify events before they get sent,
7 | # and filter events to prevent them from being sent at all.
8 | # We only do the latter. Returning false prevents sending.
9 | @spec filter_event(Sentry.Event.t()) :: Sentry.Event.t() | false
10 | def filter_event(%Sentry.Event{
11 | source: :plug,
12 | original_exception: %Phoenix.Router.NoRouteError{}
13 | }) do
14 | false
15 | end
16 |
17 | def filter_event(%Sentry.Event{message: %Sentry.Interfaces.Message{}} = event) do
18 | if String.contains?(event.message.formatted, "{{{%Phoenix.Router.NoRouteError"),
19 | do: false,
20 | else: event
21 | end
22 |
23 | def filter_event(event), do: event
24 | end
25 |
--------------------------------------------------------------------------------
/apps/api_web/lib/api_web/views/route_pattern_view.ex:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.RoutePatternView do
2 | use ApiWeb.Web, :api_view
3 |
4 | location(:route_pattern_location)
5 |
6 | def route_pattern_location(route_pattern, conn),
7 | do: route_pattern_path(conn, :show, route_pattern.id)
8 |
9 | has_one(
10 | :route,
11 | type: :route,
12 | serializer: ApiWeb.RouteView
13 | )
14 |
15 | has_one(
16 | :representative_trip,
17 | type: :trip,
18 | serializer: ApiWeb.TripView,
19 | field: :representative_trip_id
20 | )
21 |
22 | # no cover
23 | attributes([
24 | :direction_id,
25 | :name,
26 | :time_desc,
27 | :typicality,
28 | :sort_order,
29 | :canonical
30 | ])
31 |
32 | def representative_trip(%{representative_trip_id: trip_id}, conn) do
33 | optional_relationship("representative_trip", trip_id, &State.Trip.by_primary_id/1, conn)
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/apps/api_web/lib/api_web/templates/client_portal/user/_forgot_password.html.heex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Forgot Password
4 |
5 |
6 | <%= form_for @changeset, user_path(@conn, :forgot_password_submit), [], fn f -> %>
7 | <%= unless @changeset.valid? do %>
8 |
9 |
Oops, something went wrong! Please check the errors below.
10 |
11 | <% end %>
12 |
13 | <.form_group form={f} field={:email}>
14 | {label(f, :email, class: "control-label")}
15 | {email_input(f, :email, class: "form-control")}
16 | {error_tag(f, :email)}
17 |
18 |
19 |
20 | {submit("Reset Password", class: "btn btn-primary")}
21 |
22 | <% end %>
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/apps/state/test/state/alert/hooks_test.exs:
--------------------------------------------------------------------------------
1 | defmodule State.Alert.HooksTest do
2 | @moduledoc false
3 | use ExUnit.Case, async: true
4 | import State.Alert.Hooks
5 |
6 | @alert %Model.Alert{
7 | id: "alert1",
8 | informed_entity: [
9 | %{
10 | route_type: 3,
11 | route: "1",
12 | direction_id: 0,
13 | stop: "place-cool",
14 | activities: ["BOARD", "RIDE"]
15 | },
16 | %{
17 | route_type: 3,
18 | route: "1",
19 | direction_id: 0,
20 | stop: "place-cool",
21 | activities: ["EXIT"]
22 | }
23 | ],
24 | severity: 1
25 | }
26 |
27 | test "pre_insert_hook/1 merges informed entities" do
28 | [alert] = pre_insert_hook(@alert)
29 | ie = Map.get(alert, :informed_entity)
30 | assert is_list(ie)
31 | assert length(ie) == 1
32 | assert length(ie |> hd |> Map.get(:activities)) == 3
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/apps/api_web/lib/api_web/rate_limiter/limiter.ex:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.RateLimiter.Limiter do
2 | @moduledoc """
3 | Behavior for backends to the V3 API rate limiter.
4 |
5 | - `start_link(opts)` is called to start the backend by the supervisor.
6 | - `rate_limited?(user_id, max_requests)` returns :rate_limited if the user_id has used too many
7 | requests, or else {:remaining, N} where N is the number of requests remaining for the user_id
8 | in this time period.
9 |
10 | The main option passed to `start_link/1` is `clear_interval` which is a
11 | number of milliseconds to bucket the requests into.
12 |
13 | """
14 | @callback start_link(Keyword.t()) :: {:ok, pid}
15 | @callback rate_limited?(String.t(), non_neg_integer) ::
16 | {:remaining, non_neg_integer} | :rate_limited
17 | @callback clear() :: :ok
18 | @callback list() :: [String.t()]
19 |
20 | @optional_callbacks [clear: 0, list: 0]
21 | end
22 |
--------------------------------------------------------------------------------
/apps/model/lib/model/route_pattern.ex:
--------------------------------------------------------------------------------
1 | defmodule Model.RoutePattern do
2 | @moduledoc """
3 | A variant of service run within a single route_id
4 | [GTFS `route_patterns.txt`](https://github.com/mbta/gtfs-documentation/blob/master/reference/gtfs.md#route_patternstxt)
5 | """
6 |
7 | use Recordable, [
8 | :id,
9 | :route_id,
10 | :direction_id,
11 | :name,
12 | :time_desc,
13 | :typicality,
14 | :sort_order,
15 | :representative_trip_id,
16 | :canonical
17 | ]
18 |
19 | @type id :: String.t()
20 | @type t :: %__MODULE__{
21 | id: id,
22 | route_id: Model.Route.id(),
23 | direction_id: Model.Direction.id(),
24 | name: String.t(),
25 | time_desc: String.t() | nil,
26 | typicality: 0..5,
27 | sort_order: integer(),
28 | representative_trip_id: Model.Trip.id(),
29 | canonical: boolean()
30 | }
31 | end
32 |
--------------------------------------------------------------------------------
/apps/api_web/test/api_web_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ApiWebTest do
2 | @moduledoc false
3 | use ExUnit.Case
4 | doctest ApiWeb
5 |
6 | describe "runtime_config!/0" do
7 | setup do
8 | old_env = Application.get_env(:api_web, :api_pipeline)
9 |
10 | on_exit(fn ->
11 | Application.put_env(:api_web, :api_pipeline, old_env)
12 | end)
13 |
14 | :ok
15 | end
16 | end
17 |
18 | test "config/1 returns configuration" do
19 | assert Keyword.keyword?(ApiWeb.config(ApiWeb.Endpoint))
20 | end
21 |
22 | test "config/1 raises when key is missing" do
23 | assert_raise ArgumentError, fn -> ApiWeb.config(:not_exists) end
24 | end
25 |
26 | test "config/2 returns configuration" do
27 | assert is_list(ApiWeb.config(ApiWeb.Endpoint, :url))
28 | end
29 |
30 | test "config/2 raises when key is missing" do
31 | assert_raise RuntimeError, fn -> ApiWeb.config(ApiWeb.Endpoint, :not_exists) end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/apps/state/lib/logger.ex:
--------------------------------------------------------------------------------
1 | defmodule State.Logger do
2 | @moduledoc """
3 | Helpers for logging about `State`
4 | """
5 |
6 | require Logger
7 |
8 | # Don't use `:erlang.convert_time_unit/3` directly on the `microseconds` from `:timer.tc/1` because
9 | # `:erlang.convert_time_unit/3` takes floor of conversion, so we'd lose fractional milliseconds.
10 | @microseconds_per_millisecond :erlang.convert_time_unit(1, :millisecond, :microsecond)
11 |
12 | @doc """
13 | Measures time of `function` and logs
14 | """
15 | @spec debug_time(measured :: (-> result), message :: (milliseconds :: float -> String.t())) ::
16 | result
17 | when result: any
18 | def debug_time(measured, message) when is_function(measured, 0) and is_function(message, 1) do
19 | {microseconds, result} = :timer.tc(measured)
20 | _ = Logger.debug(fn -> message.(microseconds / @microseconds_per_millisecond) end)
21 |
22 | result
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/apps/events/test/events/gather_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Events.GatherTest do
2 | @moduledoc false
3 | use ExUnit.Case, async: true
4 | alias Events.Gather
5 |
6 | test "gather calls callback when all keys are present" do
7 | keys = [1, 2]
8 | state = Gather.new(keys, fn %{1 => :one, 2 => :two} -> send(self(), :ok) end)
9 | refute_receive :ok
10 | state = Gather.update(state, 1, :one)
11 | refute_receive :ok
12 | state = Gather.update(state, 2, :two)
13 | assert_receive :ok
14 |
15 | # does not re-call the callback
16 | Gather.update(state, 1, :one)
17 | refute_receive :ok
18 | end
19 |
20 | test "only remembers the last value sent" do
21 | keys = [1, 2]
22 | state = Gather.new(keys, fn %{1 => :one, 2 => :two} -> send(self(), :ok) end)
23 | state = Gather.update(state, 1, :other)
24 | state = Gather.update(state, 1, :one)
25 | Gather.update(state, 2, :two)
26 | assert_receive :ok
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/apps/parse/test/parse/line_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Parse.LineTest do
2 | use ExUnit.Case, async: true
3 |
4 | import Parse.Line
5 | alias Model.Line
6 |
7 | describe "parse_row/1" do
8 | test "parses a route CSV map into a %Line{}" do
9 | row = %{
10 | "line_id" => "line-Middleborough",
11 | "line_short_name" => "",
12 | "line_long_name" => "Middleborough/Lakeville Line",
13 | "line_desc" => "",
14 | "line_url" => "",
15 | "line_color" => "80276C",
16 | "line_text_color" => "FFFFFF",
17 | "line_sort_order" => "59"
18 | }
19 |
20 | expected = %Line{
21 | id: "line-Middleborough",
22 | short_name: "",
23 | long_name: "Middleborough/Lakeville Line",
24 | description: "",
25 | color: "80276C",
26 | text_color: "FFFFFF",
27 | sort_order: 59
28 | }
29 |
30 | assert parse_row(row) == expected
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/apps/parse/test/parse/alerts/calendar_dates_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Parse.CalendarDatesTest do
2 | use ExUnit.Case, async: true
3 |
4 | alias Parse.CalendarDates
5 | use Timex
6 |
7 | test "parses calendar dates" do
8 | blob = """
9 | "service_id","date","exception_type","holiday_name"
10 | "BUS12016-hba16011-Weekday-02","20160226",2,"Washington’s Birthday"
11 | "Boat-F4-Sunday","20150907",1,
12 | """
13 |
14 | assert Parse.CalendarDates.parse(blob) == [
15 | %CalendarDates{
16 | service_id: "BUS12016-hba16011-Weekday-02",
17 | date: ~D[2016-02-26],
18 | added: false,
19 | holiday_name: "Washington’s Birthday"
20 | },
21 | %CalendarDates{
22 | service_id: "Boat-F4-Sunday",
23 | date: ~D[2015-09-07],
24 | added: true,
25 | holiday_name: ""
26 | }
27 | ]
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/apps/fetch/lib/fetch.ex:
--------------------------------------------------------------------------------
1 | defmodule Fetch do
2 | @moduledoc """
3 | Fetches URLs from the internet
4 | """
5 |
6 | use DynamicSupervisor
7 |
8 | def start_link(opts \\ []) do
9 | DynamicSupervisor.start_link(__MODULE__, opts, name: __MODULE__)
10 | end
11 |
12 | def fetch_url(url, opts \\ []) do
13 | pid =
14 | case GenServer.whereis(Fetch.Worker.via_tuple(url)) do
15 | nil ->
16 | case start_child(url) do
17 | {:ok, pid} -> pid
18 | {:error, {:already_started, pid}} -> pid
19 | end
20 |
21 | pid ->
22 | pid
23 | end
24 |
25 | Fetch.Worker.fetch_url(pid, opts)
26 | end
27 |
28 | def start_child(url) do
29 | DynamicSupervisor.start_child(__MODULE__, {Fetch.Worker, url})
30 | end
31 |
32 | @impl DynamicSupervisor
33 | def init(opts) do
34 | DynamicSupervisor.init(
35 | strategy: :one_for_one,
36 | extra_arguments: [opts]
37 | )
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/apps/api_web/lib/api_web/templates/shared/login_form.html.heex:
--------------------------------------------------------------------------------
1 | Login
2 |
3 |
28 |
--------------------------------------------------------------------------------
/apps/state/lib/state/stop/subscriber.ex:
--------------------------------------------------------------------------------
1 | defmodule State.Stop.Subscriber do
2 | @moduledoc """
3 | Subscribes to `{:fetch, "stops.txt"}` events and uses it to reload state in `State.Stop`.
4 | """
5 | use Events.Server
6 |
7 | def start_link(_opts \\ []) do
8 | GenServer.start_link(__MODULE__, nil)
9 | end
10 |
11 | def stop(pid) do
12 | GenServer.stop(pid)
13 | end
14 |
15 | @impl Events.Server
16 | def handle_event({:fetch, "stops.txt"}, body, _, state) do
17 | :ok =
18 | body
19 | |> Parse.Stops.parse()
20 | |> State.Stop.new_state()
21 |
22 | {:noreply, state, :hibernate}
23 | end
24 |
25 | @impl GenServer
26 | def init(nil) do
27 | :ok = subscribe({:fetch, "stops.txt"})
28 |
29 | if State.Stop.size() > 0 do
30 | # if we crashed and restarted, ensure that all the workers also have
31 | # the correct state
32 | State.Stop.new_state(State.Stop.all())
33 | end
34 |
35 | {:ok, nil}
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/apps/model/lib/model/vehicle/carriage.ex:
--------------------------------------------------------------------------------
1 | defmodule Model.Vehicle.Carriage do
2 | @moduledoc """
3 | A carriage (segment) of a vehicle (for example, an individual car on a train), used for
4 | more detailed occupancy information.
5 | """
6 | use Recordable, [
7 | :label,
8 | :carriage_sequence,
9 | :occupancy_status,
10 | :occupancy_percentage
11 | ]
12 |
13 | @typedoc """
14 | Carriage-level crowding details
15 |
16 | * `:label` - Carriage-specific label, used as an identifier
17 | * `:carriage_sequence` - Provides a reliable order
18 | * `:occupancy_status` - The degree of passenger occupancy for the vehicle.
19 | * `:occupancy_percentage` - Percentage of vehicle occupied, calculated via weight average
20 | """
21 | @type t :: %__MODULE__{
22 | label: String.t() | nil,
23 | carriage_sequence: String.t() | nil,
24 | occupancy_status: String.t() | nil,
25 | occupancy_percentage: String.t() | nil
26 | }
27 | end
28 |
--------------------------------------------------------------------------------
/apps/parse/lib/parse/polyline.ex:
--------------------------------------------------------------------------------
1 | defmodule Parse.Polyline do
2 | @moduledoc """
3 | Parses the latitude/longitude pairs from shapes.txt into a
4 | [polyline](https://developers.google.com/maps/documentation/utilities/polylinealgorithm)
5 | """
6 | @behaviour Parse
7 |
8 | defstruct [:id, :polyline]
9 |
10 | def parse(blob) when is_binary(blob) do
11 | blob
12 | |> BinaryLineSplit.stream!()
13 | |> SimpleCSV.stream()
14 | |> Enum.group_by(& &1["shape_id"])
15 | |> Enum.map(&parse_shape(elem(&1, 1)))
16 | end
17 |
18 | defp parse_shape([%{"shape_id" => id} | _] = points) do
19 | %__MODULE__{
20 | id: id,
21 | polyline: polyline(points)
22 | }
23 | end
24 |
25 | defp polyline(points) do
26 | points
27 | |> Enum.map(fn %{"shape_pt_lat" => lat, "shape_pt_lon" => lon} ->
28 | {lon, ""} = Float.parse(lon)
29 | {lat, ""} = Float.parse(lat)
30 | {lon, lat}
31 | end)
32 | |> Polyline.encode()
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/apps/parse/lib/parse/facility.ex:
--------------------------------------------------------------------------------
1 | defmodule Parse.Facility do
2 | @moduledoc """
3 |
4 | Parser for elevators.csv
5 |
6 | """
7 | use Parse.Simple
8 |
9 | alias Model.Facility
10 |
11 | def parse_row(row) do
12 | %Facility{
13 | id: copy(row["facility_id"]),
14 | stop_id: copy(row["stop_id"]),
15 | long_name: optional_string(row["facility_long_name"]),
16 | short_name: optional_string(row["facility_short_name"]),
17 | type: type(row["facility_type"]),
18 | latitude: optional_latlng(row["facility_lat"]),
19 | longitude: optional_latlng(row["facility_lon"])
20 | }
21 | end
22 |
23 | defp optional_latlng("") do
24 | nil
25 | end
26 |
27 | defp optional_latlng(value) do
28 | String.to_float(value)
29 | end
30 |
31 | defp optional_string(""), do: nil
32 | defp optional_string(value), do: value
33 |
34 | defp type(string) do
35 | string
36 | |> String.upcase()
37 | |> String.replace("-", "_")
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/apps/api_web/lib/api_web/templates/client_portal/key/increase.html.heex:
--------------------------------------------------------------------------------
1 | Request Rate Limit Increase
2 |
3 |
Key Details
4 |
5 |
6 | User E-mail: {@user.email}
7 | Key: {@key.key}
8 |
9 |
10 |
11 |
12 |
13 | Can you tell us more about your app, and why you're looking for an increase?
14 |
15 |
16 |
17 | <%= form_for @conn, key_path(@conn, :do_request_increase, @key), [as: :reason, method: assigns[:method] || :post], fn f -> %>
18 |
19 | {label(f, :reason, class: "control-label")}
20 | {textarea(f, :reason, class: "form-control", required: "")}
21 |
22 |
23 |
24 | {submit("Request Increase", class: "btn btn-primary")}
25 |
26 | <% end %>
27 |
28 | {link("Back", to: portal_path(@conn, :index))}
29 |
--------------------------------------------------------------------------------
/apps/api_web/config/test.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | # We don't run a server during test. If one is required,
4 | # you can enable the server option below.
5 | config :api_web, ApiWeb.Endpoint,
6 | http: [port: 4001],
7 | server: false
8 |
9 | config :api_web, :rate_limiter,
10 | max_anon_per_interval: 5,
11 | clear_interval: 100
12 |
13 | config :api_web, RateLimiter.Memcache,
14 | connection_opts: [
15 | namespace: "api_test_rate_limit",
16 | hostname: "localhost"
17 | ]
18 |
19 | config :api_web, ApiWeb.Plugs.ModifiedSinceHandler, check_caller: true
20 |
21 | config :sentry,
22 | test_mode: true,
23 | before_send: {ApiWeb.SentryEventFilter, :filter_event}
24 |
25 | # Credentials that always show widget and pass backend validation:
26 | config :recaptcha,
27 | enabled: true,
28 | public_key: "6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI",
29 | secret: "6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe"
30 |
31 | # Print only warnings and errors during test
32 | config :logger, level: :warning
33 |
--------------------------------------------------------------------------------
/apps/parse/test/parse/timezone_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Parse.TimezoneTest do
2 | @moduledoc false
3 | use ExUnit.Case
4 | import Parse.Timezone
5 |
6 | doctest Parse.Timezone
7 |
8 | @two_hours 2 * 60 * 60
9 |
10 | describe "unix_to_local/2" do
11 | test "when springing forward, returns a time with the same Unix epoch" do
12 | # Sunday, March 11, 2018 1:00:00 AM GMT-05:00
13 | start = 1_520_748_000
14 |
15 | for time <- start..(start + @two_hours), rem(time, 300) == 0 do
16 | actual = unix_to_local(time)
17 | assert DateTime.to_unix(actual) == time
18 | end
19 | end
20 |
21 | test "when falling back, returns a time with the same Unix epoch" do
22 | # Sunday, November 4, 2018 1:00:00 AM GMT-04:00
23 | start = 1_541_307_600
24 |
25 | for time <- start..(start + @two_hours), rem(time, 300) == 0 do
26 | actual = unix_to_local(time)
27 | assert DateTime.to_unix(actual) == time
28 | end
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/apps/api_web/test/api_web/views/route_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.RouteViewTest do
2 | use ApiWeb.ConnCase, async: true
3 |
4 | alias Model.Route
5 |
6 | test "attributes/2 uses struct's type and expected attributes" do
7 | route = %Route{
8 | id: "red",
9 | type: 1,
10 | description: "desc",
11 | fare_class: "Ferry",
12 | short_name: "short",
13 | long_name: "long",
14 | sort_order: 1,
15 | color: "some color",
16 | text_color: "some text color"
17 | }
18 |
19 | expected = %{
20 | type: 1,
21 | description: "desc",
22 | fare_class: "Ferry",
23 | short_name: "short",
24 | long_name: "long",
25 | direction_names: [nil, nil],
26 | direction_destinations: [nil, nil],
27 | sort_order: 1,
28 | color: "some color",
29 | text_color: "some text color",
30 | listed_route: nil
31 | }
32 |
33 | assert ApiWeb.RouteView.attributes(route, %Plug.Conn{}) == expected
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/apps/events/lib/events/server.ex:
--------------------------------------------------------------------------------
1 | defmodule Events.Server do
2 | @moduledoc """
3 | Redirect `{:event, name, data, argument}` messages sent to a `GenServer` to a
4 | `handle_event(name, data, argument, state)` callback.
5 | """
6 |
7 | @callback handle_event(name :: term, data :: term, argument :: term, state :: term) ::
8 | {:noreply, new_state}
9 | | {:noreply, new_state, timeout | :hibernate}
10 | | {:stop, reason :: term, new_state}
11 | when new_state: term
12 |
13 | defmacro __using__(_) do
14 | quote location: :keep do
15 | use GenServer
16 | import Events
17 |
18 | @behaviour Events.Server
19 |
20 | @impl GenServer
21 | def handle_info({:event, name, data, argument}, state) do
22 | handle_event(name, data, argument, state)
23 | end
24 |
25 | def handle_info(_, state) do
26 | {:noreply, state}
27 | end
28 |
29 | defoverridable handle_info: 2
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/apps/api_web/lib/api_web/rate_limiter/memcache.ex:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.RateLimiter.Memcache do
2 | @moduledoc """
3 | RateLimiter backend which uses Memcache as a backend.
4 | """
5 | @behaviour ApiWeb.RateLimiter.Limiter
6 | alias ApiWeb.RateLimiter.Memcache.Supervisor
7 |
8 | @impl ApiWeb.RateLimiter.Limiter
9 | def start_link(opts) do
10 | clear_interval_ms = Keyword.fetch!(opts, :clear_interval)
11 | clear_interval = div(clear_interval_ms, 1000)
12 |
13 | connection_opts =
14 | [ttl: clear_interval * 2] ++ ApiWeb.config(RateLimiter.Memcache, :connection_opts)
15 |
16 | Supervisor.start_link(connection_opts)
17 | end
18 |
19 | @impl ApiWeb.RateLimiter.Limiter
20 | def rate_limited?(key, max_requests) do
21 | case Supervisor.decr(key, default: max_requests) do
22 | {:ok, 0} ->
23 | :rate_limited
24 |
25 | {:ok, n} when is_integer(n) ->
26 | {:remaining, n - 1}
27 |
28 | _ ->
29 | {:remaining, max_requests}
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/apps/state_mediator/test/state_mediator/mediator_log_test.exs:
--------------------------------------------------------------------------------
1 | defmodule StateMediator.MediatorLogTest do
2 | use ExUnit.Case
3 | import ExUnit.CaptureLog, only: [capture_log: 1]
4 | alias StateMediator.Mediator
5 |
6 | test "logs a fetch timeout as a warning" do
7 | assert capture_log(fn ->
8 | Mediator.handle_response(
9 | {:error,
10 | %HTTPoison.Error{
11 | id: nil,
12 | reason: :timeout
13 | }},
14 | %{module: nil, retries: 0}
15 | )
16 | end) =~ "[warning]"
17 | end
18 |
19 | test "logs an unknown error as an error" do
20 | assert capture_log(fn ->
21 | Mediator.handle_response(
22 | {:error,
23 | %HTTPoison.Error{
24 | id: nil,
25 | reason: :heat_death_of_universe
26 | }},
27 | %{module: nil, retries: 0}
28 | )
29 | end)
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/apps/api_web/lib/api_web/templates/client_portal/user/edit.html.heex:
--------------------------------------------------------------------------------
1 |
2 |
Account Information
3 | <%= form_for @changeset, user_path(@conn, :update), [method: :put], fn f -> %>
4 | <%= if @changeset.action do %>
5 |
6 |
Oops, something went wrong! Please check the errors below.
7 |
8 | <% end %>
9 |
10 |
11 | <.form_group form={f} field={:email}>
12 | {label(f, :email, class: "control-label")}
13 | {email_input(f, :email, class: "form-control")}
14 | {error_tag(f, :email)}
15 |
16 |
17 | <.form_group form={f} field={:phone}>
18 | {label(f, :phone, class: "control-label")}
19 | {text_input(f, :phone, class: "form-control")}
20 | {error_tag(f, :phone)}
21 |
22 |
23 |
24 | {submit("Update", class: "btn btn-primary")}
25 |
26 | <% end %>
27 |
28 |
--------------------------------------------------------------------------------
/apps/parse/test/parse/route_patterns_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Parse.RoutePatternsTest do
2 | @moduledoc false
3 | use ExUnit.Case, async: true
4 |
5 | describe "parse/1" do
6 | test "parses a CSV blob into a list of %RoutePattern{} structs" do
7 | blob = ~s(
8 | route_pattern_id,route_id,direction_id,route_pattern_name,route_pattern_time_desc,route_pattern_typicality,route_pattern_sort_order,representative_trip_id,canonical_route_pattern
9 | Red-1-0,Red,0,Ashmont,,1,10010051,38899721-21:00-KL,1
10 | )
11 |
12 | assert Parse.RoutePatterns.parse(blob) == [
13 | %Model.RoutePattern{
14 | id: "Red-1-0",
15 | route_id: "Red",
16 | direction_id: 0,
17 | name: "Ashmont",
18 | time_desc: nil,
19 | typicality: 1,
20 | sort_order: 10_010_051,
21 | representative_trip_id: "38899721-21:00-KL",
22 | canonical: true
23 | }
24 | ]
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/apps/parse/test/parse/calendar_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Parse.CalendarTest do
2 | use ExUnit.Case, async: true
3 | alias Parse.Calendar
4 | use Timex
5 |
6 | test "parses calendar entries" do
7 | blob = """
8 | "service_id","monday","tuesday","wednesday","thursday","friday","saturday","sunday","start_date","end_date"
9 | "BUS22016-hba26ns1-Weekday-02",1,1,1,1,1,0,0,"20160418","20160422"
10 | "BUS12016-hbb16hl6-Saturday-02",0,0,0,0,0,1,1,"20160215","20160215"
11 | """
12 |
13 | assert Parse.Calendar.parse(blob) == [
14 | %Calendar{
15 | service_id: "BUS22016-hba26ns1-Weekday-02",
16 | days: [1, 2, 3, 4, 5],
17 | start_date: ~D[2016-04-18],
18 | end_date: ~D[2016-04-22]
19 | },
20 | %Calendar{
21 | service_id: "BUS12016-hbb16hl6-Saturday-02",
22 | days: [6, 7],
23 | start_date: ~D[2016-02-15],
24 | end_date: ~D[2016-02-15]
25 | }
26 | ]
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/apps/model/test/recordable_test.exs:
--------------------------------------------------------------------------------
1 | defmodule RecordableTest do
2 | use ExUnit.Case, async: true
3 |
4 | defmodule Example do
5 | use Recordable, [:key, :val]
6 | end
7 |
8 | defmodule ExampleWithPairs do
9 | use Recordable, key: :key, val: :val
10 | end
11 |
12 | alias RecordableTest.{Example, ExampleWithPairs}
13 |
14 | test "to_record/1" do
15 | assert Example.to_record(%Example{key: :key, val: :val}) == {Example, :key, :val}
16 |
17 | assert ExampleWithPairs.to_record(%ExampleWithPairs{key: :other}) ==
18 | {ExampleWithPairs, :other, :val}
19 | end
20 |
21 | test "from_record/1" do
22 | assert Example.from_record({Example, :key, :val}) == %Example{key: :key, val: :val}
23 |
24 | assert ExampleWithPairs.from_record({ExampleWithPairs, :key, :val}) == %ExampleWithPairs{
25 | key: :key,
26 | val: :val
27 | }
28 | end
29 |
30 | test "fields/0" do
31 | assert Example.fields() == [:key, :val]
32 | assert ExampleWithPairs.fields() == [:key, :val]
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/apps/api_accounts/test/support/test_tables.ex:
--------------------------------------------------------------------------------
1 | defmodule ApiAccounts.Test.Model do
2 | @moduledoc false
3 | use ApiAccounts.Table
4 |
5 | table "model_table" do
6 | field(:email, :string, primary_key: true)
7 | field(:username, :string, secondary_index: true)
8 | field(:name, :string)
9 | field(:active, :boolean, default: true)
10 | field(:secret, :string, virtual: true)
11 | schema_version(1)
12 | end
13 | end
14 |
15 | defmodule ApiAccounts.Test.ModelWithoutSecondary do
16 | @moduledoc false
17 | use ApiAccounts.Table
18 |
19 | table "model_without_secondary_table" do
20 | field(:email, :string, primary_key: true)
21 | field(:name, :string)
22 | field(:secret, :string, virtual: true)
23 | schema_version(1)
24 | end
25 | end
26 |
27 | defmodule ApiAccounts.Test.MigrationModel do
28 | @moduledoc false
29 | use ApiAccounts.Table
30 |
31 | table "migration_test" do
32 | field(:id, :string, primary_key: true)
33 | field(:date, :datetime)
34 | field(:name, :string)
35 | schema_version(2)
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/apps/parse/test/parse/facility/property_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Parse.Facility.PropertyTest do
2 | use ExUnit.Case, async: true
3 | import Parse.Facility.Property
4 | alias Model.Facility.Property
5 |
6 | describe "parse/1" do
7 | test "parses a CSV blob into a list of %Facility.Property{} structs" do
8 | blob = ~s(
9 | "facility_id","property_id","value"
10 | "park-001","enclosed","2")
11 |
12 | assert parse(blob) == [
13 | %Property{
14 | name: "enclosed",
15 | facility_id: "park-001",
16 | value: 2
17 | }
18 | ]
19 | end
20 |
21 | test "doesn't decode non-numeric values" do
22 | blob = ~s(
23 | "facility_id","property_id","value"
24 | "park-001","operator","Republic Parking System")
25 |
26 | assert parse(blob) == [
27 | %Property{
28 | name: "operator",
29 | facility_id: "park-001",
30 | value: "Republic Parking System"
31 | }
32 | ]
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/apps/api_web/lib/api_web/rate_limiter/memcache/supervisor.ex:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.RateLimiter.Memcache.Supervisor do
2 | @moduledoc """
3 | Supervisor for multiple connections to a Memcache instance.
4 | """
5 | @worker_count 5
6 | @registry_name __MODULE__.Registry
7 |
8 | def start_link(connection_opts) do
9 | registry = {Registry, keys: :unique, name: @registry_name}
10 |
11 | workers =
12 | for i <- 1..@worker_count do
13 | Supervisor.child_spec({Memcache, [connection_opts, [name: worker_name(i)]]}, id: i)
14 | end
15 |
16 | children = [registry | workers]
17 |
18 | Supervisor.start_link(
19 | children,
20 | strategy: :rest_for_one,
21 | name: __MODULE__
22 | )
23 | end
24 |
25 | @doc "Decrement a given key, using a random child."
26 | def decr(key, opts) do
27 | Memcache.decr(random_child(), key, opts)
28 | end
29 |
30 | defp worker_name(index) do
31 | {:via, Registry, {@registry_name, index}}
32 | end
33 |
34 | defp random_child do
35 | worker_name(:rand.uniform(@worker_count))
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/apps/fetch/test/fetch/worker_log_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Fetch.WorkerLogTest do
2 | use ExUnit.Case
3 | import ExUnit.CaptureLog, only: [capture_log: 1]
4 |
5 | import Plug.Conn
6 |
7 | setup do
8 | lasso = Lasso.open()
9 | url = "http://localhost:#{lasso.port}"
10 | {:ok, pid} = Fetch.Worker.start_link(url)
11 | {:ok, %{lasso: lasso, pid: pid, url: url}}
12 | end
13 |
14 | test "logs a fetch timeout as a warning", %{lasso: lasso, pid: pid} do
15 | Lasso.expect(lasso, "GET", "/", fn conn ->
16 | Process.sleep(1000)
17 |
18 | conn
19 | |> resp(200, "body")
20 | end)
21 |
22 | assert capture_log(fn ->
23 | Fetch.Worker.fetch_url(pid, timeout: 100)
24 | end) =~ "[warning]"
25 | end
26 |
27 | test "logs an unknown error as an error", %{lasso: lasso, pid: pid} do
28 | Lasso.expect(lasso, "GET", "/", fn conn ->
29 | conn
30 | |> resp(500, "")
31 | end)
32 |
33 | assert capture_log(fn ->
34 | Fetch.Worker.fetch_url(pid, timeout: 100)
35 | end) =~ "[error]"
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/apps/state/test/state/stop/list_test.exs:
--------------------------------------------------------------------------------
1 | defmodule State.Stop.ListTest do
2 | @moduledoc false
3 | use ExUnit.Case, async: true
4 | use ExUnitProperties
5 | alias Model.Stop
6 | alias State.Stop.List, as: StopList
7 |
8 | test "around searches around a geo point" do
9 | stop = %Stop{id: "1", name: "stop", latitude: 1, longitude: -2}
10 | list = StopList.new([stop])
11 |
12 | assert StopList.around(list, 1.001, -2.002) == ["1"]
13 | assert StopList.around(list, -1.001, 2.002) == []
14 | assert StopList.around(list, 1.001, -2.002, 0.001) == []
15 | end
16 |
17 | property "does not crash when provided stops" do
18 | check all(stops <- list_of(stop())) do
19 | StopList.new(stops)
20 | end
21 | end
22 |
23 | defp stop do
24 | # generate stops, some of which don't have a location
25 | gen all(
26 | id <- string(:ascii),
27 | {latitude, longitude} <- one_of([tuple({float(), float()}), constant({nil, nil})])
28 | ) do
29 | %Stop{id: id, latitude: latitude, longitude: longitude}
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/apps/api_web/config/dev.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | # For development, we disable any cache and enable
4 | # debugging and code reloading.
5 | #
6 | # The watchers configuration can be used to run external
7 | # watchers to your application. For example, we use it
8 | # with brunch.io to recompile .js and .css sources.
9 | config :api_web, ApiWeb.Endpoint,
10 | http: [port: 4000],
11 | debug_errors: true,
12 | code_reloader: true,
13 | check_origin: false,
14 | live_reload: [
15 | patterns: [
16 | ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
17 | ~r{lib/api_web/views/.*(ex)$},
18 | ~r{lib/api_web/templates/.*(eex)$}
19 | ]
20 | ]
21 |
22 | config :api_web, ApiWeb.Plugs.ModifiedSinceHandler, check_caller: true
23 |
24 | # Do not include metadata nor timestamps in development logs
25 | config :logger, :console, format: "[$level] $message\n", level: :debug
26 |
27 | # Set a higher stacktrace during development.
28 | # Do not configure such in production as keeping
29 | # and calculating stacktraces is usually expensive.
30 | config :phoenix, :stacktrace_depth, 20
31 |
--------------------------------------------------------------------------------
/apps/api_web/lib/api_web/plugs/request_track.ex:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.Plugs.RequestTrack do
2 | @moduledoc """
3 | Track the number of concurrent requests made by a given API key or IP address.
4 | """
5 | @behaviour Plug
6 | import Plug.Conn, only: [register_before_send: 2]
7 |
8 | @impl Plug
9 | def init(opts) do
10 | Keyword.fetch!(opts, :name)
11 | end
12 |
13 | @impl Plug
14 | @doc """
15 | Track the API user, and decrement the count before sending.
16 |
17 | We increment the count when we're initially called, and set up a callback
18 | to decrement the count before the response is sent.
19 | """
20 | def call(conn, table_name) do
21 | key = conn.assigns.api_user
22 | RequestTrack.increment(table_name, key)
23 | _ = Logger.metadata(concurrent: RequestTrack.count(table_name, key))
24 |
25 | register_before_send(conn, fn conn ->
26 | # don't decrement if we're using chunked requests (streaming)
27 | if conn.state == :set do
28 | RequestTrack.decrement(table_name)
29 | end
30 |
31 | conn
32 | end)
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/apps/api_web/test/api_web/plugs/redirect_already_authenticated_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.Plugs.RedirectAlreadyAuthenticatedTest do
2 | use ApiWeb.ConnCase, async: true
3 |
4 | test "init" do
5 | assert ApiWeb.Plugs.RedirectAlreadyAuthenticated.init([]) == []
6 | end
7 |
8 | setup %{conn: conn} do
9 | conn =
10 | conn
11 | |> conn_with_session()
12 | |> bypass_through(ApiWeb.Router, [:browser])
13 |
14 | {:ok, %{conn: conn}}
15 | end
16 |
17 | test "redirects when user already authenticated", %{conn: conn} do
18 | conn =
19 | conn
20 | |> assign(:user, %ApiAccounts.User{})
21 | |> get("/")
22 | |> ApiWeb.Plugs.RedirectAlreadyAuthenticated.call([])
23 |
24 | assert redirected_to(conn) == portal_path(conn, :index)
25 | assert conn.halted
26 | end
27 |
28 | test "does not redirect when user isn't authenticated", %{conn: conn} do
29 | conn =
30 | conn
31 | |> get("/")
32 | |> ApiWeb.Plugs.RedirectAlreadyAuthenticated.call([])
33 |
34 | refute conn.status
35 | refute conn.halted
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/apps/parse/lib/parse/route_patterns.ex:
--------------------------------------------------------------------------------
1 | defmodule Parse.RoutePatterns do
2 | @moduledoc false
3 | use Parse.Simple
4 |
5 | @spec parse_row(%{optional(String.t()) => term()}) :: Model.RoutePattern.t()
6 | def parse_row(row) do
7 | %Model.RoutePattern{
8 | id: copy_string(row["route_pattern_id"]),
9 | route_id: copy_string(row["route_id"]),
10 | direction_id: copy_int(row["direction_id"]),
11 | name: copy_string(row["route_pattern_name"]),
12 | time_desc: copy_string(row["route_pattern_time_desc"]),
13 | typicality: copy_int(row["route_pattern_typicality"]),
14 | sort_order: copy_int(row["route_pattern_sort_order"]),
15 | representative_trip_id: copy_string(row["representative_trip_id"]),
16 | canonical: parse_canonical(row["canonical_route_pattern"])
17 | }
18 | end
19 |
20 | defp copy_string(""), do: nil
21 | defp copy_string(s), do: :binary.copy(s)
22 |
23 | defp copy_int(""), do: nil
24 | defp copy_int(s), do: String.to_integer(s)
25 |
26 | defp parse_canonical("1"), do: true
27 | defp parse_canonical(_), do: false
28 | end
29 |
--------------------------------------------------------------------------------
/apps/api_web/lib/api_web/controllers/mfa_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.MFAController do
2 | @moduledoc false
3 | use ApiWeb.Web, :controller
4 |
5 | def new(conn, _params) do
6 | user = conn |> get_session(:inc_user_id) |> ApiAccounts.get_user!()
7 | change = ApiAccounts.change_user(user)
8 |
9 | conn
10 | |> render("new.html", changeset: change)
11 | end
12 |
13 | def create(conn, params) do
14 | user = conn |> get_session(:inc_user_id) |> ApiAccounts.get_user!()
15 |
16 | case ApiAccounts.validate_totp(user, params["user"]["totp_code"]) do
17 | {:ok, user} ->
18 | destination = get_session(conn, :destination)
19 |
20 | conn
21 | |> delete_session(:inc_user_id)
22 | |> put_session(:user_id, user.id)
23 | |> configure_session(renew: true)
24 | |> delete_session(:destination)
25 | |> redirect(to: destination)
26 |
27 | {:error, changeset} ->
28 | conn
29 | |> put_flash(:error, "Invalid code. Please try again.")
30 | |> render("new.html", changeset: changeset)
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/apps/api_web/test/api_web/plugs/fetch_user_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.Plugs.FetchUserTest do
2 | use ApiWeb.ConnCase, async: false
3 |
4 | test "init" do
5 | opts = []
6 | assert ApiWeb.Plugs.FetchUser.init(opts) == opts
7 | end
8 |
9 | setup %{conn: conn} do
10 | ApiAccounts.Dynamo.create_table(ApiAccounts.User)
11 | on_exit(fn -> ApiAccounts.Dynamo.delete_all_tables() end)
12 | {:ok, user} = ApiAccounts.create_user(%{email: "test@example.com"})
13 |
14 | conn =
15 | conn
16 | |> conn_with_session()
17 | |> bypass_through(ApiWeb.Router, [:browser])
18 |
19 | {:ok, %{conn: conn, user: user}}
20 | end
21 |
22 | test "fetches user when id present in session", %{conn: conn, user: user} do
23 | conn =
24 | conn
25 | |> Plug.Conn.put_session(:user_id, user.id)
26 | |> ApiWeb.Plugs.FetchUser.call([])
27 |
28 | assert conn.assigns.user == user
29 | end
30 |
31 | test "doesn't assign when no user id in session", %{conn: conn} do
32 | conn = ApiWeb.Plugs.FetchUser.call(conn, [])
33 | refute conn.assigns[:user]
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/apps/api_web/lib/api_web/templates/client_portal/key/form.html.heex:
--------------------------------------------------------------------------------
1 | <%= form_for @changeset, @action, [method: assigns[:method] || :post], fn f -> %>
2 | <%= unless @changeset.valid? do %>
3 |
4 |
Oops, something went wrong! Please check the errors below.
5 |
6 | <% end %>
7 |
8 |
9 | {label(f, :description, class: "control-label")}
10 | {text_input(f, :description, class: "form-control")}
11 | {error_tag(f, :description)}
12 |
13 |
14 |
15 | {label(f, :api_version, class: "control-label")}
16 | {select(f, :api_version, @api_versions, class: "form-control")}
17 | {error_tag(f, :api_version)}
18 |
19 |
20 |
21 | {label(f, "Allowed domains (comma separated list of domains or *)", class: "control-label")}
22 | {text_input(f, :allowed_domains, class: "form-control")}
23 | {error_tag(f, :allowed_domains)}
24 |
25 |
26 |
27 | {submit("Submit", class: "btn btn-primary")}
28 |
29 | <% end %>
30 |
--------------------------------------------------------------------------------
/apps/health/lib/health/checkers/real_time.ex:
--------------------------------------------------------------------------------
1 | defmodule Health.Checkers.RealTime do
2 | @moduledoc """
3 | Health check which makes sure that real-time data (vehicle positions and
4 | predictions) aren't stale.
5 | """
6 | @stale_data_seconds 15 * 60
7 | @current_time_fetcher &DateTime.utc_now/0
8 |
9 | def current(current_time_fetcher \\ @current_time_fetcher) do
10 | updated_timestamps = State.Metadata.updated_timestamps()
11 | current_time = current_time_fetcher.()
12 |
13 | [
14 | prediction: updated_timestamps.prediction,
15 | prediction_diff: DateTime.diff(current_time, updated_timestamps.prediction),
16 | vehicle: updated_timestamps.vehicle,
17 | vehicle_diff: DateTime.diff(current_time, updated_timestamps.vehicle)
18 | ]
19 | end
20 |
21 | def healthy?(current_time_fetcher \\ @current_time_fetcher) do
22 | updated_timestamps = State.Metadata.updated_timestamps()
23 | current_time = current_time_fetcher.()
24 |
25 | Enum.all?([:prediction, :vehicle], fn feed ->
26 | DateTime.diff(current_time, updated_timestamps[feed]) < @stale_data_seconds
27 | end)
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/.github/workflows/deploy-dev-green.yml:
--------------------------------------------------------------------------------
1 | name: Deploy to Dev-green (ECS)
2 |
3 | on: workflow_dispatch
4 |
5 | jobs:
6 | deploy:
7 | name: Deploy
8 | runs-on: ubuntu-latest
9 | permissions:
10 | id-token: write
11 | contents: read
12 | environment: dev-green
13 | concurrency: dev-green
14 | env:
15 | ECS_CLUSTER: api
16 | ECS_SERVICE: api-dev-green
17 |
18 | steps:
19 | - uses: actions/checkout@v2
20 | - uses: mbta/actions/build-push-ecr@v2
21 | id: build-push
22 | with:
23 | role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
24 | docker-repo: ${{ secrets.DOCKER_REPO }}
25 | - uses: mbta/actions/deploy-ecs@v2
26 | with:
27 | role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
28 | ecs-cluster: ${{ env.ECS_CLUSTER }}
29 | ecs-service: ${{ env.ECS_SERVICE }}
30 | docker-tag: ${{ steps.build-push.outputs.docker-tag }}
31 | - uses: mbta/actions/notify-slack-deploy@v1
32 | if: ${{ !cancelled() }}
33 | with:
34 | webhook-url: ${{ secrets.SLACK_WEBHOOK }}
35 | job-status: ${{ job.status }}
36 |
--------------------------------------------------------------------------------
/.github/workflows/deploy-prod.yml:
--------------------------------------------------------------------------------
1 | name: Deploy to Prod (ECS)
2 |
3 | on: workflow_dispatch
4 |
5 | jobs:
6 | deploy:
7 | name: Deploy
8 | runs-on: ubuntu-latest
9 | permissions:
10 | id-token: write
11 | contents: read
12 | environment: prod
13 | concurrency: prod
14 | env:
15 | ECS_CLUSTER: api
16 | ECS_SERVICE: api-prod
17 |
18 | steps:
19 | - uses: actions/checkout@v2
20 | - uses: mbta/actions/build-push-ecr@v2
21 | id: build-push
22 | with:
23 | role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
24 | docker-repo: ${{ secrets.DOCKER_REPO }}
25 | - name: Deploy to ECS
26 | uses: mbta/actions/deploy-ecs@v2
27 | with:
28 | role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
29 | ecs-cluster: ${{ env.ECS_CLUSTER }}
30 | ecs-service: ${{ env.ECS_SERVICE }}
31 | docker-tag: ${{ steps.build-push.outputs.docker-tag }}
32 | - uses: mbta/actions/notify-slack-deploy@v1
33 | if: ${{ !cancelled() }}
34 | with:
35 | webhook-url: ${{ secrets.SLACK_WEBHOOK }}
36 | job-status: ${{ job.status }}
37 |
--------------------------------------------------------------------------------
/apps/state/test/state/stop/subscriber_test.exs:
--------------------------------------------------------------------------------
1 | defmodule State.Stop.SubscriberTest do
2 | @moduledoc false
3 | use ExUnit.Case
4 | import State.Stop.Subscriber
5 |
6 | describe "after a restart" do
7 | setup do
8 | # only put the stop into the cache, to simulate the worker not
9 | # receiving the information on startup
10 | State.Stop.Cache.new_state([
11 | %Model.Stop{latitude: 42.0, longitude: -71.0}
12 | ])
13 |
14 | :ok
15 | end
16 |
17 | test "ensures the workers have the right state" do
18 | Events.subscribe({:new_state, State.Stop})
19 |
20 | subscriber_pid =
21 | State.Stop
22 | |> Supervisor.which_children()
23 | |> Enum.find_value(fn {name, pid, _, _} ->
24 | if name == State.Stop.Subscriber, do: pid
25 | end)
26 |
27 | stop(subscriber_pid)
28 |
29 | # wait for the new_state event
30 | receive do
31 | {:event, _, _, _} -> :ok
32 | end
33 |
34 | # restarts subscriber, should pass state to the workers as well
35 | assert [_] = State.Stop.Worker.around(1, 42.0, -71.0)
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Massachusetts Bay Transportation Authority
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/apps/api_web/lib/api_web/templates/client_portal/user/edit_password.html.heex:
--------------------------------------------------------------------------------
1 |
2 |
Update Password
3 | <%= form_for @changeset, user_path(@conn, :update), [method: :put], fn f -> %>
4 | <%= if @changeset.action do %>
5 |
6 |
Oops, something went wrong! Please check the errors below.
7 |
8 | <% end %>
9 |
10 |
11 | <.form_group form={f} field={:password}>
12 | {label(f, :password, class: "control-label")}
13 | {password_input(f, :password, class: "form-control", autocomplete: "off")}
14 | {error_tag(f, :password)}
15 |
16 |
17 | <.form_group form={f} field={:password_confirmation}>
18 | {label(f, :password_confirmation, class: "control-label")}
19 | {password_input(f, :password_confirmation, class: "form-control", autocomplete: "off")}
20 | {error_tag(f, :password_confirmation)}
21 |
22 |
23 |
24 | {submit("Update", class: "btn btn-primary")}
25 |
26 | <% end %>
27 |
28 |
--------------------------------------------------------------------------------
/.github/workflows/deploy-dev.yml:
--------------------------------------------------------------------------------
1 | name: Deploy to Dev (ECS)
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | branches: [master]
7 |
8 | jobs:
9 | deploy:
10 | name: Deploy
11 | runs-on: ubuntu-latest
12 | permissions:
13 | id-token: write
14 | contents: read
15 | environment: dev
16 | concurrency: dev
17 | env:
18 | ECS_CLUSTER: api
19 | ECS_SERVICE: api-dev
20 |
21 | steps:
22 | - uses: actions/checkout@v2
23 | - uses: mbta/actions/build-push-ecr@v2
24 | id: build-push
25 | with:
26 | role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
27 | docker-repo: ${{ secrets.DOCKER_REPO }}
28 | - uses: mbta/actions/deploy-ecs@v2
29 | with:
30 | role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
31 | ecs-cluster: ${{ env.ECS_CLUSTER }}
32 | ecs-service: ${{ env.ECS_SERVICE }}
33 | docker-tag: ${{ steps.build-push.outputs.docker-tag }}
34 | - uses: mbta/actions/notify-slack-deploy@v1
35 | if: ${{ !cancelled() }}
36 | with:
37 | webhook-url: ${{ secrets.SLACK_WEBHOOK }}
38 | job-status: ${{ job.status }}
39 |
--------------------------------------------------------------------------------
/apps/api_web/lib/api_web/templates/client_portal/user/reset_password.html.heex:
--------------------------------------------------------------------------------
1 | Reset Password
2 |
3 |
4 | <%= form_for @changeset, user_path(@conn, :reset_password_submit, token: @token), [], fn f -> %>
5 | <%= if @changeset.action do %>
6 |
7 |
Oops, something went wrong! Please check the errors below.
8 |
9 | <% end %>
10 |
11 | <.form_group form={f} field={:password}>
12 | {label(f, :password, class: "control-label")}
13 | {password_input(f, :password, class: "form-control", autocomplete: "off")}
14 | {error_tag(f, :password)}
15 |
16 |
17 | <.form_group form={f} field={:password_confirmation}>
18 | {label(f, :password_confirmation, class: "control-label")}
19 | {password_input(f, :password_confirmation, class: "form-control", autocomplete: "off")}
20 | {error_tag(f, :password_confirmation)}
21 |
22 |
23 |
24 | {submit("Submit", class: "btn btn-primary")}
25 |
26 | <% end %>
27 |
28 |
--------------------------------------------------------------------------------
/apps/parse/test/parse/facility_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Parse.FacilityTest do
2 | use ExUnit.Case, async: true
3 | import Parse.Facility
4 | alias Model.Facility
5 |
6 | setup do
7 | blob = ~s(
8 | "facility_id","facility_code","facility_class","facility_type","stop_id","facility_short_name","facility_long_name","facility_desc","facility_lat","facility_lon","wheelchair_facility"
9 | "pick-qnctr-busway","","3","pick-drop","place-qnctr","Hancock Street","Quincy Center Hancock Street Pick-up/Drop-off","","42.251716","-71.004715","1"
10 | )
11 |
12 | {:ok, %{blob: blob}}
13 | end
14 |
15 | describe "parse/1" do
16 | test "parses a CSV blob into a list of %Facility{} structs", %{blob: blob} do
17 | assert parse(blob) == [
18 | %Facility{
19 | id: "pick-qnctr-busway",
20 | stop_id: "place-qnctr",
21 | type: "PICK_DROP",
22 | long_name: "Quincy Center Hancock Street Pick-up/Drop-off",
23 | short_name: "Hancock Street",
24 | latitude: 42.251716,
25 | longitude: -71.004715
26 | }
27 | ]
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/apps/api_web/lib/api_web/views/live_facility_view.ex:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.LiveFacilityView do
2 | @moduledoc """
3 | View for live Facility data like parking
4 | """
5 | use ApiWeb.Web, :api_view
6 |
7 | location(:live_facility_location)
8 |
9 | def live_facility_location(lf, conn), do: live_facility_path(conn, :show, lf.facility_id)
10 |
11 | has_one(
12 | :facility,
13 | type: :facility,
14 | serializer: ApiWeb.FacilityView
15 | )
16 |
17 | attributes([:properties, :updated_at])
18 |
19 | def type(_, %{assigns: %{api_version: ver}}) when ver < "2019-07-01", do: "live-facility"
20 | def type(_, _), do: "live_facility"
21 |
22 | def attributes(%{properties: properties, updated_at: updated_at}, _conn) do
23 | %{
24 | properties: Enum.map(properties, &property/1),
25 | updated_at: updated_at
26 | }
27 | end
28 |
29 | def id(%{facility_id: id}, _conn), do: id
30 |
31 | def facility(%{facility_id: id}, conn) do
32 | optional_relationship("facility", id, &State.Facility.by_id/1, conn)
33 | end
34 |
35 | defp property(property) do
36 | %{
37 | name: property.name,
38 | value: property.value
39 | }
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/apps/model/lib/geo_distance.ex:
--------------------------------------------------------------------------------
1 | defmodule GeoDistance do
2 | @moduledoc """
3 | Helper functions for working with geographic distances.
4 | """
5 | @degrees_to_radians 0.0174533
6 | @twice_earth_radius_miles 7918
7 |
8 | @doc "Returns the Haversine distance (in miles) between two latitude/longitude pairs"
9 | @spec distance(number, number, number, number) :: float
10 | def distance(latitude, longitude, latitude2, longitude2) do
11 | # Haversine distance
12 | a =
13 | 0.5 - :math.cos((latitude2 - latitude) * @degrees_to_radians) / 2 +
14 | :math.cos(latitude * @degrees_to_radians) * :math.cos(latitude2 * @degrees_to_radians) *
15 | (1 - :math.cos((longitude2 - longitude) * @degrees_to_radians)) / 2
16 |
17 | @twice_earth_radius_miles * :math.asin(:math.sqrt(a))
18 | end
19 |
20 | @doc "Returns a comparison function based on the distance between two points"
21 | @spec cmp(number, number) :: (%{latitude: number, longitude: number} -> float)
22 | def cmp(latitude, longitude) do
23 | fn %{latitude: latitude2, longitude: longitude2} ->
24 | GeoDistance.distance(latitude, longitude, latitude2, longitude2)
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/apps/api_accounts/lib/api_accounts/key.ex:
--------------------------------------------------------------------------------
1 | defmodule ApiAccounts.Key do
2 | @moduledoc """
3 | Representation of an API key belonging to a User.
4 | """
5 | use ApiAccounts.Table
6 | import ApiAccounts.Changeset
7 |
8 | @typedoc """
9 | Primary key for `ApiAccounts.Key.t`
10 | """
11 | @type key :: String.t()
12 |
13 | table "api_accounts_keys" do
14 | field(:key, :string, primary_key: true)
15 | field(:user_id, :string, secondary_index: true)
16 | field(:description, :string, default: nil)
17 | field(:created, :datetime)
18 | field(:requested_date, :datetime)
19 | field(:approved, :boolean, default: false)
20 | field(:locked, :boolean, default: false)
21 | field(:daily_limit, :integer)
22 | field(:rate_request_pending, :boolean, default: false)
23 | field(:api_version, :string)
24 | field(:allowed_domains, :string, default: "*")
25 | schema_version(3)
26 | end
27 |
28 | @doc false
29 | def changeset(struct, params \\ %{}) do
30 | fields = ~w(
31 | created requested_date approved locked daily_limit rate_request_pending api_version description allowed_domains
32 | )a
33 | cast(struct, params, fields)
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/apps/api_accounts/lib/api_accounts/migrations/migrations.ex:
--------------------------------------------------------------------------------
1 | defimpl ApiAccounts.Migrations.SchemaMigration, for: ApiAccounts.User do
2 | alias ApiAccounts.User
3 |
4 | def migrate(%User{join_date: nil} = user, 0, 1), do: user
5 |
6 | def migrate(%User{join_date: join_date} = user, 0, 1) do
7 | join_date = DateTime.from_naive!(join_date, "Etc/UTC")
8 | %User{user | join_date: join_date, schema_version: 1}
9 | end
10 | end
11 |
12 | defimpl ApiAccounts.Migrations.SchemaMigration, for: ApiAccounts.Key do
13 | alias ApiAccounts.Key
14 |
15 | def migrate(%Key{} = key, 0, 1) do
16 | created =
17 | if key.created do
18 | DateTime.from_naive!(key.created, "Etc/UTC")
19 | end
20 |
21 | requested_date =
22 | if key.requested_date do
23 | DateTime.from_naive!(key.requested_date, "Etc/UTC")
24 | end
25 |
26 | %Key{key | created: created, requested_date: requested_date, schema_version: 1}
27 | end
28 |
29 | def migrate(%Key{} = key, 1, 2) do
30 | %Key{key | rate_request_pending: false, schema_version: 2}
31 | end
32 |
33 | def migrate(%Key{} = key, 2, 3) do
34 | %Key{key | api_version: "2017-11-28", schema_version: 3}
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/apps/events/lib/events.ex:
--------------------------------------------------------------------------------
1 | defmodule Events do
2 | @moduledoc """
3 | A simple wrapper around Registry for event pub/sub.
4 |
5 | Example:
6 |
7 | iex> Events.subscribe(:name, :argument)
8 | :ok
9 | iex> Events.publish(:name, :data)
10 | :ok
11 | iex> receive do
12 | ...> x -> x
13 | ...> end
14 | {:event, :name, :data, :argument}
15 |
16 | """
17 |
18 | @type name :: any
19 |
20 | @doc """
21 | Subscribes the process to the given event. Whenever the event is triggered,
22 | the process will receive a message tuple:
23 |
24 | {:event, , , }
25 |
26 | """
27 | @spec subscribe(name, any) :: :ok
28 | def subscribe(name, argument \\ nil) do
29 | {:ok, _} = Registry.register(Events.Registry, name, argument)
30 | :ok
31 | end
32 |
33 | @doc """
34 | Publishes an event to any subscribers.
35 | """
36 | @spec publish(name, any) :: :ok
37 | def publish(name, data) do
38 | Registry.dispatch(Events.Registry, name, fn entries ->
39 | for {pid, argument} <- entries do
40 | send(pid, {:event, name, data, argument})
41 | end
42 | end)
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/apps/model/lib/model/facility.ex:
--------------------------------------------------------------------------------
1 | defmodule Model.Facility do
2 | @moduledoc """
3 | Station amenities such as elevators, escalators, parking lots and bike storage.
4 | """
5 |
6 | use Recordable, [
7 | :id,
8 | :stop_id,
9 | :type,
10 | :long_name,
11 | :short_name,
12 | :latitude,
13 | :longitude
14 | ]
15 |
16 | alias Model.WGS84
17 |
18 | @type id :: String.t()
19 |
20 | @typedoc """
21 | * `:id` - Unique ID
22 | * `:long_name` - Descriptive name of facility which can be used without any additional context.
23 | * `:short_name` - Short name of facility which might not include its station or type.
24 | * `:stop_id` - The `Model.Stop.id` of the station where facility is.
25 | * `:type` - What kind of amenity the facility is.
26 | * `:latitude` - Latitude of the facility
27 | * `:longitude` - Longitude of the facility
28 | """
29 | @type t :: %__MODULE__{
30 | id: id,
31 | long_name: String.t() | nil,
32 | short_name: String.t() | nil,
33 | stop_id: Model.Stop.id(),
34 | type: String.t(),
35 | latitude: WGS84.latitude() | nil,
36 | longitude: WGS84.longitude() | nil
37 | }
38 | end
39 |
--------------------------------------------------------------------------------
/apps/parse/lib/parse/line.ex:
--------------------------------------------------------------------------------
1 | defmodule Parse.Line do
2 | @moduledoc """
3 | Parses `lines.txt` CSV from GTFS zip
4 |
5 | line_id,line_short_name,line_long_name,line_desc,line_url,line_color,line_text_color,line_sort_order
6 | line-Red,,Red Line,,,DA291C,FFFFFF,1
7 | """
8 |
9 | use Parse.Simple
10 | alias Model.Line
11 |
12 | @doc """
13 | Parses (non-header) row of `lines.txt`
14 |
15 | ## Columns
16 |
17 | * `"line_id"` - `Model.Line.t` `id`
18 | * `"line_short_name"` - `Model.Line.t` `short_name`
19 | * `"line_long_name"` - `Model.Line.t` `long_name`
20 | * `"line_desc"` - `Model.Line.t` `description`
21 | * `"line_color"` - `Model.Line.t` `color`
22 | * `"line_text_color"` - `Model.Line.t` `text_color`
23 | * `"line_sort_order"` - `Model.Line.t` `sort_order`
24 |
25 | """
26 | def parse_row(row) do
27 | %Line{
28 | id: copy(row["line_id"]),
29 | short_name: copy(row["line_short_name"]),
30 | long_name: copy(row["line_long_name"]),
31 | description: copy(row["line_desc"]),
32 | color: copy(row["line_color"]),
33 | text_color: copy(row["line_text_color"]),
34 | sort_order: String.to_integer(row["line_sort_order"])
35 | }
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/.github/workflows/deploy-dev-blue.yml:
--------------------------------------------------------------------------------
1 | name: Deploy to Dev-blue (ECS)
2 |
3 | on:
4 | schedule:
5 | - cron: '0 5 * * *'
6 | workflow_dispatch:
7 |
8 | jobs:
9 | deploy:
10 | name: Deploy
11 | runs-on: ubuntu-latest
12 | permissions:
13 | id-token: write
14 | contents: read
15 | environment: dev-blue
16 | concurrency: dev-blue
17 | if: github.repository_owner == 'mbta'
18 | env:
19 | ECS_CLUSTER: api
20 | ECS_SERVICE: api-dev-blue
21 |
22 | steps:
23 | - uses: actions/checkout@v2
24 | - uses: mbta/actions/build-push-ecr@v2
25 | id: build-push
26 | with:
27 | role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
28 | docker-repo: ${{ secrets.DOCKER_REPO }}
29 | - uses: mbta/actions/deploy-ecs@v2
30 | with:
31 | role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
32 | ecs-cluster: ${{ env.ECS_CLUSTER }}
33 | ecs-service: ${{ env.ECS_SERVICE }}
34 | docker-tag: ${{ steps.build-push.outputs.docker-tag }}
35 | - uses: mbta/actions/notify-slack-deploy@v1
36 | if: ${{ !cancelled() }}
37 | with:
38 | webhook-url: ${{ secrets.SLACK_WEBHOOK }}
39 | job-status: ${{ job.status }}
40 |
--------------------------------------------------------------------------------
/apps/model/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 | import Config
4 |
5 | # This configuration is loaded before any dependency and is restricted
6 | # to this project. If another project depends on this project, this
7 | # file won't be loaded nor affect the parent project. For this reason,
8 | # if you want to provide default values for your application for
9 | # 3rd-party users, it should be done in your "mix.exs" file.
10 |
11 | # You can configure for your application as:
12 | #
13 | # config :model, key: :value
14 | #
15 | # And access this configuration in your application as:
16 | #
17 | # Application.get_env(:model, :key)
18 | #
19 | # Or configure a 3rd-party app:
20 | #
21 | # config :logger, level: :info
22 | #
23 |
24 | # It is also possible to import configuration files, relative to this
25 | # directory. For example, you can emulate configuration per environment
26 | # by uncommenting the line below and defining dev.exs, test.exs and such.
27 | # Configuration from the imported file will override the ones defined
28 | # here (which is why it is important to import them last).
29 | #
30 | # import_config "#{Mix.env}.exs"
31 |
--------------------------------------------------------------------------------
/apps/events/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 | import Config
4 |
5 | # This configuration is loaded before any dependency and is restricted
6 | # to this project. If another project depends on this project, this
7 | # file won't be loaded nor affect the parent project. For this reason,
8 | # if you want to provide default values for your application for
9 | # 3rd-party users, it should be done in your "mix.exs" file.
10 |
11 | # You can configure for your application as:
12 | #
13 | # config :events, key: :value
14 | #
15 | # And access this configuration in your application as:
16 | #
17 | # Application.get_env(:events, :key)
18 | #
19 | # Or configure a 3rd-party app:
20 | #
21 | # config :logger, level: :info
22 | #
23 |
24 | # It is also possible to import configuration files, relative to this
25 | # directory. For example, you can emulate configuration per environment
26 | # by uncommenting the line below and defining dev.exs, test.exs and such.
27 | # Configuration from the imported file will override the ones defined
28 | # here (which is why it is important to import them last).
29 | #
30 | # import_config "#{Mix.env}.exs"
31 |
--------------------------------------------------------------------------------
/apps/parse/test/parse/trip_updates_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Parse.TripUpdatesTest do
2 | @moduledoc false
3 | use ExUnit.Case, async: true
4 | import Parse.GtfsRt.TripUpdates
5 |
6 | describe "parse/1" do
7 | test "can parse an Enhanced JSON file" do
8 | trip = %{
9 | "trip_id" => "CR-Weekday-Spring-17-205",
10 | "start_date" => "2017-08-09",
11 | "schedule_relationship" => "SCHEDULED",
12 | "route_id" => "CR-Haverhill",
13 | "direction_id" => 0,
14 | "revenue" => true,
15 | "last_trip" => false
16 | }
17 |
18 | update = %{
19 | "stop_id" => "place-north",
20 | "stop_sequence" => 6,
21 | "arrival" => %{
22 | "time" => 1_502_290_000
23 | },
24 | "departure" => %{
25 | "time" => 1_502_290_500,
26 | "uncertainty" => 60
27 | }
28 | }
29 |
30 | body =
31 | Jason.encode!(%{
32 | entity: [
33 | %{
34 | trip_update: %{
35 | trip: trip,
36 | stop_time_update: [update]
37 | }
38 | }
39 | ]
40 | })
41 |
42 | assert [%Model.Prediction{}] = parse(body)
43 | end
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/apps/model/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule Model.Mixfile do
2 | use Mix.Project
3 |
4 | def project do
5 | [app: :model,
6 | aliases: aliases(),
7 | build_embedded: Mix.env == :prod,
8 | build_path: "../../_build",
9 | config_path: "../../config/config.exs",
10 | deps: deps(),
11 | deps_path: "../../deps",
12 | elixir: "~> 1.2",
13 | lockfile: "../../mix.lock",
14 | start_permanent: Mix.env == :prod,
15 | test_coverage: [tool: LcovEx],
16 | version: "0.0.1"]
17 | end
18 |
19 | # Configuration for the OTP application
20 | #
21 | # Type "mix help compile.app" for more information
22 | def application do
23 | [extra_applications: [:logger]]
24 | end
25 |
26 | defp aliases do
27 | [compile: ["compile --warnings-as-errors"]]
28 | end
29 |
30 | # Dependencies can be Hex packages:
31 | #
32 | # {:mydep, "~> 0.3.0"}
33 | #
34 | # Or git/path repositories:
35 | #
36 | # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
37 | #
38 | # To depend on another app inside the umbrella:
39 | #
40 | # {:myapp, in_umbrella: true}
41 | #
42 | # Type "mix help deps" for more examples and options
43 | defp deps do
44 | [{:timex, "~> 3.7"}]
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/apps/api_web/lib/api_web/plugs/deadline.ex:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.Plugs.Deadline do
2 | @moduledoc """
3 | Support for giving requests a time deadline, and allowing the request to end early if needed.
4 | """
5 | @behaviour Plug
6 | import Plug.Conn
7 |
8 | @impl Plug
9 | def init(_) do
10 | []
11 | end
12 |
13 | @impl Plug
14 | def call(conn, _) do
15 | put_private(conn, :api_web_deadline_start, current_time())
16 | end
17 |
18 | @spec set(Plug.Conn.t(), integer) :: Plug.Conn.t()
19 | def set(%{private: %{api_web_deadline_start: start_time}} = conn, budget) do
20 | deadline = start_time + budget
21 | put_private(conn, :api_web_deadline, deadline)
22 | end
23 |
24 | @spec check!(Plug.Conn.t()) :: :ok | no_return
25 | def check!(%Plug.Conn{private: %{api_web_deadline: deadline}} = conn) do
26 | if current_time() > deadline do
27 | raise __MODULE__.Error, conn: conn
28 | else
29 | :ok
30 | end
31 | end
32 |
33 | def check!(%Plug.Conn{}) do
34 | # no deadline set
35 | :ok
36 | end
37 |
38 | defp current_time do
39 | System.monotonic_time(:millisecond)
40 | end
41 |
42 | defmodule Error do
43 | defexception plug_status: 503, message: "deadline exceeded", conn: nil
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/apps/state_mediator/lib/state_mediator/mqtt_mediator/handler.ex:
--------------------------------------------------------------------------------
1 | defmodule StateMediator.MqttMediator.Handler do
2 | @moduledoc """
3 | EmqttFailover.ConnectionHandler implementation which sends the data to the provided state module.
4 | """
5 | require Logger
6 |
7 | use EmqttFailover.ConnectionHandler
8 |
9 | @enforce_keys [
10 | :module,
11 | :topic,
12 | :timeout
13 | ]
14 | defstruct @enforce_keys
15 |
16 | @impl EmqttFailover.ConnectionHandler
17 | def init(opts) do
18 | state = struct!(__MODULE__, opts)
19 | {:ok, state}
20 | end
21 |
22 | @impl EmqttFailover.ConnectionHandler
23 | def handle_connected(state) do
24 | Logger.info("StateMediator.MqttMediator subscribed topic=#{state.topic}")
25 | {:ok, [state.topic], state}
26 | end
27 |
28 | @impl EmqttFailover.ConnectionHandler
29 | def handle_message(message, state) do
30 | debug_time("#{state.module} new state", fn ->
31 | state.module.new_state(message.payload, state.timeout)
32 | end)
33 |
34 | {:ok, state}
35 | end
36 |
37 | defp debug_time(description, func) do
38 | State.Logger.debug_time(func, fn milliseconds ->
39 | "StateMediator.MqttMediator #{description} took #{milliseconds}ms"
40 | end)
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: mix
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | open-pull-requests-limit: 10
8 | - package-ecosystem: mix
9 | directory: "/apps/api_accounts"
10 | schedule:
11 | interval: daily
12 | open-pull-requests-limit: 10
13 | - package-ecosystem: mix
14 | directory: "/apps/api_web"
15 | schedule:
16 | interval: daily
17 | open-pull-requests-limit: 10
18 | - package-ecosystem: mix
19 | directory: "/apps/events"
20 | schedule:
21 | interval: daily
22 | open-pull-requests-limit: 10
23 | - package-ecosystem: mix
24 | directory: "/apps/fetch"
25 | schedule:
26 | interval: daily
27 | open-pull-requests-limit: 10
28 | - package-ecosystem: mix
29 | directory: "/apps/health"
30 | schedule:
31 | interval: daily
32 | open-pull-requests-limit: 10
33 | - package-ecosystem: mix
34 | directory: "/apps/model"
35 | schedule:
36 | interval: daily
37 | open-pull-requests-limit: 10
38 | - package-ecosystem: mix
39 | directory: "/apps/state"
40 | schedule:
41 | interval: daily
42 | open-pull-requests-limit: 10
43 | - package-ecosystem: mix
44 | directory: "/apps/state_mediator"
45 | schedule:
46 | interval: daily
47 | open-pull-requests-limit: 10
48 |
--------------------------------------------------------------------------------
/apps/api_web/test/api_web/views/vehicle_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.VehicleViewTest do
2 | use ApiWeb.ConnCase
3 |
4 | # Bring render/3 and render_to_string/3 for testing custom views
5 | import Phoenix.View
6 |
7 | alias Model.Vehicle
8 |
9 | @vehicle %Vehicle{
10 | id: "vehicle",
11 | revenue: :REVENUE
12 | }
13 |
14 | setup %{conn: conn} do
15 | conn = Phoenix.Controller.put_view(conn, ApiWeb.VehicleView)
16 | {:ok, %{conn: conn}}
17 | end
18 |
19 | test "render returns JSONAPI", %{conn: conn} do
20 | rendered = render(ApiWeb.VehicleView, "index.json-api", data: @vehicle, conn: conn)
21 | assert rendered["data"]["type"] == "vehicle"
22 | assert rendered["data"]["id"] == "vehicle"
23 |
24 | assert rendered["data"]["attributes"] == %{
25 | "direction_id" => nil,
26 | "revenue" => "REVENUE",
27 | "bearing" => nil,
28 | "carriages" => [],
29 | "current_status" => nil,
30 | "current_stop_sequence" => nil,
31 | "label" => nil,
32 | "latitude" => nil,
33 | "longitude" => nil,
34 | "occupancy_status" => nil,
35 | "speed" => nil,
36 | "updated_at" => nil
37 | }
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/apps/api_web/lib/api_web/controller_helpers.ex:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.ControllerHelpers do
2 | @moduledoc """
3 | Simple helpers for multiple controllers.
4 | """
5 | alias ApiWeb.LegacyStops
6 |
7 | import Plug.Conn
8 |
9 | @doc "Grab the ID from a struct/map"
10 | @spec id(%{id: any}) :: any
11 | def id(%{id: value}), do: value
12 |
13 | @doc """
14 | Returns the current service date for the connection. If one isn't present, we look it up and store it.
15 | """
16 | def conn_service_date(conn) do
17 | case conn.private do
18 | %{api_web_service_date: date} ->
19 | {conn, date}
20 |
21 | _ ->
22 | date = Parse.Time.service_date()
23 | conn = put_private(conn, :api_web_service_date, date)
24 | {conn, date}
25 | end
26 | end
27 |
28 | @doc """
29 | Given a map containing a set of filters, one of which expresses a list of stop IDs, expands the
30 | list using `LegacyStops` with the given API version.
31 | """
32 | @spec expand_stops_filter(map, any, String.t()) :: map
33 | def expand_stops_filter(filters, stops_key, api_version) do
34 | case Map.has_key?(filters, stops_key) do
35 | true -> Map.update!(filters, stops_key, &LegacyStops.expand(&1, api_version))
36 | false -> filters
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/apps/api_web/test/api_web/sentry_event_filter_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.SentryEventFilterTest do
2 | use ApiWeb.ConnCase, async: true
3 |
4 | describe "filter_event/1" do
5 | setup do
6 | Sentry.Test.start_collecting_sentry_reports()
7 | end
8 |
9 | test "filters out `NoRouteError`s rescued by plugs", %{conn: conn} do
10 | conn = get(conn, "/a_nonexistent_route")
11 | assert response(conn, 404)
12 |
13 | assert [] = Sentry.Test.pop_sentry_reports()
14 | end
15 |
16 | test "filters out `NoRouteError`s surfaced as messages via crashes" do
17 | Sentry.capture_message("Something something {{{%Phoenix.Router.NoRouteError}}}")
18 | assert [] = Sentry.Test.pop_sentry_reports()
19 |
20 | Sentry.capture_message("Something something {{{%SomeOtherError}}}")
21 | assert [%Sentry.Event{} = err] = Sentry.Test.pop_sentry_reports()
22 | assert err.message.formatted =~ "SomeOtherError"
23 | end
24 |
25 | test "does not filter out other exceptions" do
26 | err = RuntimeError.exception("An error other than NoRouteError")
27 |
28 | Sentry.capture_exception(err)
29 |
30 | assert [%Sentry.Event{} = event] = Sentry.Test.pop_sentry_reports()
31 | assert ^err = event.original_exception
32 | end
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/apps/events/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule Events.Mixfile do
2 | use Mix.Project
3 |
4 | def project do
5 | [app: :events,
6 | aliases: aliases(),
7 | build_embedded: Mix.env == :prod,
8 | build_path: "../../_build",
9 | config_path: "../../config/config.exs",
10 | deps: deps(),
11 | deps_path: "../../deps",
12 | elixir: "~> 1.2",
13 | lockfile: "../../mix.lock",
14 | start_permanent: Mix.env == :prod,
15 | test_coverage: [tool: LcovEx],
16 | version: "0.0.1"]
17 | end
18 |
19 | # Configuration for the OTP application
20 | #
21 | # Type "mix help compile.app" for more information
22 | def application do
23 | [
24 | mod: {Events.Application, []},
25 | extra_applications: [:logger]
26 | ]
27 | end
28 |
29 | defp aliases do
30 | [compile: ["compile --warnings-as-errors"]]
31 | end
32 |
33 | # Dependencies can be Hex packages:
34 | #
35 | # {:mydep, "~> 0.3.0"}
36 | #
37 | # Or git/path repositories:
38 | #
39 | # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
40 | #
41 | # To depend on another app inside the umbrella:
42 | #
43 | # {:myapp, in_umbrella: true}
44 | #
45 | # Type "mix help deps" for more examples and options
46 | defp deps do
47 | []
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/apps/parse/lib/parse/simple.ex:
--------------------------------------------------------------------------------
1 | defmodule Parse.Simple do
2 | @moduledoc """
3 | Simple CSV parser that only needs to define `parse_row(row)`.
4 |
5 | defmodule Parse.Thing do
6 | use Parse.Simple
7 |
8 | def parse_row(row) do
9 | ...
10 | end
11 | end
12 |
13 | """
14 |
15 | alias NimbleCSV.RFC4180, as: CSV
16 |
17 | @callback parse_row(%{String.t() => String.t()}) :: any
18 |
19 | defmacro __using__([]) do
20 | quote location: :keep do
21 | import :binary, only: [copy: 1]
22 | @behaviour Parse
23 | @behaviour unquote(__MODULE__)
24 |
25 | @spec parse(String.t()) :: [any]
26 | def parse(blob) do
27 | unquote(__MODULE__).parse(blob, &parse_row/1)
28 | end
29 | end
30 | end
31 |
32 | @spec parse(String.t(), (map -> any)) :: [map]
33 | def parse(blob, row_callback)
34 |
35 | def parse("", _) do
36 | []
37 | end
38 |
39 | def parse(blob, row_callback) do
40 | blob
41 | |> String.trim()
42 | |> CSV.parse_string(skip_headers: false)
43 | |> Stream.transform(nil, fn
44 | headers, nil -> {[], headers}
45 | row, headers -> {[headers |> Enum.zip(row) |> Map.new()], headers}
46 | end)
47 | |> Enum.to_list()
48 | |> Enum.map(row_callback)
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/apps/api_web/lib/api_web/plugs/rate_limiter.ex:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.Plugs.RateLimiter do
2 | @moduledoc """
3 | Rate limits a user based on their API key or by their IP address if no
4 | API key is provided.
5 | """
6 |
7 | import Plug.Conn
8 | import Phoenix.Controller, only: [render: 3, put_view: 2]
9 |
10 | def init(opts), do: opts
11 |
12 | def call(%{assigns: assigns, request_path: request_path} = conn, _) do
13 | case ApiWeb.RateLimiter.log_request(assigns.api_user, request_path) do
14 | :ok ->
15 | conn
16 |
17 | {:ok, {limit, remaining, reset_ms}} ->
18 | conn
19 | |> put_rate_limit_headers(limit, remaining, reset_ms)
20 |
21 | {:rate_limited, {limit, remaining, reset_ms}} ->
22 | conn
23 | |> put_rate_limit_headers(limit, remaining, reset_ms)
24 | |> put_status(429)
25 | |> put_view(ApiWeb.ErrorView)
26 | |> render("429.json-api", [])
27 | |> halt()
28 | end
29 | end
30 |
31 | defp put_rate_limit_headers(conn, limit, remaining, reset_ms) do
32 | reset_seconds = div(reset_ms, 1_000)
33 |
34 | conn
35 | |> put_resp_header("x-ratelimit-limit", "#{limit}")
36 | |> put_resp_header("x-ratelimit-remaining", "#{remaining}")
37 | |> put_resp_header("x-ratelimit-reset", "#{reset_seconds}")
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/apps/api_web/test/api_web/plugs/require_admin_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.Plugs.RequireAdminTest do
2 | use ApiWeb.ConnCase, async: true
3 |
4 | setup %{conn: conn} do
5 | conn =
6 | conn
7 | |> conn_with_session()
8 | |> bypass_through(ApiWeb.Router, [:browser, :admin])
9 |
10 | {:ok, conn: conn}
11 | end
12 |
13 | test "opts" do
14 | assert ApiWeb.Plugs.RequireAdmin.init([]) == []
15 | end
16 |
17 | describe ":require_admin plug" do
18 | test "gives 404 with no authenicated user", %{conn: conn} do
19 | conn = get(conn, "/")
20 | assert conn.status == 404
21 | assert html_response(conn, 404) =~ "not found"
22 | end
23 |
24 | test "gives 404 for user without administrator role", %{conn: conn} do
25 | conn =
26 | conn
27 | |> user_with_role(nil)
28 | |> get("/")
29 |
30 | assert html_response(conn, 404) =~ "not found"
31 | end
32 |
33 | test "allows user with administrator role to proceed", %{conn: conn} do
34 | conn =
35 | conn
36 | |> user_with_role("administrator")
37 | |> get("/")
38 |
39 | refute conn.status
40 | end
41 | end
42 |
43 | defp user_with_role(conn, role) do
44 | Plug.Conn.assign(conn, :user, %ApiAccounts.User{role: role, totp_enabled: true})
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/apps/health/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule Health.Mixfile do
2 | use Mix.Project
3 |
4 | def project do
5 | [app: :health,
6 | aliases: aliases(),
7 | build_embedded: Mix.env == :prod,
8 | build_path: "../../_build",
9 | config_path: "../../config/config.exs",
10 | deps: deps(),
11 | deps_path: "../../deps",
12 | elixir: "~> 1.3",
13 | lockfile: "../../mix.lock",
14 | start_permanent: Mix.env == :prod,
15 | test_coverage: [tool: LcovEx],
16 | version: "0.1.0"]
17 | end
18 |
19 | # Configuration for the OTP application
20 | #
21 | # Type "mix help compile.app" for more information
22 | def application do
23 | [extra_applications: [:logger],
24 | mod: {Health, []}]
25 | end
26 |
27 | defp aliases do
28 | [compile: ["compile --warnings-as-errors"]]
29 | end
30 |
31 | # Dependencies can be Hex packages:
32 | #
33 | # {:mydep, "~> 0.3.0"}
34 | #
35 | # Or git/path repositories:
36 | #
37 | # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
38 | #
39 | # To depend on another app inside the umbrella:
40 | #
41 | # {:myapp, in_umbrella: true}
42 | #
43 | # Type "mix help deps" for more examples and options
44 | defp deps do
45 | [{:events, in_umbrella: true},
46 | {:state, in_umbrella: true}]
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | ARG ELIXIR_VERSION=1.17.3
2 | ARG ERLANG_VERSION=27.2
3 | ARG ALPINE_VERSION=3.21.0
4 |
5 | FROM hexpm/elixir:${ELIXIR_VERSION}-erlang-${ERLANG_VERSION}-alpine-${ALPINE_VERSION} as builder
6 |
7 | WORKDIR /root
8 |
9 | # Install Hex+Rebar
10 | RUN mix local.hex --force && \
11 | mix local.rebar --force
12 |
13 | RUN apk add --update git make build-base erlang-dev
14 |
15 | ENV MIX_ENV=prod
16 |
17 | ADD apps apps
18 | ADD config config
19 | ADD mix.* /root/
20 |
21 | RUN mix do deps.get --only prod, phx.swagger.generate, compile, phx.digest, sentry.package_source_code
22 | RUN mix eval "Application.ensure_all_started(:tzdata); Tzdata.DataBuilder.load_and_save_table()"
23 |
24 | ADD rel/ rel/
25 |
26 | RUN mix release
27 |
28 | # The one the elixir image was built with
29 | FROM alpine:${ALPINE_VERSION}
30 |
31 | RUN apk add --no-cache libssl3 dumb-init libstdc++ libgcc ncurses-libs && \
32 | mkdir /work /api && \
33 | adduser -D api && chown api /work
34 |
35 | COPY --from=builder /root/_build/prod/rel/api_web /api
36 |
37 | # Set exposed ports
38 | EXPOSE 4000
39 | ENV PORT=4000 MIX_ENV=prod TERM=xterm LANG=C.UTF-8 \
40 | ERL_CRASH_DUMP_SECONDS=0 RELEASE_TMP=/work
41 |
42 | USER api
43 | WORKDIR /work
44 |
45 | ENTRYPOINT ["/usr/bin/dumb-init", "--"]
46 |
47 | HEALTHCHECK CMD ["/api/bin/api_web", "rpc", "1 + 1"]
48 | CMD ["/api/bin/api_web", "start"]
49 |
--------------------------------------------------------------------------------
/apps/api_web/lib/api_web/endpoint.ex:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.Endpoint do
2 | use Sentry.PlugCapture
3 | use Phoenix.Endpoint, otp_app: :api_web
4 |
5 | # You should set gzip to true if you are running phoenix.digest
6 | # when deploying your static files in production.
7 |
8 | # Code reloading can be explicitly enabled under the
9 | # :code_reloader configuration of your endpoint.
10 | if code_reloading? do
11 | plug(Phoenix.CodeReloader)
12 | end
13 |
14 | plug(Plug.Static, at: "/", from: :api_web, gzip: false, only: ~w(js css images favicon.ico))
15 |
16 | plug(Plug.RequestId)
17 | plug(Logster.Plugs.Logger)
18 |
19 | plug(
20 | Plug.Parsers,
21 | parsers: [:urlencoded],
22 | pass: ["application/json", "application/vnd.api+json", "application/x-www-form-urlencoded"]
23 | )
24 |
25 | # Sentry must be invoked after Plug.Parsers:
26 | plug(Sentry.PlugContext)
27 |
28 | plug(Plug.MethodOverride)
29 | plug(Plug.Head)
30 | plug(ApiWeb.Plugs.ClearMetadata, ~w(api_key ip api_version concurrent records)a)
31 | plug(ApiWeb.Plugs.Deadline)
32 | plug(ApiWeb.Plugs.ExperimentalFeatures)
33 | # CORS needs to be before the router, and Authenticate needs to be before CORS
34 | plug(ApiWeb.Plugs.Authenticate)
35 | plug(ApiWeb.Plugs.CORS)
36 | plug(ApiWeb.Plugs.RequestTrack, name: ApiWeb.RequestTrack)
37 | plug(ApiWeb.Router)
38 | end
39 |
--------------------------------------------------------------------------------
/apps/state/bench/schedule_bench.exs:
--------------------------------------------------------------------------------
1 | defmodule State.ScheduleBench do
2 | use Benchfella
3 |
4 | @schedule %Model.Schedule{
5 | trip_id: "trip",
6 | stop_id: "stop",
7 | position: :first}
8 |
9 | def setup_all do
10 | Application.ensure_all_started(:events)
11 | State.Schedule.start_link
12 | @schedule
13 | |> expand_schedule(30_000)
14 | |> State.Schedule.new_state
15 |
16 | {:ok, pid}
17 | end
18 |
19 | def teardown_all(pid) do
20 | State.Schedule.stop
21 | end
22 |
23 | defp expand_schedule(schedule, count) do
24 | [schedule|do_expand_schedule(schedule, count)]
25 | end
26 | defp do_expand_schedule(_, 0), do: []
27 | defp do_expand_schedule(%{trip_id: trip_id} = schedule, count) do
28 | new_schedule = case rem(count, 3) do
29 | 0 -> %{schedule | trip_id: "#{trip_id}_#{count}"}
30 | 1 -> schedule
31 | 2 -> %{schedule | stop_id: "#{trip_id}_#{count}"}
32 | end
33 | [new_schedule|do_expand_schedule(schedule, count - 1)]
34 | end
35 |
36 | bench "by_trip_id" do
37 | "trip"
38 | |> State.Schedule.by_trip_id
39 | |> Enum.filter(&(&1.position == :first))
40 | end
41 |
42 | bench "match" do
43 | #[@schedule] =
44 | State.Schedule.match(%{trip_id: "trip", position: :first}, :trip_id)
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/apps/parse/lib/parse/calendar.ex:
--------------------------------------------------------------------------------
1 | defmodule Parse.Calendar do
2 | @moduledoc """
3 |
4 | Parse a calendar.txt
5 | (https://developers.google.com/transit/gtfs/reference#calendartxt) file
6 | into a simple struct.
7 |
8 | """
9 | @behaviour Parse
10 | defstruct [:service_id, :days, :start_date, :end_date]
11 | use Timex
12 |
13 | def parse(blob) do
14 | blob
15 | |> BinaryLineSplit.stream!()
16 | |> SimpleCSV.decode()
17 | |> Enum.map(&parse_row/1)
18 | end
19 |
20 | defp parse_row(row) do
21 | %__MODULE__{
22 | service_id: :binary.copy(row["service_id"]),
23 | start_date: parse_date(row["start_date"]),
24 | end_date: parse_date(row["end_date"]),
25 | days: parse_days(row)
26 | }
27 | end
28 |
29 | defp parse_date(date) do
30 | date
31 | |> Timex.parse!("{YYYY}{0M}{0D}")
32 | |> NaiveDateTime.to_date()
33 | end
34 |
35 | defp parse_days(row) do
36 | []
37 | |> add_day(row["sunday"], 7)
38 | |> add_day(row["saturday"], 6)
39 | |> add_day(row["friday"], 5)
40 | |> add_day(row["thursday"], 4)
41 | |> add_day(row["wednesday"], 3)
42 | |> add_day(row["tuesday"], 2)
43 | |> add_day(row["monday"], 1)
44 | end
45 |
46 | defp add_day(result, "1", value) do
47 | [value | result]
48 | end
49 |
50 | defp add_day(result, _, _) do
51 | result
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/apps/parse/test/parse/polyline_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Parse.PolylineTest do
2 | use ExUnit.Case, async: true
3 | import Parse.Polyline
4 |
5 | @blob """
6 | "shape_id","shape_pt_lat","shape_pt_lon","shape_pt_sequence","shape_dist_traveled"
7 | "cf00001",41.660225,-70.276583,1,""
8 | "cf00001",41.683585,-70.258956,2,""
9 | "cf00001",41.692367,-70.256982,3,""
10 | "cf00001",41.699288,-70.259557,4,""
11 | "cf00001",41.700506,-70.262682,5,""
12 | "cf00001",41.700554,-70.265043,6,""
13 | "cf00001",41.696837,-70.279333,7,""
14 | "cf00001",41.697878,-70.300469,8,""
15 | "cf00001",41.701739,-70.319245,9,""
16 | "cf00001",41.700282,-70.343428,10,""
17 | "cf00001",41.701403,-70.354543,11,""
18 | "cf00001",41.702508,-70.360386,12,""
19 | "cf00001",41.708372,-70.377788,13,""
20 | "cf00001",41.731347,-70.433868,14,""
21 | "cf00001",41.744468,-70.455551,15,""
22 | "cf00001",41.746093,-70.464638,16,""
23 | "cf00001",41.751929,-70.477781,17,""
24 | "cf00001",41.75517,-70.48748,18,""
25 | """
26 |
27 | describe "parse/1" do
28 | test "test returns a list of shapes" do
29 | parsed = parse(@blob)
30 |
31 | assert [
32 | %Parse.Polyline{id: "cf00001", polyline: polyline}
33 | ] = parsed
34 |
35 | # slight loss of precision
36 | assert [{-70.27658, 41.66022} | _] = Polyline.decode(polyline)
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/apps/parse/test/parse/routes_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Parse.RoutesTest do
2 | use ExUnit.Case, async: true
3 |
4 | import Parse.Routes
5 | alias Model.Route
6 |
7 | describe "parse_row/1" do
8 | test "parses a route CSV map into a %Route{}" do
9 | row = %{
10 | "route_id" => "CapeFlyer",
11 | "agency_id" => "3",
12 | "route_short_name" => "",
13 | "route_long_name" => "CapeFLYER",
14 | "route_desc" => "",
15 | "route_fare_class" => "Rapid Transit",
16 | "route_type" => "2",
17 | "route_url" => "http://capeflyer.com/",
18 | "route_color" => "006595",
19 | "route_text_color" => "FFFFFF",
20 | "route_sort_order" => "100",
21 | "line_id" => "line-Orange",
22 | "listed_route" => "1"
23 | }
24 |
25 | expected = %Route{
26 | id: "CapeFlyer",
27 | agency_id: "3",
28 | short_name: "",
29 | long_name: "CapeFLYER",
30 | description: "",
31 | fare_class: "Rapid Transit",
32 | type: 2,
33 | color: "006595",
34 | text_color: "FFFFFF",
35 | sort_order: 100,
36 | line_id: "line-Orange",
37 | listed_route: false,
38 | direction_destinations: [nil, nil],
39 | direction_names: [nil, nil]
40 | }
41 |
42 | assert parse_row(row) == expected
43 | end
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/apps/api_web/test/api_web/controllers/status_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.StatusControllerTest do
2 | use ApiWeb.ConnCase
3 |
4 | test "returns service metadata", %{conn: conn} do
5 | State.Feed.new_state(%Model.Feed{
6 | version: "TEST",
7 | start_date: ~D[2019-01-01],
8 | end_date: ~D[2019-02-01]
9 | })
10 |
11 | conn = get(conn, status_path(conn, :index))
12 | assert json = json_response(conn, 200)
13 | assert_attribute_key(json, "feed")
14 | assert_feed_key(json, "version")
15 | assert_feed_key(json, "start_date")
16 | assert_feed_key(json, "end_date")
17 | assert_attribute_key(json, "alert")
18 | assert_attribute_key(json, "facility")
19 | assert_attribute_key(json, "prediction")
20 | assert_attribute_key(json, "route")
21 | assert_attribute_key(json, "route_pattern")
22 | assert_attribute_key(json, "schedule")
23 | assert_attribute_key(json, "service")
24 | assert_attribute_key(json, "shape")
25 | assert_attribute_key(json, "stop")
26 | assert_attribute_key(json, "trip")
27 | assert_attribute_key(json, "vehicle")
28 | end
29 |
30 | def assert_attribute_key(json, attribute_key) do
31 | assert get_in(json, ["data", "attributes", attribute_key])
32 | end
33 |
34 | def assert_feed_key(json, feed_key) do
35 | assert get_in(json, ["data", "attributes", "feed", feed_key])
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/apps/api_web/test/api_web/views/error_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.ErrorViewTest do
2 | use ApiWeb.ConnCase, async: true
3 |
4 | # Bring render/3 and render_to_string/3 for testing custom views
5 | import Phoenix.View
6 |
7 | test "render 400 (invalid request)" do
8 | rendered = render(ApiWeb.ErrorView, "400.json", %{error: :invalid})
9 | only_route_type = render(ApiWeb.ErrorView, "400.json", %{error: :only_route_type})
10 | distance_params = render(ApiWeb.ErrorView, "400.json", %{error: :distance_params})
11 | assert [%{code: :bad_request}] = rendered["errors"]
12 | assert [%{code: :bad_request}] = only_route_type["errors"]
13 | assert [%{code: :bad_request}] = distance_params["errors"]
14 | end
15 |
16 | test "renders 404.json" do
17 | rendered = render(ApiWeb.ErrorView, "404.json", [])
18 | assert [%{code: :not_found}] = rendered["errors"]
19 | end
20 |
21 | test "render 406.json" do
22 | rendered = render(ApiWeb.ErrorView, "406.json", [])
23 | assert [%{code: :not_acceptable}] = rendered["errors"]
24 | end
25 |
26 | test "render 500.json" do
27 | rendered = render(ApiWeb.ErrorView, "500.json", [])
28 | assert [%{code: :internal_error}] = rendered["errors"]
29 | end
30 |
31 | test "render any other" do
32 | rendered = render(ApiWeb.ErrorView, "505.json", [])
33 | assert [%{code: :internal_error}] = rendered["errors"]
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/apps/api_accounts/lib/decoders.ex:
--------------------------------------------------------------------------------
1 | defimpl ExAws.Dynamo.Decodable, for: ApiAccounts.User do
2 | def decode(%ApiAccounts.User{join_date: nil} = user), do: user
3 |
4 | def decode(%ApiAccounts.User{join_date: join_date, totp_since: totp_since} = user) do
5 | join_date = Decoders.datetime(join_date)
6 | totp_since = Decoders.datetime(totp_since)
7 | totp_binary = if user.totp_secret, do: Base.decode32!(user.totp_secret)
8 |
9 | %ApiAccounts.User{
10 | user
11 | | join_date: join_date,
12 | totp_since: totp_since,
13 | totp_secret_bin: totp_binary
14 | }
15 | end
16 | end
17 |
18 | defimpl ExAws.Dynamo.Decodable, for: ApiAccounts.Key do
19 | def decode(%ApiAccounts.Key{} = key) do
20 | created =
21 | if key.created do
22 | Decoders.datetime(key.created)
23 | end
24 |
25 | requested_date =
26 | if key.requested_date do
27 | Decoders.datetime(key.requested_date)
28 | end
29 |
30 | %ApiAccounts.Key{key | created: created, requested_date: requested_date}
31 | end
32 | end
33 |
34 | defmodule Decoders do
35 | @moduledoc false
36 | def datetime(nil), do: nil
37 |
38 | def datetime(datetime_string) do
39 | if String.ends_with?(datetime_string, "Z") do
40 | {:ok, datetime, _} = DateTime.from_iso8601(datetime_string)
41 | datetime
42 | else
43 | NaiveDateTime.from_iso8601!(datetime_string)
44 | end
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/apps/model/lib/recordable.ex:
--------------------------------------------------------------------------------
1 | defmodule Recordable do
2 | @moduledoc """
3 | Converts a `struct` to a record that can be stores in ETS and mnesia and back out again.
4 | """
5 |
6 | defmacro recordable(opts) do
7 | keys_or_kvs = Macro.expand(opts, __CALLER__)
8 |
9 | keys =
10 | for key_or_kv <- keys_or_kvs do
11 | case key_or_kv do
12 | {key, _} -> key
13 | key -> key
14 | end
15 | end
16 |
17 | vals = Enum.map(keys, &{&1, [], nil})
18 | pairs = Enum.zip(keys, vals)
19 |
20 | # create a pairs object with each value being the filler variable
21 | fill_pairs = Enum.zip(keys, Stream.cycle([:_]))
22 |
23 | quote do
24 | require Record
25 |
26 | defstruct unquote(keys_or_kvs)
27 | Record.defrecord(__MODULE__, [unquote_splicing(keys)])
28 |
29 | def to_record(%__MODULE__{unquote_splicing(pairs)}) do
30 | {__MODULE__, unquote_splicing(vals)}
31 | end
32 |
33 | def from_record({__MODULE__, unquote_splicing(vals)}) do
34 | %__MODULE__{unquote_splicing(pairs)}
35 | end
36 |
37 | def fields, do: unquote(keys)
38 |
39 | def filled(_) do
40 | %__MODULE__{unquote_splicing(fill_pairs)}
41 | end
42 | end
43 | end
44 |
45 | defmacro __using__(keys) do
46 | quote do
47 | require unquote(__MODULE__)
48 | unquote(__MODULE__).recordable(unquote(keys))
49 | end
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/apps/state/test/state/alert/active_period_test.exs:
--------------------------------------------------------------------------------
1 | defmodule State.Alert.ActivePeriodTest do
2 | @moduledoc false
3 | use ExUnit.Case, async: true
4 | import State.Alert.ActivePeriod
5 | alias Model.Alert
6 |
7 | @table __MODULE__
8 |
9 | @alerts [
10 | %Alert{
11 | id: "1"
12 | # no active period, matches everything
13 | },
14 | %Alert{
15 | id: "2",
16 | active_period: [
17 | {nil, DateTime.from_unix!(1000)},
18 | {DateTime.from_unix!(2000), DateTime.from_unix!(3000)},
19 | {DateTime.from_unix!(4000), nil}
20 | ]
21 | }
22 | ]
23 |
24 | setup do
25 | new(@table)
26 | update(@table, @alerts)
27 | :ok
28 | end
29 |
30 | describe "update/2" do
31 | test "ignores empty updates" do
32 | update(@table, [])
33 | assert size(@table) == 4
34 | end
35 | end
36 |
37 | describe "filter/3" do
38 | test "filters a list to just those that match the given ID" do
39 | for {unix, expected_ids} <- [
40 | {0, ~w(1 2)},
41 | {1000, ~w(1)},
42 | {2500, ~w(1 2)},
43 | {3500, ~w(1)},
44 | {5000, ~w(1 2)}
45 | ] do
46 | dt = DateTime.from_unix!(unix)
47 |
48 | actual_ids =
49 | @table
50 | |> filter(~w(1 2 3), dt)
51 | |> Enum.sort()
52 |
53 | assert actual_ids == expected_ids
54 | end
55 | end
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/apps/api_web/lib/api_web/views/status_view.ex:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.StatusView do
2 | use ApiWeb.Web, :api_view
3 |
4 | attributes([
5 | :feed,
6 | :alert,
7 | :facility,
8 | :prediction,
9 | :route,
10 | :route_pattern,
11 | :schedule,
12 | :service,
13 | :shape,
14 | :stop,
15 | :trip,
16 | :vehicle
17 | ])
18 |
19 | def feed(data, _), do: data.feed
20 |
21 | def alert(data, _) do
22 | %{last_updated: data.timestamps.alert}
23 | end
24 |
25 | def facility(data, _) do
26 | %{last_updated: data.timestamps.facility}
27 | end
28 |
29 | def prediction(data, _) do
30 | %{last_updated: data.timestamps.prediction}
31 | end
32 |
33 | def route(data, _) do
34 | %{last_updated: data.timestamps.route}
35 | end
36 |
37 | def route_pattern(data, _) do
38 | %{last_updated: data.timestamps.route_pattern}
39 | end
40 |
41 | def schedule(data, _) do
42 | %{last_updated: data.timestamps.schedule}
43 | end
44 |
45 | def service(data, _) do
46 | %{last_updated: data.timestamps.service}
47 | end
48 |
49 | def shape(data, _) do
50 | %{last_updated: data.timestamps.shape}
51 | end
52 |
53 | def stop(data, _) do
54 | %{last_updated: data.timestamps.stop}
55 | end
56 |
57 | def trip(data, _) do
58 | %{last_updated: data.timestamps.trip}
59 | end
60 |
61 | def vehicle(data, _) do
62 | %{last_updated: data.timestamps.vehicle}
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/apps/api_web/test/api_web/controllers/mfa_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.MfaControllerTest do
2 | @moduledoc false
3 | use ApiWeb.ConnCase, async: false
4 |
5 | alias ApiWeb.Fixtures
6 |
7 | setup %{conn: conn} do
8 | ApiAccounts.Dynamo.create_table(ApiAccounts.User)
9 | on_exit(fn -> ApiAccounts.Dynamo.delete_all_tables() end)
10 | {:ok, conn: conn}
11 | end
12 |
13 | test "2fa redirects user on success", %{conn: conn} do
14 | user = Fixtures.fixture(:totp_user)
15 |
16 | conn =
17 | conn
18 | |> conn_with_session()
19 | |> put_session(:inc_user_id, user.id)
20 | |> put_session(:destination, portal_path(conn, :index))
21 |
22 | conn =
23 | post(
24 | form_header(conn),
25 | mfa_path(conn, :create),
26 | user: %{totp_code: NimbleTOTP.verification_code(user.totp_secret_bin)}
27 | )
28 |
29 | assert redirected_to(conn) == portal_path(conn, :index)
30 | end
31 |
32 | test "2fa does not accept invalid codes", %{conn: conn} do
33 | user = Fixtures.fixture(:totp_user)
34 |
35 | conn = conn |> conn_with_session() |> put_session(:inc_user_id, user.id)
36 |
37 | conn =
38 | post(
39 | form_header(conn),
40 | mfa_path(conn, :create),
41 | user: %{totp_code: "1234"}
42 | )
43 |
44 | assert html_response(conn, 200) =~ "TOTP"
45 | assert Phoenix.Flash.get(conn.assigns.flash, :error) != nil
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/apps/health/lib/health/checkers/state.ex:
--------------------------------------------------------------------------------
1 | defmodule Health.Checkers.State do
2 | @moduledoc """
3 | Health check which makes sure various State modules have data.
4 | """
5 | use Events.Server
6 |
7 | @apps [
8 | State.Schedule,
9 | State.Alert,
10 | State.ServiceByDate,
11 | State.StopsOnRoute,
12 | State.RoutesPatternsAtStop,
13 | State.Shape
14 | ]
15 |
16 | def start_link(_opts \\ []) do
17 | GenServer.start_link(__MODULE__, nil, name: __MODULE__)
18 | end
19 |
20 | def current do
21 | GenServer.call(__MODULE__, :current)
22 | end
23 |
24 | def healthy? do
25 | current()
26 | |> Enum.all?(fn {_, count} ->
27 | count > 0
28 | end)
29 | end
30 |
31 | @impl Events.Server
32 | def handle_event({:new_state, app}, count, _, state) do
33 | new_state =
34 | state
35 | |> Keyword.put(app_name(app), count)
36 |
37 | {:noreply, new_state}
38 | end
39 |
40 | @impl GenServer
41 | def init(nil) do
42 | statuses =
43 | for app <- @apps do
44 | subscribe({:new_state, app})
45 | {app_name(app), app.size()}
46 | end
47 |
48 | {:ok, statuses}
49 | end
50 |
51 | @impl GenServer
52 | def handle_call(:current, _from, state) do
53 | {:reply, state, state}
54 | end
55 |
56 | defp app_name(app) do
57 | app
58 | |> Module.split()
59 | |> List.last()
60 | |> String.downcase()
61 | |> String.to_atom()
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/apps/api_web/lib/api_web/templates/admin/layout/navigation.html.heex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
19 |
20 |
21 | <%= if user = @conn.assigns[:user] do %>
22 |
23 | {link("Users", to: admin_user_path(@conn, :index))}
24 | {link("Keys", to: admin_key_path(@conn, :index))}
25 |
26 |
27 |
28 | {link("Logout",
29 | to: admin_session_path(@conn, :delete),
30 | method: :delete,
31 | style: "display: inline-block; padding: 15px"
32 | )}
33 |
34 |
35 |
{user.email}
36 | <% end %>
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/apps/api_web/test/api_web/controllers/portal/portal_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.Controllers.Portal.PortalControllerTest do
2 | use ApiWeb.ConnCase
3 |
4 | describe "Test portal with keys" do
5 | setup :setup_key_requesting_user
6 |
7 | test "index loads", %{conn: conn} do
8 | conn = get(conn, portal_path(conn, :index))
9 | assert html_response(conn, 200) =~ "Api Keys"
10 | end
11 |
12 | test "index displays default key limit per minute", %{
13 | user: user,
14 | conn: conn
15 | } do
16 | {:ok, key} = ApiAccounts.create_key(user)
17 | {:ok, _} = ApiAccounts.update_key(key, %{approved: true})
18 | conn = get(conn, portal_path(conn, :index))
19 | max = ApiWeb.config(:rate_limiter, :max_registered_per_interval)
20 | interval = ApiWeb.config(:rate_limiter, :clear_interval)
21 | per_interval_minute = div(max, interval) * 60_000
22 | assert html_response(conn, 200) =~ "#{per_interval_minute}"
23 | end
24 |
25 | test "index displays dynamic key limit", %{user: user, conn: conn} do
26 | {:ok, key} = ApiAccounts.create_key(user)
27 | {:ok, _} = ApiAccounts.update_key(key, %{approved: true, daily_limit: 999_999_999_999})
28 | conn = get(conn, portal_path(conn, :index))
29 | assert html_response(conn, 200) =~ "694444200"
30 | end
31 | end
32 |
33 | test "landing", %{conn: conn} do
34 | conn = get(conn, portal_path(conn, :landing))
35 | assert html_response(conn, 200) =~ "MBTA V3 API Portal "
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/apps/fetch/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule Fetch.Mixfile do
2 | use Mix.Project
3 |
4 | def project do
5 | [
6 | app: :fetch,
7 | aliases: aliases(),
8 | build_embedded: Mix.env() == :prod,
9 | build_path: "../../_build",
10 | config_path: "../../config/config.exs",
11 | deps: deps(),
12 | deps_path: "../../deps",
13 | elixir: "~> 1.2",
14 | lockfile: "../../mix.lock",
15 | start_permanent: Mix.env() == :prod,
16 | test_coverage: [tool: LcovEx],
17 | version: "0.0.1"
18 | ]
19 | end
20 |
21 | # Configuration for the OTP application
22 | #
23 | # Type "mix help compile.app" for more information
24 | def application do
25 | [
26 | mod: {Fetch.App, []}
27 | ]
28 | end
29 |
30 | defp aliases do
31 | [compile: ["compile --warnings-as-errors"]]
32 | end
33 |
34 | # Dependencies can be Hex packages:
35 | #
36 | # {:mydep, "~> 0.3.0"}
37 | #
38 | # Or git/path repositories:
39 | #
40 | # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
41 | #
42 | # To depend on another app inside the umbrella:
43 | #
44 | # {:myapp, in_umbrella: true}
45 | #
46 | # Type "mix help deps" for more examples and options
47 | defp deps do
48 | [
49 | {:httpoison, "~> 2.0"},
50 | {:events, in_umbrella: true},
51 | {:model, in_umbrella: true},
52 | {:timex, "~> 3.7"},
53 | {:ex_aws, "~> 2.4"},
54 | {:ex_aws_s3, "~> 2.4"},
55 | {:lasso, "~> 0.1.1-pre", only: :test}
56 | ]
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/apps/parse/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule Parse.Mixfile do
2 | use Mix.Project
3 |
4 | def project do
5 | [
6 | app: :parse,
7 | aliases: aliases(),
8 | build_embedded: Mix.env() == :prod,
9 | build_path: "../../_build",
10 | config_path: "../../config/config.exs",
11 | deps: deps(),
12 | deps_path: "../../deps",
13 | elixir: "~> 1.2",
14 | lockfile: "../../mix.lock",
15 | start_permanent: Mix.env() == :prod,
16 | test_coverage: [tool: LcovEx],
17 | version: "0.0.1"
18 | ]
19 | end
20 |
21 | # Configuration for the OTP application
22 | #
23 | # Type "mix help compile.app" for more information
24 | def application do
25 | [
26 | extra_applications: [:logger]
27 | ]
28 | end
29 |
30 | defp aliases do
31 | [compile: ["compile --warnings-as-errors"]]
32 | end
33 |
34 | # Dependencies can be Hex packages:
35 | #
36 | # {:mydep, "~> 0.3.0"}
37 | #
38 | # Or git/path repositories:
39 | #
40 | # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
41 | #
42 | # To depend on another app inside the umbrella:
43 | #
44 | # {:myapp, in_umbrella: true}
45 | #
46 | # Type "mix help deps" for more examples and options
47 | defp deps do
48 | [
49 | {:nimble_csv, "~> 1.2"},
50 | {:timex, "~> 3.7"},
51 | {:jason, "~> 1.4"},
52 | {:model, in_umbrella: true},
53 | {:polyline, "~> 1.3"},
54 | {:fast_local_datetime, "~> 1.0"},
55 | {:nimble_parsec, "~> 1.2"}
56 | ]
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/apps/api_web/lib/api_web/plugs/check_for_shutdown.ex:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.Plugs.CheckForShutdown do
2 | @moduledoc """
3 | Tells all requests to close with the "Connection: close" header when the system is shutting down.
4 | """
5 |
6 | import Plug.Conn
7 |
8 | @behaviour Plug
9 |
10 | @impl Plug
11 | def init(_opts) do
12 | {:ok, true}
13 | end
14 |
15 | @impl Plug
16 | def call(conn, _) do
17 | if running?() do
18 | conn
19 | else
20 | put_resp_header(conn, "connection", "close")
21 | end
22 | end
23 |
24 | @compile inline: [running?: 0]
25 |
26 | @spec running?() :: boolean
27 | @doc """
28 | Return a boolean indicating whether the system is still running.
29 | """
30 | def running? do
31 | :persistent_term.get(__MODULE__, true)
32 | end
33 |
34 | @spec started() :: :ok
35 | @doc """
36 | Mark the system as started.
37 |
38 | Not required, but improves the performance in the "is-running" case.
39 |
40 | We can't do this in `init/1`, because that might happen at compile-time instead of runtime.
41 | """
42 | def started do
43 | :persistent_term.put(__MODULE__, true)
44 |
45 | :ok
46 | end
47 |
48 | @spec shutdown() :: :ok
49 | @doc """
50 | Mark the system as shutting down, so that all connections are closed.
51 | """
52 | def shutdown do
53 | :persistent_term.put(__MODULE__, false)
54 |
55 | :ok
56 | end
57 |
58 | # test-only function for re-setting the persistent_term state
59 | @doc false
60 | def reset do
61 | :persistent_term.erase(__MODULE__)
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/apps/api_web/test/api_web/canary_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ApiWeb.CanaryTest do
2 | use ExUnit.Case, async: true
3 |
4 | alias ApiWeb.Canary
5 |
6 | test "calls the provided function when terminated with reason :shutdown" do
7 | test_pid = self()
8 | {:ok, canary} = GenServer.start(Canary, fn -> send(test_pid, :notified) end)
9 | refute_receive :notified
10 |
11 | GenServer.stop(canary, :shutdown)
12 | assert_receive :notified
13 | end
14 |
15 | test "calls the provided function when terminated with reason :normal" do
16 | test_pid = self()
17 | {:ok, canary} = GenServer.start(Canary, fn -> send(test_pid, :notified) end)
18 | refute_receive :notified
19 |
20 | GenServer.stop(canary, :shutdown)
21 | assert_receive :notified
22 | end
23 |
24 | @tag :capture_log
25 | test "does nothing when terminated with a reason other than :shutdown or :normal" do
26 | test_pid = self()
27 | {:ok, canary} = GenServer.start(Canary, fn -> send(test_pid, :notified) end)
28 |
29 | GenServer.stop(canary, :unexpected)
30 | refute_receive :notified
31 | end
32 |
33 | test "default notify function is set if function is not provided" do
34 | Process.flag(:trap_exit, true)
35 | pid = spawn_link(Canary, :start_link, [])
36 |
37 | assert_receive {:EXIT, ^pid, :normal}
38 | end
39 |
40 | test "stops with reason if given a value that is not a function for notify_fn" do
41 | Process.flag(:trap_exit, true)
42 |
43 | assert {:error, "expect function/0 for notify_fn, got nil"} =
44 | Canary.start_link(notify_fn: nil)
45 | end
46 | end
47 |
--------------------------------------------------------------------------------