Skip to content

Commit

Permalink
✨ mock db for tests
Browse files Browse the repository at this point in the history
  • Loading branch information
AbdelStark committed Sep 24, 2024
1 parent 88ff12a commit ed07f05
Show file tree
Hide file tree
Showing 10 changed files with 333 additions and 85 deletions.
2 changes: 2 additions & 0 deletions config/dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,6 @@ config :gakimint, Gakimint.Repo,
port: 5432,
pool_size: 10

config :gakimint, :repo, Gakimint.Repo

config :gakimint, ecto_repos: [Gakimint.Repo]
29 changes: 29 additions & 0 deletions config/test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Config

# We don't run a server during test. If one is required,
# you can enable the server option below.
config :gakimint, Gakimint.Web.Endpoint,
http: [ip: {127, 0, 0, 1}, port: 4002],
secret_key_base: "your_secret_key_base_for_tests",
server: false

# Print only warnings and errors during test
config :logger, level: :warning

# Initialize plugs at runtime for faster test compilation
config :phoenix, :plug_init_mode, :runtime

# Use mock repo if MOCK_DB environment variable is set to "true"
if System.get_env("MOCK_DB") == "true" do
config :gakimint, :repo, Gakimint.MockRepo
else
config :gakimint, Gakimint.Repo,
username: "postgres",
password: "postgres",
hostname: "localhost",
database: "gakimint_test#{System.get_env("MIX_TEST_PARTITION")}",
pool: Ecto.Adapters.SQL.Sandbox,
pool_size: 10

config :gakimint, :repo, Gakimint.MockRepo
end
15 changes: 12 additions & 3 deletions lib/gakimint/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,21 @@ defmodule Gakimint.Application do
def start(_type, _args) do
children = [
Gakimint.Web.Telemetry,
Gakimint.Repo,
{Phoenix.PubSub, name: Gakimint.PubSub},
Endpoint,
Gakimint.Mint
Endpoint
]

# Conditionally add the appropriate repo to the children list
children =
case Application.get_env(:gakimint, :repo) do
Gakimint.MockRepo -> [Gakimint.MockRepo | children]
Gakimint.Repo -> [Gakimint.Repo | children]
_ -> children
end

# Always add Gakimint.Mint after the repo
children = children ++ [Gakimint.Mint]

opts = [strategy: :one_for_one, name: Gakimint.Supervisor]
Supervisor.start_link(children, opts)
end
Expand Down
55 changes: 30 additions & 25 deletions lib/gakimint/core/mint.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule Gakimint.Mint do
"""

use GenServer
alias Gakimint.{Crypto.BDHKE, Repo}
alias Gakimint.Crypto.BDHKE
alias Gakimint.Schema.{Key, Keyset, MintConfiguration}
import Ecto.Query

Expand All @@ -25,32 +25,37 @@ defmodule Gakimint.Mint do

@impl true
def handle_continue(:load_keysets_and_mint_key, state) do
seed = get_or_create_seed()
keysets = load_or_create_keysets(seed)
{_, mint_pubkey} = get_or_create_mint_key(seed)
repo = Application.get_env(:gakimint, :repo)
seed = get_or_create_seed(repo)
keysets = load_or_create_keysets(repo, seed)
{_, mint_pubkey} = get_or_create_mint_key(repo, seed)
{:noreply, %{state | keysets: keysets, mint_pubkey: mint_pubkey}}
end

defp get_or_create_seed do
case Repo.get_by(MintConfiguration, key: @keyset_generation_seed_key) do
defp get_or_create_seed(repo) do
case repo.get_by(MintConfiguration, key: @keyset_generation_seed_key) do
nil ->
seed =
System.get_env("KEYSET_GENERATION_SEED") ||
:crypto.strong_rand_bytes(32) |> Base.encode16(case: :lower)

%MintConfiguration{}
|> MintConfiguration.changeset(%{key: @keyset_generation_seed_key, value: seed})
|> Repo.insert!()
case %MintConfiguration{}
|> MintConfiguration.changeset(%{key: @keyset_generation_seed_key, value: seed})
|> repo.insert() do
{:ok, config} ->
config.value

seed
{:error, changeset} ->
raise "Failed to insert MintConfiguration: #{inspect(changeset.errors)}"
end

config ->
config.value
end
end

defp load_or_create_keysets(seed) do
case Repo.all(Keyset) do
defp load_or_create_keysets(repo, seed) do
case repo.all(Keyset) do
[] ->
keyset =
Keyset.generate("sat", seed, @keyset_generation_derivation_path, @default_input_fee_ppk)
Expand All @@ -62,9 +67,9 @@ defmodule Gakimint.Mint do
end
end

defp get_or_create_mint_key(seed) do
case {Repo.get_by(MintConfiguration, key: @mint_pubkey_key),
Repo.get_by(MintConfiguration, key: @mint_privkey_key)} do
defp get_or_create_mint_key(repo, seed) do
case {repo.get_by(MintConfiguration, key: @mint_pubkey_key),
repo.get_by(MintConfiguration, key: @mint_privkey_key)} do
{nil, nil} ->
{privkey, pubkey} = derive_mint_key(seed)

Expand All @@ -73,14 +78,14 @@ defmodule Gakimint.Mint do
key: @mint_pubkey_key,
value: Base.encode16(pubkey, case: :lower)
})
|> Repo.insert!()
|> repo.insert!()

%MintConfiguration{}
|> MintConfiguration.changeset(%{
key: @mint_privkey_key,
value: Base.encode16(privkey, case: :lower)
})
|> Repo.insert!()
|> repo.insert!()

{privkey, pubkey}

Expand Down Expand Up @@ -113,8 +118,8 @@ defmodule Gakimint.Mint do
GenServer.call(__MODULE__, :get_keysets)
end

def get_keys_for_keyset(keyset_id) do
Repo.all(
def get_keys_for_keyset(repo, keyset_id) do
repo.all(
from(k in Key,
where: k.keyset_id == ^keyset_id,
order_by: [asc: k.amount]
Expand All @@ -126,15 +131,15 @@ defmodule Gakimint.Mint do
GenServer.call(__MODULE__, :get_mint_pubkey)
end

def get_active_keysets do
Repo.all(from(k in Keyset, where: k.active == true))
def get_active_keysets(repo) do
repo.all(from(k in Keyset, where: k.active == true))
end

def get_all_keysets do
Repo.all(Keyset)
def get_all_keysets(repo) do
repo.all(Keyset)
end

def get_keyset(keyset_id) do
Repo.get(Keyset, keyset_id)
def get_keyset(repo, keyset_id) do
repo.get(Keyset, keyset_id)
end
end
200 changes: 200 additions & 0 deletions lib/gakimint/db/mock_repo.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
defmodule Gakimint.MockRepo do
@moduledoc """
Mock repository for testing without a database.
"""

use GenServer
alias Ecto.Schema.Loader

@behaviour Ecto.Repo

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

def init(_) do
{:ok, %{}}
end

# Implement required Ecto.Repo callbacks

def config do
[
pool_size: 1,
telemetry_prefix: [:gakimint, :mock_repo]
]
end

def __adapter__ do
Ecto.Adapters.SQL
end

def checkout(_, _) do
{:ok, self()}
end

def checked_out? do
false
end

def default_options(_) do
[]
end

def get_dynamic_repo do
__MODULE__
end

def put_dynamic_repo(_) do
:ok
end

def stop(_) do
:ok
end

def load(schema_or_types, data) do
case schema_or_types do
schema when is_atom(schema) ->
Loader.unsafe_load(schema, data, &load_type/2)

types when is_map(types) ->
Loader.unsafe_load(%{}, types, data, &load_type/2)
end
end

defp load_type(type, value) do
case Ecto.Type.adapter_load(Ecto.Adapters.SQL, type, value) do
{:ok, value} -> value
:error -> raise ArgumentError, "cannot load `#{inspect(value)}` as type #{inspect(type)}"
end
end

# Existing CRUD operations

def all(queryable) do
GenServer.call(__MODULE__, {:all, queryable})
end

def get(queryable, id, opts \\ []) do
GenServer.call(__MODULE__, {:get, queryable, id, opts})
end

def get_by(queryable, clauses, opts \\ []) do
GenServer.call(__MODULE__, {:get_by, queryable, clauses, opts})
end

def insert(struct, opts \\ []) do
GenServer.call(__MODULE__, {:insert, struct, opts})
end

def insert!(struct, opts \\ []) do
case insert(struct, opts) do
{:ok, struct} ->
struct

{:error, changeset} ->
raise Ecto.InvalidChangesetError, action: :insert, changeset: changeset
end
end

def update(struct, opts \\ []) do
GenServer.call(__MODULE__, {:update, struct, opts})
end

def delete(struct, opts \\ []) do
GenServer.call(__MODULE__, {:delete, struct, opts})
end

# GenServer callbacks

def handle_call({:all, queryable}, _from, state) do
result = Map.get(state, queryable, [])
{:reply, result, state}
end

def handle_call({:get, queryable, id, _opts}, _from, state) do
result = get_by_id(state, queryable, id)
{:reply, result, state}
end

def handle_call({:get_by, queryable, clauses, _opts}, _from, state) do
result = get_by_clauses(state, queryable, clauses)
{:reply, result, state}
end

def handle_call({:insert, %Ecto.Changeset{} = changeset, _opts}, _from, state) do
insert_changeset(changeset, state)
end

def handle_call({:insert, struct, opts}, from, state) do
changeset = Ecto.Changeset.change(struct)
handle_call({:insert, changeset, opts}, from, state)
end

def handle_call({:update, struct, _opts}, _from, state) do
new_state = update_struct(state, struct)
{:reply, {:ok, struct}, new_state}
end

def handle_call({:delete, struct, _opts}, _from, state) do
new_state = delete_struct(state, struct)
{:reply, {:ok, struct}, new_state}
end

# Private functions

defp get_by_id(state, queryable, id) do
state
|> Map.get(queryable, [])
|> Enum.find(fn item -> item.id == id end)
end

defp get_by_clauses(state, queryable, clauses) do
state
|> Map.get(queryable, [])
|> Enum.find(fn item ->
Enum.all?(clauses, fn {key, value} -> Map.get(item, key) == value end)
end)
end

defp insert_changeset(changeset, state) do
case Ecto.Changeset.apply_action(changeset, :insert) do
{:ok, struct} -> insert_struct(struct, state)
{:error, changeset} -> {:reply, {:error, changeset}, state}
end
end

defp insert_struct(struct, state) do
id = System.unique_integer([:positive])
inserted_at = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
updated_at = inserted_at

new_struct =
struct
|> Map.from_struct()
|> Map.merge(%{id: id, inserted_at: inserted_at, updated_at: updated_at})
|> then(&struct(struct.__struct__, &1))

new_state = Map.update(state, struct.__struct__, [new_struct], &[new_struct | &1])
{:reply, {:ok, new_struct}, new_state}
end

defp update_struct(state, struct) do
Map.update!(state, struct.__struct__, &update_list(&1, struct))
end

defp update_list(list, struct) do
Enum.map(list, &update_item(&1, struct))
end

defp update_item(item, struct) do
if item.id == struct.id, do: struct, else: item
end

defp delete_struct(state, struct) do
Map.update!(state, struct.__struct__, fn list ->
Enum.reject(list, &(&1.id == struct.id))
end)
end
end
Loading

0 comments on commit ed07f05

Please sign in to comment.