diff --git a/.gitignore b/.gitignore index 17ae5f6..c74d7db 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,9 @@ tempest-*.tar # Ignore digested assets cache. /priv/static/cache_manifest.json +# Local SQLite data directory. +/priv/tempest_dev/ + # In case you use Node.js/npm, you want to ignore these. npm-debug.log /assets/node_modules/ diff --git a/README.md b/README.md index 234f3b4..67b2745 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,9 @@ Start the Phoenix server: mix phx.server ``` +Server boot creates `account.sqlite`, `sequencer.sqlite`, and local storage directories +inside `TEMPEST_DATA_DIR` when they do not exist. + By default, development uses `localhost`, `http://localhost:4000`, a data directory under `priv/tempest_dev`, and a 10 MB blob limit. Override those settings with: diff --git a/config/config.exs b/config/config.exs index 9f9700e..0d44f6f 100644 --- a/config/config.exs +++ b/config/config.exs @@ -18,6 +18,13 @@ config :tempest, Tempest.Config, data_dir: Path.expand("../priv/tempest_dev", __DIR__), blob_max_bytes: 10_000_000 +config :tempest, Tempest.Repo, + database: Path.expand("../priv/tempest_dev/account.sqlite", __DIR__), + journal_mode: :wal, + busy_timeout: 5_000, + default_transaction_mode: :immediate, + pool_size: 5 + # Configure the endpoint config :tempest, TempestWeb.Endpoint, url: [host: "localhost"], diff --git a/config/dev.exs b/config/dev.exs index 8cf71db..a30fea2 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -1,14 +1,9 @@ import Config -# Configure your database config :tempest, Tempest.Repo, - username: "postgres", - password: "postgres", - hostname: "localhost", - database: "tempest_dev", stacktrace: true, show_sensitive_data_on_connection_error: true, - pool_size: 10 + pool_size: 5 # For development, we disable any cache and enable # debugging and code reloading. diff --git a/config/runtime.exs b/config/runtime.exs index 2287122..9047684 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -40,23 +40,6 @@ if blob_max_bytes = System.get_env("TEMPEST_BLOB_MAX_BYTES") do end if config_env() == :prod do - database_url = - System.get_env("DATABASE_URL") || - raise """ - environment variable DATABASE_URL is missing. - For example: ecto://USER:PASS@HOST/DATABASE - """ - - maybe_ipv6 = if System.get_env("ECTO_IPV6") in ~w(true 1), do: [:inet6], else: [] - - config :tempest, Tempest.Repo, - # ssl: true, - url: database_url, - pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"), - # For machines with several cores, consider starting multiple pools of `pool_size` - # pool_count: 4, - socket_options: maybe_ipv6 - # The secret key base is used to sign/encrypt cookies and other secrets. # A default value is used in config/dev.exs and config/test.exs but you # want to use a different value for prod and you most likely don't want @@ -87,6 +70,10 @@ if config_env() == :prod do public_url: public_url, data_dir: data_dir + config :tempest, Tempest.Repo, + database: Path.join(data_dir, "account.sqlite"), + pool_size: String.to_integer(System.get_env("POOL_SIZE") || "5") + config :tempest, TempestWeb.Endpoint, url: [host: host, port: 443, scheme: "https"], http: [ diff --git a/config/test.exs b/config/test.exs index 306d217..6c24718 100644 --- a/config/test.exs +++ b/config/test.exs @@ -1,26 +1,20 @@ import Config +test_data_dir = + Path.join([ + System.tmp_dir!(), + "tempest_test", + System.get_env("MIX_TEST_PARTITION") || "default" + ]) + config :tempest, Tempest.Config, - data_dir: - Path.join([ - System.tmp_dir!(), - "tempest_test", - System.get_env("MIX_TEST_PARTITION") || "default" - ]), + data_dir: test_data_dir, public_url: "http://localhost:4002" -# Configure your database -# -# The MIX_TEST_PARTITION environment variable can be used -# to provide built-in test partitioning in CI environment. -# Run `mix help test` for more information. config :tempest, Tempest.Repo, - username: "postgres", - password: "postgres", - hostname: "localhost", - database: "tempest_test#{System.get_env("MIX_TEST_PARTITION")}", + database: Path.join(test_data_dir, "account.sqlite"), pool: Ecto.Adapters.SQL.Sandbox, - pool_size: System.schedulers_online() * 2 + pool_size: 5 # We don't run a server during test. If one is required, # you can enable the server option below. diff --git a/docs/tasks/00-foundation.md b/docs/tasks/00-foundation.md index e21e82c..b25aaa5 100644 --- a/docs/tasks/00-foundation.md +++ b/docs/tasks/00-foundation.md @@ -35,6 +35,10 @@ Expected JSON fields: ```text version status +storage.dataDir +storage.accountDb +storage.sequencerDb +storage.writable ``` ## Done diff --git a/docs/tasks/01-xrpc-shell.md b/docs/tasks/01-xrpc-shell.md index 91d4572..fffb6aa 100644 --- a/docs/tasks/01-xrpc-shell.md +++ b/docs/tasks/01-xrpc-shell.md @@ -17,7 +17,7 @@ Goal: expose the XRPC routing, method registry, and protocol-shaped JSON errors. - [ ] T01-07: Add controller tests for unknown method JSON error. - [ ] T01-08: Add controller tests for wrong HTTP verb. - [ ] T01-09: Add response content-type assertions. -- [ ] T01-10: Add `test/smoke/01-xrpc-shell.hurl` entries for health and `describeServer`. +- [ ] T01-10: Add `test/smoke/xrpc.hurl` entries for health and `describeServer`. ## Integration Tests diff --git a/docs/tasks/02-accounts-sessions.md b/docs/tasks/02-accounts-sessions.md index 01dcb6e..caebed0 100644 --- a/docs/tasks/02-accounts-sessions.md +++ b/docs/tasks/02-accounts-sessions.md @@ -9,8 +9,8 @@ Goal: create local accounts and authenticate XRPC calls. ## Tasks -- [ ] T02-01: Replace Postgres dependency/config with SQLite-first config if still present. -- [ ] T02-02: Add migrations or bootstrap code for `account.sqlite`. +- [x] T02-01: Replace Postgres dependency/config with SQLite-first config if still present. +- [x] T02-02: Add migrations or bootstrap code for `account.sqlite`. - [ ] T02-03: Add `accounts` table and schema/context. - [ ] T02-04: Add password hashing dependency and wrapper. - [ ] T02-05: Implement account creation with validation. diff --git a/lib/mix/tasks/tempest.storage.setup.ex b/lib/mix/tasks/tempest.storage.setup.ex new file mode 100644 index 0000000..7974b7c --- /dev/null +++ b/lib/mix/tasks/tempest.storage.setup.ex @@ -0,0 +1,17 @@ +defmodule Mix.Tasks.Tempest.Storage.Setup do + @moduledoc """ + Creates Tempest's SQLite data directory layout. + """ + + use Mix.Task + + @shortdoc "Creates Tempest SQLite storage files and directories" + + @impl true + def run(_args) do + Mix.Task.run("app.config") + + Tempest.Config.load!() + |> Tempest.Storage.bootstrap!() + end +end diff --git a/lib/tempest/application.ex b/lib/tempest/application.ex index 036a88c..3c4ef89 100644 --- a/lib/tempest/application.ex +++ b/lib/tempest/application.ex @@ -7,7 +7,8 @@ defmodule Tempest.Application do @impl true def start(_type, _args) do - Tempest.Config.load!() + config = Tempest.Config.load!() + Tempest.Storage.bootstrap!(config) children = [ TempestWeb.Telemetry, diff --git a/lib/tempest/config.ex b/lib/tempest/config.ex index 4475115..fcb9790 100644 --- a/lib/tempest/config.ex +++ b/lib/tempest/config.ex @@ -23,6 +23,15 @@ defmodule Tempest.Config do ) end + def account_db_path(%__MODULE__{data_dir: data_dir}), do: Path.join(data_dir, "account.sqlite") + + def sequencer_db_path(%__MODULE__{data_dir: data_dir}), + do: Path.join(data_dir, "sequencer.sqlite") + + def data_dirs(%__MODULE__{data_dir: data_dir}) do + Enum.map(~w(repos blobs tmp backups), &Path.join(data_dir, &1)) + end + @doc """ Validates Tempest configuration and returns a normalized struct. """ diff --git a/lib/tempest/repo.ex b/lib/tempest/repo.ex index 664adee..ee24751 100644 --- a/lib/tempest/repo.ex +++ b/lib/tempest/repo.ex @@ -1,5 +1,5 @@ defmodule Tempest.Repo do use Ecto.Repo, otp_app: :tempest, - adapter: Ecto.Adapters.Postgres + adapter: Ecto.Adapters.SQLite3 end diff --git a/lib/tempest/storage.ex b/lib/tempest/storage.ex new file mode 100644 index 0000000..8ff71a5 --- /dev/null +++ b/lib/tempest/storage.ex @@ -0,0 +1,88 @@ +defmodule Tempest.Storage do + @moduledoc """ + Boots the SQLite-first storage layout used by Tempest. + """ + + alias Exqlite.Sqlite3 + alias Tempest.Config + + @sqlite_bootstrap """ + PRAGMA journal_mode = WAL; + PRAGMA busy_timeout = 5000; + """ + + @sequencer_schema """ + CREATE TABLE IF NOT EXISTS repo_seq ( + seq INTEGER PRIMARY KEY AUTOINCREMENT, + did TEXT NOT NULL, + event_type TEXT NOT NULL, + rev TEXT, + commit_cid TEXT, + event_cbor BLOB NOT NULL, + created_at TEXT NOT NULL + ); + """ + + @doc """ + Creates the Tempest data directory layout and initializes SQLite files. + """ + def bootstrap!(%Config{} = config) do + File.mkdir_p!(config.data_dir) + Enum.each(Config.data_dirs(config), &File.mkdir_p!/1) + + config + |> Config.account_db_path() + |> bootstrap_sqlite_file!() + + config + |> Config.sequencer_db_path() + |> bootstrap_sqlite_file!(@sequencer_schema) + + :ok + end + + @doc """ + Returns public storage readiness metadata for health responses. + """ + def health(%Config{} = config, env) do + account_db = Config.account_db_path(config) + sequencer_db = Config.sequencer_db_path(config) + + %{ + "dataDir" => public_path(config.data_dir, env), + "accountDb" => public_path(account_db, env), + "sequencerDb" => public_path(sequencer_db, env), + "writable" => + writable?(config.data_dir) and File.exists?(account_db) and File.exists?(sequencer_db) + } + end + + defp bootstrap_sqlite_file!(path, schema \\ "") do + File.mkdir_p!(Path.dirname(path)) + + with {:ok, conn} <- Sqlite3.open(path), + :ok <- Sqlite3.execute(conn, @sqlite_bootstrap <> schema), + :ok <- Sqlite3.close(conn) do + :ok + else + {:error, reason} -> + raise RuntimeError, "failed to bootstrap SQLite database #{path}: #{reason}" + end + end + + defp writable?(data_dir) do + test_path = Path.join(data_dir, ".writable") + + case File.write(test_path, "ok") do + :ok -> + File.rm(test_path) + true + + {:error, _reason} -> + false + end + end + + defp public_path(path, :prod), do: Path.basename(path) + defp public_path(path, _env), do: path +end diff --git a/lib/tempest_web/controllers/health_controller.ex b/lib/tempest_web/controllers/health_controller.ex index b7e1e28..22d2c05 100644 --- a/lib/tempest_web/controllers/health_controller.ex +++ b/lib/tempest_web/controllers/health_controller.ex @@ -2,9 +2,13 @@ defmodule TempestWeb.HealthController do use TempestWeb, :controller def show(conn, _params) do + config = Tempest.Config.load!() + env = Application.get_env(:tempest, :env, :prod) + json(conn, %{ status: "ok", - version: Tempest.version() + version: Tempest.version(), + storage: Tempest.Storage.health(config, env) }) end end diff --git a/mix.exs b/mix.exs index 3a6c81b..901d165 100644 --- a/mix.exs +++ b/mix.exs @@ -43,7 +43,7 @@ defmodule Tempest.MixProject do {:phoenix, "~> 1.8.5"}, {:phoenix_ecto, "~> 4.5"}, {:ecto_sql, "~> 3.13"}, - {:postgrex, ">= 0.0.0"}, + {:ecto_sqlite3, "~> 0.23.0"}, {:phoenix_html, "~> 4.1"}, {:phoenix_live_reload, "~> 1.2", only: :dev}, {:phoenix_live_view, "~> 1.1.0"}, @@ -77,10 +77,10 @@ defmodule Tempest.MixProject do # See the documentation for `Mix` for more info on aliases. defp aliases do [ - setup: ["deps.get", "ecto.setup", "assets.setup", "assets.build"], - "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], - "ecto.reset": ["ecto.drop", "ecto.setup"], - test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"], + setup: ["deps.get", "tempest.storage.setup", "ecto.setup", "assets.setup", "assets.build"], + "ecto.setup": ["ecto.migrate", "run priv/repo/seeds.exs"], + "ecto.reset": ["ecto.drop", "tempest.storage.setup", "ecto.setup"], + test: ["tempest.storage.setup", "ecto.migrate --quiet", "test"], "assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"], "assets.build": ["compile", "tailwind tempest", "esbuild tempest"], "assets.deploy": [ diff --git a/mix.lock b/mix.lock index 527cebf..3c11f5f 100644 --- a/mix.lock +++ b/mix.lock @@ -6,9 +6,11 @@ "dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"}, "ecto": {:hex, :ecto, "3.13.6", "352135b474f91d1ab99a1b502171d207e9db60421c9e3d0ecab4c7ab96b24d14", [:mix], [{:decimal, "~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8afa059bc16cd2c94739ec0a11e3e5df69d828125119109bef35f20a21a76af2"}, "ecto_sql": {:hex, :ecto_sql, "3.13.5", "2f8282b2ad97bf0f0d3217ea0a6fff320ead9e2f8770f810141189d182dc304e", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aa36751f4e6a2b56ae79efb0e088042e010ff4935fc8684e74c23b1f49e25fdc"}, + "ecto_sqlite3": {:hex, :ecto_sqlite3, "0.23.0", "79da75815627582f081f00d418c130c4cf587672b720b54e7a8798c6d46b5415", [:mix], [{:decimal, "~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.13.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "e97041bcec746ed525df7d9ad996fbae3b0660767f99fbe9e9b58d6208729703"}, "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, "esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"}, "expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [:mix], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"}, + "exqlite": {:hex, :exqlite, "0.36.0", "07b4f95d61cb82b8d52946d0639497fa7d32117e09b2c8d25e24a38723c295cb", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "cbeca3ce781f9ff07cfa9a87486f3ebd512a143ad6a14ed5c9fca21fe0bf3ae7"}, "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, "finch": {:hex, :finch, "0.21.0", "b1c3b2d48af02d0c66d2a9ebfb5622be5c5ecd62937cf79a88a7f98d48a8290c", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "87dc6e169794cb2570f75841a19da99cfde834249568f2a5b121b809588a4377"}, "fine": {:hex, :fine, "0.1.6", "4bf7151493443c454aac9f2fa2f34f5fefd0346a83fb5586a016c4a135c63247", [:mix], [], "hexpm", "5638eb4495488e885ebec167fa57973e5c35e1a50c344eb7666c90ec1c4e3b12"}, @@ -32,7 +34,6 @@ "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"}, "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, - "postgrex": {:hex, :postgrex, "0.22.1", "b3665ad17e15441557da8f45eeebfcd56e4a2b0b98538b855679a13d05e5cc5d", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "df59f828b167b49a5853f645b65f57eb1bc5f3b230497ceaca7af5d8ac05afef"}, "req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"}, "swoosh": {:hex, :swoosh, "1.25.1", "569fcff34817da8a03f28775146b3c8b71b4c9b14f8f78d37ff3ef422862a18b", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:idna, "~> 6.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "58b3e8db6406fe417a89b5042358d2e8f15d32a3317d4f8581d7a3ae501e410b"}, "tailwind": {:hex, :tailwind, "0.4.1", "e7bcc222fe96a1e55f948e76d13dd84a1a7653fb051d2a167135db3b4b08d3e9", [:mix], [], "hexpm", "6249d4f9819052911120dbdbe9e532e6bd64ea23476056adb7f730aa25c220d1"}, diff --git a/test/smoke/health.hurl b/test/smoke/health.hurl index 2828cef..edc942c 100644 --- a/test/smoke/health.hurl +++ b/test/smoke/health.hurl @@ -4,3 +4,7 @@ HTTP 200 header "content-type" contains "application/json" jsonpath "$.version" exists jsonpath "$.status" == "ok" +jsonpath "$.storage.dataDir" exists +jsonpath "$.storage.accountDb" exists +jsonpath "$.storage.sequencerDb" exists +jsonpath "$.storage.writable" == true diff --git a/test/support/data_case.ex b/test/support/data_case.ex index 19fb1b0..09da0eb 100644 --- a/test/support/data_case.ex +++ b/test/support/data_case.ex @@ -8,10 +8,9 @@ defmodule Tempest.DataCase do Finally, if the test case interacts with the database, we enable the SQL sandbox, so changes done to the database - are reverted at the end of every test. If you are using - PostgreSQL, you can even run database tests asynchronously - by setting `use Tempest.DataCase, async: true`, although - this option is not recommended for other databases. + are reverted at the end of every test. SQLite only supports + one write transaction at a time, so database tests should not + use `async: true`. """ use ExUnit.CaseTemplate diff --git a/test/tempest/storage_test.exs b/test/tempest/storage_test.exs new file mode 100644 index 0000000..18f6e54 --- /dev/null +++ b/test/tempest/storage_test.exs @@ -0,0 +1,73 @@ +defmodule Tempest.StorageTest do + use ExUnit.Case, async: false + + test "bootstraps SQLite data directory layout" do + data_dir = + Path.join(System.tmp_dir!(), "tempest_storage_test_#{System.unique_integer([:positive])}") + + config = + Tempest.Config.validate!( + [ + hostname: "localhost", + public_url: "http://localhost:4000", + data_dir: data_dir, + blob_max_bytes: 10_000_000 + ], + env: :test + ) + + on_exit(fn -> File.rm_rf(data_dir) end) + + assert :ok = Tempest.Storage.bootstrap!(config) + + assert File.dir?(Path.join(data_dir, "repos")) + assert File.dir?(Path.join(data_dir, "blobs")) + assert File.dir?(Path.join(data_dir, "tmp")) + assert File.dir?(Path.join(data_dir, "backups")) + assert File.exists?(Path.join(data_dir, "account.sqlite")) + assert File.exists?(Path.join(data_dir, "sequencer.sqlite")) + assert sequencer_table_exists?(Path.join(data_dir, "sequencer.sqlite")) + + assert %{ + "dataDir" => ^data_dir, + "accountDb" => account_db, + "sequencerDb" => sequencer_db, + "writable" => true + } = Tempest.Storage.health(config, :test) + + assert account_db == Path.join(data_dir, "account.sqlite") + assert sequencer_db == Path.join(data_dir, "sequencer.sqlite") + end + + test "redacts full paths from production health metadata" do + config = + Tempest.Config.validate!( + [ + hostname: "tempest.example.com", + public_url: "https://tempest.example.com", + data_dir: "/var/lib/tempest", + blob_max_bytes: 10_000_000 + ], + env: :test + ) + + assert %{ + "dataDir" => "tempest", + "accountDb" => "account.sqlite", + "sequencerDb" => "sequencer.sqlite" + } = Tempest.Storage.health(config, :prod) + end + + defp sequencer_table_exists?(path) do + {:ok, conn} = Exqlite.Sqlite3.open(path) + + {:ok, statement} = + Exqlite.Sqlite3.prepare(conn, "SELECT name FROM sqlite_master WHERE name = 'repo_seq'") + + {:ok, [["repo_seq"]]} = Exqlite.Sqlite3.fetch_all(conn, statement) + :ok = Exqlite.Sqlite3.release(conn, statement) + :ok = Exqlite.Sqlite3.close(conn) + + true + end +end diff --git a/test/tempest_web/controllers/health_controller_test.exs b/test/tempest_web/controllers/health_controller_test.exs index b416cf5..da18615 100644 --- a/test/tempest_web/controllers/health_controller_test.exs +++ b/test/tempest_web/controllers/health_controller_test.exs @@ -8,5 +8,9 @@ defmodule TempestWeb.HealthControllerTest do assert get_resp_header(conn, "content-type") == ["application/json; charset=utf-8"] assert response["status"] == "ok" assert response["version"] =~ ~r/\Av0\.1\.0\.dev\d+\+g[0-9a-f]{7}\z/ + assert response["storage"]["dataDir"] =~ "tempest_test" + assert response["storage"]["accountDb"] =~ "account.sqlite" + assert response["storage"]["sequencerDb"] =~ "sequencer.sqlite" + assert response["storage"]["writable"] == true end end diff --git a/test/test_helper.exs b/test/test_helper.exs index 41add82..39fca39 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,2 +1,3 @@ ExUnit.start() +Tempest.Config.load!() |> Tempest.Storage.bootstrap!() Ecto.Adapters.SQL.Sandbox.mode(Tempest.Repo, :manual)