Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci_cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
# Specify the OTP and Elixir versions to use when building
# and running the workflow steps.
matrix:
otp: [ '27.2.1' ] # Define the OTP version [required]
otp: [ '27.3.4.13' ] # Define the OTP version [required]
elixir: [ '1.18.2-otp-27' ] # Define the elixir version [required]
steps:
# Step: Check out the code.
Expand Down Expand Up @@ -86,7 +86,7 @@ jobs:
# Specify the OTP and Elixir versions to use when building
# and running the workflow steps.
matrix:
otp: [ '27.2.1' ] # Define the OTP version [required]
otp: [ '27.3.4.13' ] # Define the OTP version [required]
elixir: [ '1.18.2-otp-27' ] # Define the elixir version [required]
steps:
# Step: Check out the code.
Expand Down
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
elixir 1.18.2-otp-27
erlang 27.2.1
erlang 27.3.4.13
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
# - Ex: hexpm/elixir:1.14.3-erlang-24.3.4.10-debian-bullseye-20230227-slim
#
ARG ELIXIR_VERSION=1.18.2
ARG OTP_VERSION=27.2.1
ARG DEBIAN_VERSION=bookworm-20250113-slim
ARG OTP_VERSION=27.3.4.13
ARG DEBIAN_VERSION=bookworm-20260518-slim

ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}"
ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}"
Expand Down
21 changes: 18 additions & 3 deletions config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,24 @@ if System.get_env("PHX_SERVER") do
config :pears, PearsWeb.Endpoint, server: true
end

config :pears, slack_client_id: System.fetch_env!("SLACK_CLIENT_ID")
config :pears, slack_client_secret: System.fetch_env!("SLACK_CLIENT_SECRET")
config :pears, slack_signing_secret: System.fetch_env!("SLACK_SIGNING_SECRET")
# Slack credentials are required in production (fail fast if missing), but in
# dev/test we fall back to placeholders so the app can boot without the secrets
# being present (e.g. in CI). Tests sign requests with whatever is configured
# here, so a fixed placeholder is sufficient.
if config_env() == :prod do
config :pears, slack_client_id: System.fetch_env!("SLACK_CLIENT_ID")
config :pears, slack_client_secret: System.fetch_env!("SLACK_CLIENT_SECRET")
config :pears, slack_signing_secret: System.fetch_env!("SLACK_SIGNING_SECRET")
else
config :pears, slack_client_id: System.get_env("SLACK_CLIENT_ID", "test-slack-client-id")

config :pears,
slack_client_secret: System.get_env("SLACK_CLIENT_SECRET", "test-slack-client-secret")

config :pears,
slack_signing_secret: System.get_env("SLACK_SIGNING_SECRET", "test-slack-signing-secret")
end

config :pears, slack_oauth_redirect_uri: System.get_env("SLACK_OAUTH_URL")

case System.fetch_env("OTEL_EXPORTER") do
Expand Down
7 changes: 7 additions & 0 deletions lib/pears/slack.ex
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,13 @@ defmodule Pears.Slack do
O11y.set_error(error)
{:error, error}
end
rescue
# The Slack HTTP client raises on transport-level failures (e.g. a TLS
# handshake alert). Capture it as an error rather than letting it crash the
# caller (which previously took down the record-pears LiveView handler).
exception ->
O11y.set_error(exception)
{:error, exception}
end

defp fetch_tokens(slack_code) do
Expand Down
27 changes: 22 additions & 5 deletions lib/pears_web/live/pairing_board_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,12 @@ defmodule PearsWeb.PairingBoardLive do

case Pears.record_pears(team_name) do
{:ok, _updated_team} ->
Slack.send_daily_pears_summary(team_name)
# Post the Slack summary off the reply path so a slow or failing Slack
# call can't block the response or crash this handler. The board itself
# refreshes via the PubSub team-updated broadcast that record_pears fires.
send(self(), {:post_daily_pears_summary, team_name})

{:noreply,
socket
|> put_flash(:info, "Today's assigned pears have been recorded!")
|> push_navigate(to: "/")}
{:noreply, put_flash(socket, :info, "Today's assigned pears have been recorded!")}

{:error, changeset} ->
Pears.O11y.set_changeset_errors(changeset)
Expand All @@ -125,6 +125,7 @@ defmodule PearsWeb.PairingBoardLive do

error ->
O11y.set_error(error)
{:noreply, put_flash(socket, :error, "Sorry! Something went wrong, please try again.")}
end
end

Expand Down Expand Up @@ -277,6 +278,22 @@ defmodule PearsWeb.PairingBoardLive do
{:noreply, put_flash(socket, type, message)}
end

@impl true
def handle_info({:post_daily_pears_summary, team_name}, socket) do
case Slack.send_daily_pears_summary(team_name) do
{:error, _reason} ->
{:noreply,
put_flash(
socket,
:error,
"Today's pears were recorded, but the Slack summary could not be posted."
)}

_ ->
{:noreply, socket}
end
end

defp team(socket), do: socket.assigns.team
defp team_name(socket), do: team(socket).name
defp selected_pear(socket), do: socket.assigns.selected_pear
Expand Down
1 change: 0 additions & 1 deletion mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"bandit": {:hex, :bandit, "1.10.2", "d15ea32eb853b5b42b965b24221eb045462b2ba9aff9a0bda71157c06338cbff", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "27b2a61b647914b1726c2ced3601473be5f7aa6bb468564a688646a689b3ee45"},
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.2.1", "e361261a0401d82dadc1ab7b969f91d250bf7577283e933fe8c5b72f8f5b3c46", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "81170177d5c2e280d12141a0b9d9e299bf731535e2d959982bdcd4cfe3c82865"},
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
"castore": {:hex, :castore, "1.0.12", "053f0e32700cbec356280c0e835df425a3be4bc1e0627b714330ad9d0f05497f", [:mix], [], "hexpm", "3dca286b2186055ba0c9449b4e95b97bf1b57b47c1f2644555879e659960c224"},
"cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
"certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"},
"chatterbox": {:hex, :ts_chatterbox, "0.15.1", "5cac4d15dd7ad61fc3c4415ce4826fc563d4643dee897a558ec4ea0b1c835c9c", [:rebar3], [{:hpack, "~> 0.3.0", [hex: :hpack_erl, repo: "hexpm", optional: false]}], "hexpm", "4f75b91451338bc0da5f52f3480fa6ef6e3a2aeecfc33686d6b3d0a0948f31aa"},
Expand Down
13 changes: 13 additions & 0 deletions test/pears/slack_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,19 @@ defmodule Pears.SlackTest do
{:error, _} = Slack.send_message_to_team(name, "Hey, friends!")
refute_receive {:send_message, _, _, _}
end

test "returns an error instead of raising when the slack client blows up", %{name: name} do
channel = %{id: "UXXXXXXX", name: "random"}
{:ok, _} = Slack.save_team_channel(Details.empty(), name, channel)

# Simulates a transport-level failure such as the TLS handshake alert that
# crashed record-pears: the underlying client raises rather than returning.
expect(MockSlackClient, :send_message, fn _channel, _message, _token ->
raise "TLS client: ... Unsupported Certificate (key_usage_mismatch)"
end)

assert {:error, _} = Slack.send_message_to_team(name, "Hey, friends!")
end
end

describe "send_daily_pears_summary" do
Expand Down
44 changes: 44 additions & 0 deletions test/pears_web/live/pairing_board_live_test.exs
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
defmodule PearsWeb.TeamLiveTest do
use PearsWeb.ConnCase, async: true
import Phoenix.LiveViewTest
import Mox

alias Pears.Slack
alias Pears.SlackFixtures

setup :verify_on_exit!

describe "when logged in" do
setup :register_and_log_in_team
Expand All @@ -12,6 +18,44 @@ defmodule PearsWeb.TeamLiveTest do
end
end

describe "recording pears when Slack is failing" do
setup :register_and_log_in_team

setup %{team: team} do
stub(Pears.MockSlackClient, :retrieve_access_tokens, fn _code, _url ->
SlackFixtures.valid_token_response(%{access_token: "xoxb-test-token"})
end)

{:ok, _} = Slack.onboard_team(team.name, "valid_code")

{:ok, _} =
Slack.save_team_channel(Slack.Details.empty(), team.name, %{
id: "UXXXXXXX",
name: "random"
})

FeatureFlags.enable(:send_daily_pears_summary, for_actor: team)
:ok
end

test "Save still records and warns the user instead of crashing the board", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/teams")
# The async Slack post runs in the LiveView process; allow the mock there.
allow(Pears.MockSlackClient, self(), view.pid)

stub(Pears.MockSlackClient, :send_message, fn _channel, _message, _token ->
raise "TLS client: ... Unsupported Certificate (key_usage_mismatch)"
end)

view |> element("button", "Save") |> render_click()

html = render(view)
assert html =~ "recorded"
assert html =~ "Slack summary could not be posted"
assert Process.alive?(view.pid)
end
end

describe "when logged out" do
test "when not logged in, redirects to login", %{conn: conn} do
{:error, {:redirect, %{to: redirected_to}}} = live(conn, "/teams")
Expand Down
Loading