defmodule Aprsme.PartitionManagerTest do use Aprsme.DataCase, async: false alias Aprsme.Packet alias Aprsme.PartitionManager setup do # Defensive: any rows in packets_default block ATTACH PARTITION on # overlapping ranges with a check_violation. Wipe the parent table # so partition tests can freely create/drop partitions. Repo.delete_all(Packet) :ok end describe "partition_name/1" do test "generates correct name for a date" do assert PartitionManager.partition_name(~D[2026-02-20]) == "packets_20260220" assert PartitionManager.partition_name(~D[2026-01-01]) == "packets_20260101" assert PartitionManager.partition_name(~D[2025-12-31]) == "packets_20251231" end end describe "partition_range/1" do test "returns start and end timestamps for a date" do {start_dt, end_dt} = PartitionManager.partition_range(~D[2026-02-20]) assert start_dt == ~U[2026-02-20 00:00:00Z] assert end_dt == ~U[2026-02-21 00:00:00Z] end test "handles month boundaries" do {_start_dt, end_dt} = PartitionManager.partition_range(~D[2026-01-31]) assert end_dt == ~U[2026-02-01 00:00:00Z] end test "handles year boundaries" do {_start_dt, end_dt} = PartitionManager.partition_range(~D[2025-12-31]) assert end_dt == ~U[2026-01-01 00:00:00Z] end end describe "ensure_partitions_exist/0" do test "creates partitions for today and next 2 days" do today = Date.utc_today() {:ok, created} = PartitionManager.ensure_partitions_exist() assert match?([_ | _], created) # Verify partitions exist in the database for offset <- 0..2 do date = Date.add(today, offset) name = PartitionManager.partition_name(date) assert {:ok, %{num_rows: 1}} = Repo.query( "SELECT 1 FROM pg_tables WHERE tablename = $1 AND schemaname = 'public'", [name] ) end end test "is idempotent - calling twice doesn't error" do {:ok, _created1} = PartitionManager.ensure_partitions_exist() {:ok, _created2} = PartitionManager.ensure_partitions_exist() end end describe "drop_old_partitions/1" do test "drops partitions older than retention days" do # Create a partition for 30 days ago old_date = Date.add(Date.utc_today(), -30) name = PartitionManager.partition_name(old_date) {from_dt, to_dt} = PartitionManager.partition_range(old_date) Repo.query!( "CREATE TABLE IF NOT EXISTS #{name} PARTITION OF packets FOR VALUES FROM ('#{DateTime.to_iso8601(from_dt)}') TO ('#{DateTime.to_iso8601(to_dt)}')" ) # Verify it exists assert {:ok, %{num_rows: 1}} = Repo.query( "SELECT 1 FROM pg_tables WHERE tablename = $1 AND schemaname = 'public'", [name] ) # Drop partitions older than 7 days {:ok, dropped} = PartitionManager.drop_old_partitions(7) assert name in dropped # Verify it's gone assert {:ok, %{num_rows: 0}} = Repo.query( "SELECT 1 FROM pg_tables WHERE tablename = $1 AND schemaname = 'public'", [name] ) end test "does not drop recent partitions" do # Ensure today's partition exists {:ok, _} = PartitionManager.ensure_partitions_exist() today_name = PartitionManager.partition_name(Date.utc_today()) {:ok, dropped} = PartitionManager.drop_old_partitions(7) refute today_name in dropped # Verify today's partition still exists assert {:ok, %{num_rows: 1}} = Repo.query( "SELECT 1 FROM pg_tables WHERE tablename = $1 AND schemaname = 'public'", [today_name] ) end test "drops the partition at the retention cutoff boundary" do cutoff_date = Date.add(Date.utc_today(), -7) name = PartitionManager.partition_name(cutoff_date) {from_dt, to_dt} = PartitionManager.partition_range(cutoff_date) Repo.query!( "CREATE TABLE IF NOT EXISTS #{name} PARTITION OF packets FOR VALUES FROM ('#{DateTime.to_iso8601(from_dt)}') TO ('#{DateTime.to_iso8601(to_dt)}')" ) {:ok, dropped} = PartitionManager.drop_old_partitions(7) assert name in dropped end end describe "list_partitions/0" do test "returns list of existing partition names" do {:ok, _} = PartitionManager.ensure_partitions_exist() partitions = PartitionManager.list_partitions() # credo:disable-for-next-line Credo.Check.Performance.Length assert length(partitions) >= 3 today_name = PartitionManager.partition_name(Date.utc_today()) assert today_name in partitions end end describe "init/1 and handle_info/2" do test "init/1 returns :ok state without scheduling in :test env" do original = Application.get_env(:aprsme, :env) Application.put_env(:aprsme, :env, :test) try do assert {:ok, %{}} = PartitionManager.init([]) # The :manage_partitions message should NOT arrive since we're in :test. refute_receive :manage_partitions, 100 after Application.put_env(:aprsme, :env, original) end end test "init/1 schedules :manage_partitions in non-test envs" do original = Application.get_env(:aprsme, :env) Application.put_env(:aprsme, :env, :prod) try do assert {:ok, %{}} = PartitionManager.init([]) # send/2 is synchronous to self() — should be in the mailbox already. assert_receive :manage_partitions, 1000 after Application.put_env(:aprsme, :env, original) end end test "handle_info(:manage_partitions) reschedules and returns :noreply" do # This exercises the create + drop path via real DB access. assert {:noreply, %{}} = PartitionManager.handle_info(:manage_partitions, %{}) # Next tick is scheduled via Process.send_after at 1h, so we won't receive it. end end describe "validate_partition_name! (via create_partition path)" do test "ensure_partitions_exist rejects dates that would produce malformed names" do # partition_name is a pure date formatter — the format is guaranteed # valid for every Date. This test just documents that normal dates pass. for offset <- -7..7 do name = PartitionManager.partition_name(Date.add(Date.utc_today(), offset)) assert name =~ ~r/^packets_\d{8}$/ end end end describe "drop_old_partitions/1 default retention" do test "dropping with the default 7-day retention is a no-op when no partitions are old" do # Ensure today's partition exists. {:ok, _} = PartitionManager.ensure_partitions_exist() # Calling without args uses the default retention_days = 7. assert {:ok, dropped} = PartitionManager.drop_old_partitions() today_name = PartitionManager.partition_name(Date.utc_today()) refute today_name in dropped end end describe "start_link/1" do test "starts the GenServer process with the module name" do # If a previous test left one running, just confirm start_link works. case Process.whereis(PartitionManager) do nil -> {:ok, pid} = PartitionManager.start_link([]) assert Process.alive?(pid) GenServer.stop(pid) pid -> assert Process.alive?(pid) end end end describe "manage_partitions/0 idempotent paths" do test "running :manage_partitions twice covers the {:ok, []} branches" do # First run creates today/+1/+2 partitions (or finds them existing) and # drops nothing old. Second run finds everything in place — both # ensure_partitions_exist and drop_old_partitions return {:ok, []}. assert {:noreply, %{}} = PartitionManager.handle_info(:manage_partitions, %{}) assert {:noreply, %{}} = PartitionManager.handle_info(:manage_partitions, %{}) end end describe "manage_partitions/0 via :manage_partitions handler with non-trivial work" do test "logs created+dropped partitions via the success branches" do # Pre-create a partition far in the past so manage_partitions has # something to drop. old_date = Date.add(Date.utc_today(), -30) old_name = PartitionManager.partition_name(old_date) {from_dt, to_dt} = PartitionManager.partition_range(old_date) Repo.query!( "CREATE TABLE IF NOT EXISTS #{old_name} PARTITION OF packets FOR VALUES FROM ('#{DateTime.to_iso8601(from_dt)}') TO ('#{DateTime.to_iso8601(to_dt)}')" ) assert {:noreply, %{}} = PartitionManager.handle_info(:manage_partitions, %{}) refute old_name in PartitionManager.list_partitions() end end end