• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

supabase / supavisor / 17802639047

17 Sep 2025 03:26PM UTC coverage: 59.631% (+0.1%) from 59.516%
17802639047

push

github

web-flow
test: add test for DB SSL negotiation rejection ('N' response) (#743)

## What kind of change does this PR introduce?

Test coverage

## What is the new behavior?

Add new test to verify the connection is properly rejected when the DB
denies SSL negotiation with an `'N'` response.

1551 of 2601 relevant lines covered (59.63%)

5277.12 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.44
/lib/supavisor/secret_checker.ex
1
defmodule Supavisor.SecretChecker do
2
  @moduledoc false
3

4
  use GenServer
5
  require Logger
6

7
  alias Supavisor.Helpers
8

9
  @interval :timer.seconds(15)
10

11
  def start_link(args) do
12
    name = {:via, Registry, {Supavisor.Registry.Tenants, {:secret_checker, args.id}}}
28✔
13
    GenServer.start_link(__MODULE__, args, name: name)
28✔
14
  end
15

16
  @spec get_secrets(Supavisor.id()) ::
17
          {:ok, {method :: atom(), Supavisor.secrets()}} | {:error, :not_started}
18
  def get_secrets(id) do
19
    case Registry.lookup(Supavisor.Registry.Tenants, {:secret_checker, id}) do
21✔
20
      [] ->
19✔
21
        {:error, :not_started}
22

23
      [{pid, _}] ->
24
        GenServer.call(pid, :get_secrets)
2✔
25
    end
26
  end
27

28
  def init(args) do
29
    Logger.debug("SecretChecker: Starting secret checker")
28✔
30
    tenant = Supavisor.tenant(args.id)
28✔
31

32
    %{auth: auth, user: user} = Enum.find(args.replicas, fn e -> e.replica_type == :write end)
28✔
33

34
    state = %{
28✔
35
      tenant: tenant,
36
      auth: auth,
37
      user: user,
38
      key: {:secrets, tenant, user},
39
      ttl: args[:ttl] || :timer.hours(24),
28✔
40
      conn: nil,
41
      check_ref: check()
42
    }
43

44
    Logger.metadata(project: tenant, user: user)
28✔
45
    {:ok, state, {:continue, :init_conn}}
28✔
46
  end
47

48
  def handle_continue(:init_conn, %{auth: auth} = state) do
49
    ssl_opts =
28✔
50
      if auth.upstream_ssl and auth.upstream_verify == :peer do
28✔
51
        [
52
          verify: :verify_peer,
53
          cacerts: [Helpers.upstream_cert(auth.upstream_tls_ca)],
×
54
          server_name_indication: auth.host,
×
55
          customize_hostname_check: [{:match_fun, fn _, _ -> true end}]
×
56
        ]
57
      else
58
        [
59
          verify: :verify_none
60
        ]
61
      end
62

63
    {:ok, conn} =
28✔
64
      Postgrex.start_link(
65
        hostname: auth.host,
28✔
66
        port: auth.port,
28✔
67
        database: auth.database,
28✔
68
        password: auth.password.(),
28✔
69
        username: auth.user,
28✔
70
        parameters: [application_name: "Supavisor auth_query"],
71
        ssl: auth.upstream_ssl,
28✔
72
        socket_options: [
73
          auth.ip_version
28✔
74
        ],
75
        queue_target: 1_000,
76
        queue_interval: 5_000,
77
        ssl_opts: ssl_opts
78
      )
79

80
    # kill the postgrex connection if the current process exits unexpectedly
81
    Process.link(conn)
28✔
82
    {:noreply, %{state | conn: conn}}
83
  end
84

85
  def handle_info(:check, state) do
86
    check_secrets(state.user, state)
3✔
87
    {:noreply, %{state | check_ref: check()}}
88
  end
89

90
  def handle_info(msg, state) do
91
    Logger.error("Unexpected message: #{inspect(msg)}")
×
92
    {:noreply, state}
93
  end
94

95
  def terminate(_, state) do
96
    :gen_statem.stop(state.conn)
×
97
    :ok
98
  end
99

100
  def check(interval \\ @interval),
31✔
101
    do: Process.send_after(self(), :check, interval)
31✔
102

103
  def check_secrets(user, %{auth: auth, conn: conn} = state) do
104
    case Helpers.get_user_secret(conn, auth.auth_query, user) do
5✔
105
      {:ok, secret} ->
106
        method = if secret.digest == :md5, do: :auth_query_md5, else: :auth_query
5✔
107
        secrets = Map.put(secret, :alias, auth.alias)
5✔
108

109
        update_cache =
5✔
110
          case Cachex.get(Supavisor.Cache, state.key) do
5✔
111
            {:ok, {:cached, {_, {old_method, old_secrets}}}} ->
112
              method != old_method or secrets != old_secrets.()
5✔
113

114
            other ->
115
              Logger.error("Failed to get cache: #{inspect(other)}")
×
116
              true
117
          end
118

119
        if update_cache do
5✔
120
          Logger.info("Secrets changed or not present, updating cache")
2✔
121
          value = {:ok, {method, fn -> secrets end}}
2✔
122
          Cachex.put(Supavisor.Cache, state.key, {:cached, value}, expire: :timer.hours(24))
2✔
123
        end
124

125
        {:ok, {method, fn -> secret end}}
10✔
126

127
      other ->
128
        Logger.error("Failed to get secret: #{inspect(other)}")
×
129
    end
130
  end
131

132
  def handle_call(:get_secrets, _from, state) do
133
    {:reply, check_secrets(state.user, state), state}
2✔
134
  end
135
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc