Skip to content

Commit

Permalink
Merge pull request #11 from Nickforall/chore/tests
Browse files Browse the repository at this point in the history
Add tests
  • Loading branch information
Nickforall authored Apr 7, 2021
2 parents a6ce810 + e65b6ff commit c9b2705
Show file tree
Hide file tree
Showing 13 changed files with 353 additions and 38 deletions.
56 changes: 56 additions & 0 deletions .github/workflows/elixir.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Based upon https://github.com/absinthe-graphql/absinthe/blob/master/.github/workflows/elixir.yml
# Copyright (c) Bruce Williams, Ben Wilson

name: CI

on:
push:
pull_request:
branches:
- main

jobs:
test:
name: Elixir ${{matrix.elixir}} / OTP ${{matrix.otp}}
runs-on: ubuntu-latest

strategy:
matrix:
elixir:
- "1.10"
- "1.11"
otp:
- "22"
- "23"

steps:
- name: Checkout
uses: actions/checkout@v2

- name: Set up Elixir
uses: erlef/setup-elixir@v1
with:
elixir-version: ${{ matrix.elixir }}
otp-version: ${{ matrix.otp }}

- name: Restore deps cache
uses: actions/cache@v2
with:
path: |
deps
_build
key: deps-${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('**/mix.lock') }}-${{ github.sha }}
restore-keys: |
deps-${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('**/mix.lock') }}
deps-${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}
- name: Install package dependencies
run: mix deps.get

- name: Check Formatting
run: mix format --check-formatted

- name: Run unit tests
run: |
mix clean
mix test
13 changes: 13 additions & 0 deletions lib/key_source.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
defmodule ExFirebaseAuth.KeySource do
@moduledoc false

@callback fetch_certificates() :: :error | {:ok, list(JOSE.JWK.t())}

def fetch_certificates do
apply(
Application.get_env(:ex_firebase_auth, :key_source, ExFirebaseAuth.KeySource.Google),
:fetch_certificates,
[]
)
end
end
45 changes: 13 additions & 32 deletions lib/key_store.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,46 +5,46 @@ defmodule ExFirebaseAuth.KeyStore do

require Logger

@endpoint_url "https://www.googleapis.com/robot/v1/metadata/x509/[email protected]"

def start_link(_) do
GenServer.start_link(__MODULE__, %{}, name: ExFirebaseAuth.KeyStore)
end

def init(_) do
find_or_create_ets_table()

case fetch_certificates() do
case ExFirebaseAuth.KeySource.fetch_certificates() do
# when we could not fetch certs initially the application cannot run because all Auth will fail
:error ->
{:stop, "Initial certificate fetch failed"}

{:ok, data} ->
store_data_to_ets(data)
schedule_refresh(1)

Logger.debug("Fetched initial firebase auth certificates.")

schedule_refresh()

{:ok, %{}}
end
end

# When the refresh `info` is sent, we want to fetch the certificates
def handle_info(:refresh, state) do
case fetch_certificates() do
case ExFirebaseAuth.KeySource.fetch_certificates() do
# keep trying with a lower interval, until then keep the old state
:error ->
Logger.warn("Fetching firebase auth certificates failed, using old state and retrying...")
schedule_refresh(10)

{:noreply, state}

# if everything went okay, refresh at the regular interval and store the returned keys in state
{:ok, jsondata} ->
{:ok, keys} ->
store_data_to_ets(keys)

Logger.debug("Fetched new firebase auth certificates.")
store_data_to_ets(jsondata)
schedule_refresh()

{:noreply, state}
end

{:noreply, state}
end

def find_or_create_ets_table do
Expand All @@ -54,15 +54,8 @@ defmodule ExFirebaseAuth.KeyStore do
end
end

defp store_data_to_ets(jsondata) do
jsondata
|> Enum.map(fn {key, value} ->
case JOSE.JWK.from_pem(value) do
[] -> {key, nil}
jwk -> {key, jwk}
end
end)
|> Enum.filter(fn {_, value} -> not is_nil(value) end)
defp store_data_to_ets(data) do
data
|> Enum.each(fn {key, value} ->
:ets.insert(ExFirebaseAuth.KeyStore, {key, value})
end)
Expand All @@ -71,16 +64,4 @@ defmodule ExFirebaseAuth.KeyStore do
defp schedule_refresh(after_s \\ 300) do
Process.send_after(self(), :refresh, after_s * 1000)
end

# Fetch certificates from google's endpoint
defp fetch_certificates do
with {:ok, %Finch.Response{body: body}} <-
Finch.build(:get, @endpoint_url) |> Finch.request(ExFirebaseAuthFinch),
{:ok, jsondata} <- Jason.decode(body) do
{:ok, jsondata}
else
_ ->
:error
end
end
end
15 changes: 13 additions & 2 deletions lib/mock.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,27 @@ defmodule ExFirebaseAuth.Mock do
you probably do not need this.**
"""
def generate_and_store_key_pair do
unless is_enabled?() do
raise "Cannot generate mocked token, because ExFirebaseAuth.Mock is not enabled in your config."
end

private_table = find_or_create_private_key_table()
public_table = ExFirebaseAuth.KeyStore.find_or_create_ets_table()

{kid, public_key, private_key} = generate_key()

:ets.insert(private_table, {kid, private_key})
:ets.insert(public_table, {kid, public_key})
end

@doc false
def generate_key do
private_key = JOSE.JWS.generate_key(%{"alg" => "RS256"})
public_key = JOSE.JWK.to_public(private_key)

kid = JOSE.JWK.thumbprint(:md5, public_key)

:ets.insert(private_table, {kid, private_key})
:ets.insert(public_table, {kid, public_key})
{kid, public_key, private_key}
end

@spec generate_token(String.t(), map) :: String.t()
Expand Down
29 changes: 29 additions & 0 deletions lib/source/google_key_source.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
defmodule ExFirebaseAuth.KeySource.Google do
@moduledoc false

@endpoint_url "https://www.googleapis.com/robot/v1/metadata/x509/[email protected]"

@behaviour ExFirebaseAuth.KeySource

def fetch_certificates do
with {:ok, %Finch.Response{body: body}} <-
Finch.build(:get, @endpoint_url) |> Finch.request(ExFirebaseAuthFinch),
{:ok, json_data} <- Jason.decode(body) do
{:ok, convert_to_jose_keys(json_data)}
else
_ ->
:error
end
end

defp convert_to_jose_keys(json_data) do
json_data
|> Enum.map(fn {key, value} ->
case JOSE.JWK.from_pem(value) do
[] -> {key, nil}
jwk -> {key, jwk}
end
end)
|> Enum.filter(fn {_, value} -> not is_nil(value) end)
end
end
13 changes: 13 additions & 0 deletions lib/source/mock_key_source.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
defmodule ExFirebaseAuth.KeySource.Mock do
@moduledoc false

@behaviour ExFirebaseAuth.KeySource

defp config do
Application.get_env(:ex_firebase_auth, :key_source_mock, keys: [])
end

def fetch_certificates do
{:ok, config()[:keys]}
end
end
6 changes: 6 additions & 0 deletions lib/token.ex
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ defmodule ExFirebaseAuth.Token do
{:key, _} ->
{:error, "Public key retrieved from google was not found or could not be parsed"}

{:verify, {false, _, _}} ->
{:error, "Invalid signature"}

{:verify, {true, _, _}} ->
{:error, "Signed by invalid issuer"}

{:verify, _} ->
{:error, "None of public keys matched auth token's key ids"}
end
Expand Down
3 changes: 2 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ defmodule ExFirebaseAuth.MixProject do
[
app: :ex_firebase_auth,
version: "0.3.1",
elixir: "~> 1.11",
elixir: "~> 1.10",
start_permanent: Mix.env() == :prod,
deps: deps(),
package: package(),
aliases: [test: "test"],
docs: [
main: "readme",
extras: ["README.md"]
Expand Down
3 changes: 0 additions & 3 deletions test/ex_firebase_auth_test.exs

This file was deleted.

28 changes: 28 additions & 0 deletions test/key_store_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
defmodule ExFirebaseAuth.KeyStoreTest do
use ExUnit.Case

setup do
{kid, public_key, _} = ExFirebaseAuth.Mock.generate_key()

Application.get_env(:ex_firebase_auth, :key_source, ExFirebaseAuth.KeySource.Mock)

Application.put_env(:ex_firebase_auth, :key_source_mock,
keys: [
{kid, public_key}
]
)

%{kid: kid, key: public_key}
end

test "Does add new key to ets on refresh", %{kid: kid, key: public_key} do
assert :ets.lookup(ExFirebaseAuth.KeyStore, kid) == []

Process.send(ExFirebaseAuth.KeyStore, :refresh, [])

# TODO: there's probably a better way to test this behavior
Process.sleep(100)

assert :ets.lookup(ExFirebaseAuth.KeyStore, kid) == [{kid, public_key}]
end
end
32 changes: 32 additions & 0 deletions test/mock_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
defmodule ExFirebaseAuth.MockTest do
use ExUnit.Case

alias ExFirebaseAuth.Mock

setup do
on_exit(fn ->
:ok = Application.delete_env(:ex_firebase_auth, :mock)
end)
end

describe "Token.generate_and_store_key_pair/0" do
test "Fails when mock is disabled" do
assert_raise(
RuntimeError,
~r/^Cannot generate mocked token, because ExFirebaseAuth.Mock is not enabled in your config./,
fn ->
Mock.generate_and_store_key_pair()
end
)
end

test "Creates ETS table and stores key" do
Application.put_env(:ex_firebase_auth, :mock, enabled: true)

assert :ets.whereis(ExFirebaseAuth.Mock) == :undefined
Mock.generate_and_store_key_pair()

[{_, %JOSE.JWK{} = _}] = :ets.lookup(ExFirebaseAuth.Mock, :ets.first(ExFirebaseAuth.Mock))
end
end
end
2 changes: 2 additions & 0 deletions test/test_helper.exs
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
Application.put_env(:ex_firebase_auth, :key_source, ExFirebaseAuth.KeySource.Mock)

ExUnit.start()
Loading

0 comments on commit c9b2705

Please sign in to comment.