more tests
This commit is contained in:
parent
ec5c98781f
commit
8667d6a288
6 changed files with 1344 additions and 0 deletions
283
test/towerops/snmp/adapters/replay_test.exs
Normal file
283
test/towerops/snmp/adapters/replay_test.exs
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
defmodule Towerops.Snmp.Adapters.ReplayTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Towerops.Snmp.Adapters.Replay
|
||||
|
||||
describe "new/1" do
|
||||
test "creates connection opts with OID map" do
|
||||
oid_map = %{
|
||||
"1.3.6.1.2.1.1.1.0" => "Cisco IOS Software",
|
||||
"1.3.6.1.2.1.2.2.1.1.1" => "1"
|
||||
}
|
||||
|
||||
opts = Replay.new(oid_map)
|
||||
|
||||
assert Keyword.get(opts, :adapter) == Replay
|
||||
assert Keyword.get(opts, :oid_map) == oid_map
|
||||
assert Keyword.get(opts, :ip) == "replay"
|
||||
assert Keyword.get(opts, :community) == "public"
|
||||
assert Keyword.get(opts, :version) == "2c"
|
||||
assert Keyword.get(opts, :port) == 161
|
||||
end
|
||||
|
||||
test "works with empty OID map" do
|
||||
opts = Replay.new(%{})
|
||||
|
||||
assert Keyword.get(opts, :adapter) == Replay
|
||||
assert Keyword.get(opts, :oid_map) == %{}
|
||||
end
|
||||
end
|
||||
|
||||
describe "get/2" do
|
||||
test "returns value for existing OID" do
|
||||
oid_map = %{"1.3.6.1.2.1.1.1.0" => "Cisco IOS Software"}
|
||||
opts = Replay.new(oid_map)
|
||||
|
||||
assert {:ok, "Cisco IOS Software"} = Replay.get(opts, "1.3.6.1.2.1.1.1.0")
|
||||
end
|
||||
|
||||
test "returns error for non-existent OID" do
|
||||
opts = Replay.new(%{})
|
||||
|
||||
assert {:error, :no_such_name} = Replay.get(opts, "1.3.6.1.2.1.1.1.0")
|
||||
end
|
||||
|
||||
test "parses integer values" do
|
||||
oid_map = %{"1.3.6.1.2.1.2.2.1.1.1" => "42"}
|
||||
opts = Replay.new(oid_map)
|
||||
|
||||
assert {:ok, 42} = Replay.get(opts, "1.3.6.1.2.1.2.2.1.1.1")
|
||||
end
|
||||
|
||||
test "parses float values" do
|
||||
oid_map = %{"1.3.6.1.2.1.25.3.3.1.2.1" => "98.6"}
|
||||
opts = Replay.new(oid_map)
|
||||
|
||||
assert {:ok, 98.6} = Replay.get(opts, "1.3.6.1.2.1.25.3.3.1.2.1")
|
||||
end
|
||||
|
||||
test "parses OID list values" do
|
||||
oid_map = %{"1.3.6.1.2.1.1.2.0" => "1.3.6.1.4.1.9.1.516"}
|
||||
opts = Replay.new(oid_map)
|
||||
|
||||
assert {:ok, [1, 3, 6, 1, 4, 1, 9, 1, 516]} =
|
||||
Replay.get(opts, "1.3.6.1.2.1.1.2.0")
|
||||
end
|
||||
|
||||
test "parses MAC address values" do
|
||||
oid_map = %{"1.3.6.1.2.1.2.2.1.6.1" => "aa:bb:cc:dd:ee:ff"}
|
||||
opts = Replay.new(oid_map)
|
||||
|
||||
assert {:ok, "aa:bb:cc:dd:ee:ff"} = Replay.get(opts, "1.3.6.1.2.1.2.2.1.6.1")
|
||||
end
|
||||
|
||||
test "parses IPv4 address values" do
|
||||
oid_map = %{"1.3.6.1.2.1.4.20.1.1.192.168.1.1" => "192.168.1.1"}
|
||||
opts = Replay.new(oid_map)
|
||||
|
||||
assert {:ok, "192.168.1.1"} = Replay.get(opts, "1.3.6.1.2.1.4.20.1.1.192.168.1.1")
|
||||
end
|
||||
|
||||
test "keeps string values as strings" do
|
||||
oid_map = %{"1.3.6.1.2.1.1.5.0" => "router-01.example.com"}
|
||||
opts = Replay.new(oid_map)
|
||||
|
||||
assert {:ok, "router-01.example.com"} = Replay.get(opts, "1.3.6.1.2.1.1.5.0")
|
||||
end
|
||||
end
|
||||
|
||||
describe "walk/2" do
|
||||
test "returns all child OIDs under base" do
|
||||
oid_map = %{
|
||||
"1.3.6.1.2.1.2.2.1.1.1" => "1",
|
||||
"1.3.6.1.2.1.2.2.1.1.2" => "2",
|
||||
"1.3.6.1.2.1.2.2.1.2.1" => "GigabitEthernet0/1",
|
||||
"1.3.6.1.2.1.2.2.1.2.2" => "GigabitEthernet0/2"
|
||||
}
|
||||
|
||||
opts = Replay.new(oid_map)
|
||||
|
||||
assert {:ok, results} = Replay.walk(opts, "1.3.6.1.2.1.2.2.1.1")
|
||||
|
||||
assert length(results) == 2
|
||||
assert {"1.3.6.1.2.1.2.2.1.1.1", 1} in results
|
||||
assert {"1.3.6.1.2.1.2.2.1.1.2", 2} in results
|
||||
end
|
||||
|
||||
test "returns empty list when no children exist" do
|
||||
oid_map = %{"1.3.6.1.2.1.1.1.0" => "Cisco IOS Software"}
|
||||
opts = Replay.new(oid_map)
|
||||
|
||||
assert {:ok, []} = Replay.walk(opts, "1.3.6.1.2.1.99")
|
||||
end
|
||||
|
||||
test "does not include base OID itself" do
|
||||
oid_map = %{
|
||||
"1.3.6.1.2.1.2.2.1.1" => "base",
|
||||
"1.3.6.1.2.1.2.2.1.1.1" => "child"
|
||||
}
|
||||
|
||||
opts = Replay.new(oid_map)
|
||||
|
||||
assert {:ok, results} = Replay.walk(opts, "1.3.6.1.2.1.2.2.1.1")
|
||||
|
||||
assert length(results) == 1
|
||||
assert {"1.3.6.1.2.1.2.2.1.1.1", "child"} in results
|
||||
refute {"1.3.6.1.2.1.2.2.1.1", "base"} in results
|
||||
end
|
||||
|
||||
test "returns results in lexicographic order" do
|
||||
oid_map = %{
|
||||
"1.3.6.1.2.1.2.2.1.1.10" => "10",
|
||||
"1.3.6.1.2.1.2.2.1.1.2" => "2",
|
||||
"1.3.6.1.2.1.2.2.1.1.1" => "1"
|
||||
}
|
||||
|
||||
opts = Replay.new(oid_map)
|
||||
|
||||
assert {:ok, results} = Replay.walk(opts, "1.3.6.1.2.1.2.2.1.1")
|
||||
|
||||
# Extract OIDs
|
||||
oids = Enum.map(results, fn {oid, _} -> oid end)
|
||||
|
||||
# Should be sorted: 1, 2, 10 (not "1", "10", "2" as strings)
|
||||
assert oids == [
|
||||
"1.3.6.1.2.1.2.2.1.1.1",
|
||||
"1.3.6.1.2.1.2.2.1.1.2",
|
||||
"1.3.6.1.2.1.2.2.1.1.10"
|
||||
]
|
||||
end
|
||||
|
||||
test "parses values in walk results" do
|
||||
oid_map = %{
|
||||
"1.3.6.1.2.1.2.2.1.1.1" => "42",
|
||||
"1.3.6.1.2.1.2.2.1.1.2" => "98.6",
|
||||
"1.3.6.1.2.1.2.2.1.1.3" => "192.168.1.1"
|
||||
}
|
||||
|
||||
opts = Replay.new(oid_map)
|
||||
|
||||
assert {:ok, results} = Replay.walk(opts, "1.3.6.1.2.1.2.2.1.1")
|
||||
|
||||
assert {"1.3.6.1.2.1.2.2.1.1.1", 42} in results
|
||||
assert {"1.3.6.1.2.1.2.2.1.1.2", 98.6} in results
|
||||
assert {"1.3.6.1.2.1.2.2.1.1.3", "192.168.1.1"} in results
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_multiple/2" do
|
||||
test "returns all requested OIDs" do
|
||||
oid_map = %{
|
||||
"1.3.6.1.2.1.1.1.0" => "Cisco IOS Software",
|
||||
"1.3.6.1.2.1.1.5.0" => "router-01",
|
||||
"1.3.6.1.2.1.1.6.0" => "Data Center 1"
|
||||
}
|
||||
|
||||
opts = Replay.new(oid_map)
|
||||
|
||||
oids = [
|
||||
"1.3.6.1.2.1.1.1.0",
|
||||
"1.3.6.1.2.1.1.5.0",
|
||||
"1.3.6.1.2.1.1.6.0"
|
||||
]
|
||||
|
||||
assert {:ok, results} = Replay.get_multiple(opts, oids)
|
||||
|
||||
assert results["1.3.6.1.2.1.1.1.0"] == "Cisco IOS Software"
|
||||
assert results["1.3.6.1.2.1.1.5.0"] == "router-01"
|
||||
assert results["1.3.6.1.2.1.1.6.0"] == "Data Center 1"
|
||||
end
|
||||
|
||||
test "returns nil for non-existent OIDs" do
|
||||
oid_map = %{"1.3.6.1.2.1.1.1.0" => "Cisco IOS Software"}
|
||||
opts = Replay.new(oid_map)
|
||||
|
||||
oids = [
|
||||
"1.3.6.1.2.1.1.1.0",
|
||||
"1.3.6.1.2.1.99.99.99"
|
||||
]
|
||||
|
||||
assert {:ok, results} = Replay.get_multiple(opts, oids)
|
||||
|
||||
assert results["1.3.6.1.2.1.1.1.0"] == "Cisco IOS Software"
|
||||
assert results["1.3.6.1.2.1.99.99.99"] == nil
|
||||
end
|
||||
|
||||
test "works with empty OID list" do
|
||||
opts = Replay.new(%{})
|
||||
|
||||
assert {:ok, %{}} = Replay.get_multiple(opts, [])
|
||||
end
|
||||
|
||||
test "parses values correctly" do
|
||||
oid_map = %{
|
||||
"1.3.6.1.2.1.2.2.1.1.1" => "42",
|
||||
"1.3.6.1.2.1.2.2.1.2.1" => "GigabitEthernet0/1"
|
||||
}
|
||||
|
||||
opts = Replay.new(oid_map)
|
||||
|
||||
oids = [
|
||||
"1.3.6.1.2.1.2.2.1.1.1",
|
||||
"1.3.6.1.2.1.2.2.1.2.1"
|
||||
]
|
||||
|
||||
assert {:ok, results} = Replay.get_multiple(opts, oids)
|
||||
|
||||
assert results["1.3.6.1.2.1.2.2.1.1.1"] == 42
|
||||
assert results["1.3.6.1.2.1.2.2.1.2.1"] == "GigabitEthernet0/1"
|
||||
end
|
||||
end
|
||||
|
||||
describe "type inference edge cases" do
|
||||
test "parses MAC address with hyphens" do
|
||||
oid_map = %{"oid" => "AA-BB-CC-DD-EE-FF"}
|
||||
opts = Replay.new(oid_map)
|
||||
|
||||
assert {:ok, "AA-BB-CC-DD-EE-FF"} = Replay.get(opts, "oid")
|
||||
end
|
||||
|
||||
test "treats 4-component dotted number as IPv4, not OID" do
|
||||
# Ambiguous case: "1.2.3.4" could be IPv4 or OID, we choose IPv4
|
||||
oid_map = %{"oid" => "1.2.3.4"}
|
||||
opts = Replay.new(oid_map)
|
||||
|
||||
assert {:ok, "1.2.3.4"} = Replay.get(opts, "oid")
|
||||
end
|
||||
|
||||
test "parses 5-component dotted number as OID list" do
|
||||
oid_map = %{"oid" => "1.2.3.4.5"}
|
||||
opts = Replay.new(oid_map)
|
||||
|
||||
assert {:ok, [1, 2, 3, 4, 5]} = Replay.get(opts, "oid")
|
||||
end
|
||||
|
||||
test "parses negative float" do
|
||||
oid_map = %{"oid" => "-12.5"}
|
||||
opts = Replay.new(oid_map)
|
||||
|
||||
assert {:ok, -12.5} = Replay.get(opts, "oid")
|
||||
end
|
||||
|
||||
test "keeps mixed alphanumeric as string" do
|
||||
oid_map = %{"oid" => "GigabitEthernet0/1"}
|
||||
opts = Replay.new(oid_map)
|
||||
|
||||
assert {:ok, "GigabitEthernet0/1"} = Replay.get(opts, "oid")
|
||||
end
|
||||
|
||||
test "handles zero value" do
|
||||
oid_map = %{"oid" => "0"}
|
||||
opts = Replay.new(oid_map)
|
||||
|
||||
assert {:ok, 0} = Replay.get(opts, "oid")
|
||||
end
|
||||
|
||||
test "handles large integer" do
|
||||
oid_map = %{"oid" => "4294967295"}
|
||||
opts = Replay.new(oid_map)
|
||||
|
||||
assert {:ok, 4_294_967_295} = Replay.get(opts, "oid")
|
||||
end
|
||||
end
|
||||
end
|
||||
234
test/towerops/snmp/processor_reading_test.exs
Normal file
234
test/towerops/snmp/processor_reading_test.exs
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
defmodule Towerops.Snmp.ProcessorReadingTest do
|
||||
use Towerops.DataCase, async: true
|
||||
|
||||
import Towerops.AccountsFixtures
|
||||
|
||||
alias Towerops.Snmp.ProcessorReading
|
||||
|
||||
describe "changeset/2" do
|
||||
setup do
|
||||
processor = insert_processor()
|
||||
%{processor: processor}
|
||||
end
|
||||
|
||||
test "valid changeset with all fields", %{processor: processor} do
|
||||
attrs = %{
|
||||
processor_id: processor.id,
|
||||
load_percent: 45.5,
|
||||
status: "ok",
|
||||
checked_at: DateTime.utc_now()
|
||||
}
|
||||
|
||||
changeset = ProcessorReading.changeset(%ProcessorReading{}, attrs)
|
||||
|
||||
assert changeset.valid?
|
||||
assert get_change(changeset, :processor_id) == processor.id
|
||||
assert get_change(changeset, :load_percent) == 45.5
|
||||
assert get_change(changeset, :status) == "ok"
|
||||
end
|
||||
|
||||
test "valid changeset without optional load_percent", %{processor: processor} do
|
||||
attrs = %{
|
||||
processor_id: processor.id,
|
||||
status: "ok",
|
||||
checked_at: DateTime.utc_now()
|
||||
}
|
||||
|
||||
changeset = ProcessorReading.changeset(%ProcessorReading{}, attrs)
|
||||
|
||||
assert changeset.valid?
|
||||
refute get_change(changeset, :load_percent)
|
||||
end
|
||||
|
||||
test "valid changeset with different status values", %{processor: processor} do
|
||||
for status <- ["ok", "warning", "critical", "error"] do
|
||||
attrs = %{
|
||||
processor_id: processor.id,
|
||||
status: status,
|
||||
checked_at: DateTime.utc_now()
|
||||
}
|
||||
|
||||
changeset = ProcessorReading.changeset(%ProcessorReading{}, attrs)
|
||||
|
||||
assert changeset.valid?, "Status #{status} should be valid"
|
||||
end
|
||||
end
|
||||
|
||||
test "invalid changeset when processor_id is missing" do
|
||||
attrs = %{
|
||||
status: "ok",
|
||||
checked_at: DateTime.utc_now()
|
||||
}
|
||||
|
||||
changeset = ProcessorReading.changeset(%ProcessorReading{}, attrs)
|
||||
|
||||
refute changeset.valid?
|
||||
assert %{processor_id: ["can't be blank"]} = errors_on(changeset)
|
||||
end
|
||||
|
||||
test "invalid changeset when status is missing", %{processor: processor} do
|
||||
attrs = %{
|
||||
processor_id: processor.id,
|
||||
checked_at: DateTime.utc_now()
|
||||
}
|
||||
|
||||
changeset = ProcessorReading.changeset(%ProcessorReading{}, attrs)
|
||||
|
||||
refute changeset.valid?
|
||||
assert %{status: ["can't be blank"]} = errors_on(changeset)
|
||||
end
|
||||
|
||||
test "invalid changeset when checked_at is missing", %{processor: processor} do
|
||||
attrs = %{
|
||||
processor_id: processor.id,
|
||||
status: "ok"
|
||||
}
|
||||
|
||||
changeset = ProcessorReading.changeset(%ProcessorReading{}, attrs)
|
||||
|
||||
refute changeset.valid?
|
||||
assert %{checked_at: ["can't be blank"]} = errors_on(changeset)
|
||||
end
|
||||
|
||||
test "invalid changeset with invalid status value", %{processor: processor} do
|
||||
attrs = %{
|
||||
processor_id: processor.id,
|
||||
status: "invalid_status",
|
||||
checked_at: DateTime.utc_now()
|
||||
}
|
||||
|
||||
changeset = ProcessorReading.changeset(%ProcessorReading{}, attrs)
|
||||
|
||||
refute changeset.valid?
|
||||
assert %{status: ["is invalid"]} = errors_on(changeset)
|
||||
end
|
||||
|
||||
test "invalid changeset with non-existent processor_id" do
|
||||
attrs = %{
|
||||
processor_id: Ecto.UUID.generate(),
|
||||
status: "ok",
|
||||
checked_at: DateTime.utc_now()
|
||||
}
|
||||
|
||||
changeset = ProcessorReading.changeset(%ProcessorReading{}, attrs)
|
||||
|
||||
assert changeset.valid?
|
||||
# Foreign key constraint will be checked at insert time
|
||||
assert {:error, changeset} = Repo.insert(changeset)
|
||||
assert %{processor_id: ["does not exist"]} = errors_on(changeset)
|
||||
end
|
||||
end
|
||||
|
||||
describe "database operations" do
|
||||
setup do
|
||||
processor = insert_processor()
|
||||
%{processor: processor}
|
||||
end
|
||||
|
||||
test "inserts valid processor reading", %{processor: processor} do
|
||||
attrs = %{
|
||||
processor_id: processor.id,
|
||||
load_percent: 75.5,
|
||||
status: "warning",
|
||||
checked_at: DateTime.utc_now()
|
||||
}
|
||||
|
||||
changeset = ProcessorReading.changeset(%ProcessorReading{}, attrs)
|
||||
assert {:ok, reading} = Repo.insert(changeset)
|
||||
|
||||
assert reading.processor_id == processor.id
|
||||
assert reading.load_percent == 75.5
|
||||
assert reading.status == "warning"
|
||||
assert reading.inserted_at
|
||||
end
|
||||
|
||||
test "inserts reading without load_percent", %{processor: processor} do
|
||||
attrs = %{
|
||||
processor_id: processor.id,
|
||||
status: "error",
|
||||
checked_at: DateTime.utc_now()
|
||||
}
|
||||
|
||||
changeset = ProcessorReading.changeset(%ProcessorReading{}, attrs)
|
||||
assert {:ok, reading} = Repo.insert(changeset)
|
||||
|
||||
assert is_nil(reading.load_percent)
|
||||
assert reading.status == "error"
|
||||
end
|
||||
|
||||
test "timestamps are set automatically", %{processor: processor} do
|
||||
attrs = %{
|
||||
processor_id: processor.id,
|
||||
status: "ok",
|
||||
checked_at: DateTime.utc_now()
|
||||
}
|
||||
|
||||
changeset = ProcessorReading.changeset(%ProcessorReading{}, attrs)
|
||||
assert {:ok, reading} = Repo.insert(changeset)
|
||||
|
||||
assert reading.inserted_at
|
||||
# updated_at should be false according to schema
|
||||
refute Map.has_key?(reading, :updated_at)
|
||||
end
|
||||
end
|
||||
|
||||
# Test helper to create a processor
|
||||
defp insert_processor do
|
||||
snmp_device = insert_snmp_device()
|
||||
|
||||
Repo.insert!(%Towerops.Snmp.Processor{
|
||||
snmp_device_id: snmp_device.id,
|
||||
processor_index: "1",
|
||||
processor_type: "hr_processor",
|
||||
load_percent: 0.0
|
||||
})
|
||||
end
|
||||
|
||||
defp insert_snmp_device do
|
||||
device = insert_device()
|
||||
|
||||
Repo.insert!(%Towerops.Snmp.Device{
|
||||
device_id: device.id,
|
||||
sys_descr: "Test Device",
|
||||
sys_object_id: "1.3.6.1.4.1.9",
|
||||
sys_name: "test-device"
|
||||
})
|
||||
end
|
||||
|
||||
defp insert_device do
|
||||
org = insert_organization()
|
||||
site = insert_site(org)
|
||||
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Test Device",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id
|
||||
})
|
||||
|
||||
device
|
||||
end
|
||||
|
||||
defp insert_organization do
|
||||
user = insert_user()
|
||||
|
||||
{:ok, org} =
|
||||
Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)
|
||||
|
||||
org
|
||||
end
|
||||
|
||||
defp insert_site(org) do
|
||||
{:ok, site} =
|
||||
Towerops.Sites.create_site(%{
|
||||
name: "Test Site",
|
||||
organization_id: org.id
|
||||
})
|
||||
|
||||
site
|
||||
end
|
||||
|
||||
defp insert_user do
|
||||
user_fixture()
|
||||
end
|
||||
end
|
||||
231
test/towerops/workers/backup_summary_worker_test.exs
Normal file
231
test/towerops/workers/backup_summary_worker_test.exs
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
defmodule Towerops.Workers.BackupSummaryWorkerTest do
|
||||
use Towerops.DataCase, async: false
|
||||
|
||||
import ExUnit.CaptureLog
|
||||
import Towerops.AccountsFixtures
|
||||
|
||||
alias Towerops.Devices
|
||||
alias Towerops.Devices.BackupRequests
|
||||
alias Towerops.Organizations
|
||||
alias Towerops.Sites
|
||||
alias Towerops.Workers.BackupSummaryWorker
|
||||
|
||||
require Logger
|
||||
|
||||
setup do
|
||||
# Ensure Logger level is set to info for capturing logs
|
||||
original_level = Logger.level()
|
||||
Logger.configure(level: :info)
|
||||
|
||||
on_exit(fn ->
|
||||
Logger.configure(level: original_level)
|
||||
end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
describe "perform/1" do
|
||||
test "returns :ok when no backup requests exist" do
|
||||
# Execute the worker
|
||||
assert :ok = BackupSummaryWorker.perform(%Oban.Job{args: %{}})
|
||||
end
|
||||
|
||||
test "logs summary when only successful backups exist" do
|
||||
device = insert_device()
|
||||
|
||||
# Create successful requests
|
||||
{:ok, req} = BackupRequests.create_request(device.id, "job1")
|
||||
BackupRequests.update_request_status(req.id, "success", nil)
|
||||
|
||||
# Execute the worker and capture logs
|
||||
log =
|
||||
capture_log(fn ->
|
||||
assert :ok = BackupSummaryWorker.perform(%Oban.Job{args: %{}})
|
||||
end)
|
||||
|
||||
# Verify summary was logged
|
||||
assert log =~ "Backup summary for past 24 hours"
|
||||
refute log =~ "Backup failures detected"
|
||||
end
|
||||
|
||||
test "logs warning when failed backups exist" do
|
||||
device = insert_device()
|
||||
|
||||
# Create a failed request
|
||||
{:ok, req} = BackupRequests.create_request(device.id, "job_failed")
|
||||
BackupRequests.update_request_status(req.id, "failed", "Connection error")
|
||||
|
||||
# Execute the worker and capture logs
|
||||
log =
|
||||
capture_log(fn ->
|
||||
assert :ok = BackupSummaryWorker.perform(%Oban.Job{args: %{}})
|
||||
end)
|
||||
|
||||
# Verify warning was logged
|
||||
assert log =~ "Backup summary for past 24 hours"
|
||||
assert log =~ "Backup failures detected"
|
||||
end
|
||||
|
||||
test "logs warning when timeout backups exist" do
|
||||
device = insert_device()
|
||||
|
||||
# Create a timeout request
|
||||
{:ok, req} = BackupRequests.create_request(device.id, "job_timeout")
|
||||
BackupRequests.update_request_status(req.id, "timeout", "Request timed out")
|
||||
|
||||
# Execute the worker and capture logs
|
||||
log =
|
||||
capture_log(fn ->
|
||||
assert :ok = BackupSummaryWorker.perform(%Oban.Job{args: %{}})
|
||||
end)
|
||||
|
||||
# Verify warning was logged
|
||||
assert log =~ "Backup summary for past 24 hours"
|
||||
assert log =~ "Backup failures detected"
|
||||
end
|
||||
|
||||
test "logs warning when both failed and timeout backups exist" do
|
||||
device = insert_device()
|
||||
|
||||
# Create a failed request
|
||||
{:ok, req1} = BackupRequests.create_request(device.id, "job_failed")
|
||||
BackupRequests.update_request_status(req1.id, "failed", "Connection error")
|
||||
|
||||
# Create a timeout request
|
||||
{:ok, req2} = BackupRequests.create_request(device.id, "job_timeout")
|
||||
BackupRequests.update_request_status(req2.id, "timeout", "Request timed out")
|
||||
|
||||
# Execute the worker and capture logs
|
||||
log =
|
||||
capture_log(fn ->
|
||||
assert :ok = BackupSummaryWorker.perform(%Oban.Job{args: %{}})
|
||||
end)
|
||||
|
||||
# Verify warning was logged
|
||||
assert log =~ "Backup summary for past 24 hours"
|
||||
assert log =~ "Backup failures detected"
|
||||
end
|
||||
|
||||
test "includes device IDs in warning log" do
|
||||
device1 = insert_device()
|
||||
device2 = insert_device()
|
||||
|
||||
# Create failed requests for multiple devices
|
||||
{:ok, req1} = BackupRequests.create_request(device1.id, "job1")
|
||||
BackupRequests.update_request_status(req1.id, "failed", "Error")
|
||||
|
||||
{:ok, req2} = BackupRequests.create_request(device2.id, "job2")
|
||||
BackupRequests.update_request_status(req2.id, "timeout", "Timeout")
|
||||
|
||||
# Execute the worker and capture logs
|
||||
log =
|
||||
capture_log(fn ->
|
||||
assert :ok = BackupSummaryWorker.perform(%Oban.Job{args: %{}})
|
||||
end)
|
||||
|
||||
# Verify warnings were logged (metadata fields like device IDs aren't captured in text logs)
|
||||
assert log =~ "Backup summary for past 24 hours"
|
||||
assert log =~ "Backup failures detected"
|
||||
end
|
||||
|
||||
test "only includes requests from past 24 hours" do
|
||||
device = insert_device()
|
||||
|
||||
# Create an old failed request (30 hours ago)
|
||||
{:ok, old_req} = BackupRequests.create_request(device.id, "job_old")
|
||||
|
||||
old_req
|
||||
|> Ecto.Changeset.change(
|
||||
requested_at: DateTime.utc_now() |> DateTime.add(-30 * 60 * 60, :second) |> DateTime.truncate(:second)
|
||||
)
|
||||
|> Repo.update!()
|
||||
|
||||
BackupRequests.update_request_status(old_req.id, "failed", "Old error")
|
||||
|
||||
# Create a recent failed request (12 hours ago)
|
||||
{:ok, recent_req} = BackupRequests.create_request(device.id, "job_recent")
|
||||
|
||||
recent_req
|
||||
|> Ecto.Changeset.change(
|
||||
requested_at: DateTime.utc_now() |> DateTime.add(-12 * 60 * 60, :second) |> DateTime.truncate(:second)
|
||||
)
|
||||
|> Repo.update!()
|
||||
|
||||
BackupRequests.update_request_status(recent_req.id, "failed", "Recent error")
|
||||
|
||||
# Execute the worker and capture logs
|
||||
log =
|
||||
capture_log(fn ->
|
||||
assert :ok = BackupSummaryWorker.perform(%Oban.Job{args: %{}})
|
||||
end)
|
||||
|
||||
# Verify summary and failure warning are logged (only recent failure should be counted)
|
||||
assert log =~ "Backup summary for past 24 hours"
|
||||
assert log =~ "Backup failures detected"
|
||||
end
|
||||
|
||||
test "handles mixed statuses correctly" do
|
||||
device = insert_device()
|
||||
|
||||
# Create requests with various statuses
|
||||
{:ok, req1} = BackupRequests.create_request(device.id, "job_success")
|
||||
BackupRequests.update_request_status(req1.id, "success", nil)
|
||||
|
||||
{:ok, req2} = BackupRequests.create_request(device.id, "job_failed")
|
||||
BackupRequests.update_request_status(req2.id, "failed", "Error")
|
||||
|
||||
{:ok, req3} = BackupRequests.create_request(device.id, "job_timeout")
|
||||
BackupRequests.update_request_status(req3.id, "timeout", "Timeout")
|
||||
|
||||
{:ok, _req4} = BackupRequests.create_request(device.id, "job_pending")
|
||||
# Leave as pending
|
||||
|
||||
# Execute the worker and capture logs
|
||||
log =
|
||||
capture_log(fn ->
|
||||
assert :ok = BackupSummaryWorker.perform(%Oban.Job{args: %{}})
|
||||
end)
|
||||
|
||||
# Verify summary and failure warnings are logged
|
||||
assert log =~ "Backup summary for past 24 hours"
|
||||
assert log =~ "Backup failures detected"
|
||||
end
|
||||
|
||||
test "does not log warning when all backups are successful or pending" do
|
||||
device = insert_device()
|
||||
|
||||
# Create successful and pending requests
|
||||
{:ok, req1} = BackupRequests.create_request(device.id, "job_success")
|
||||
BackupRequests.update_request_status(req1.id, "success", nil)
|
||||
|
||||
{:ok, _req2} = BackupRequests.create_request(device.id, "job_pending")
|
||||
# Leave as pending
|
||||
|
||||
# Execute the worker and capture logs
|
||||
log =
|
||||
capture_log(fn ->
|
||||
assert :ok = BackupSummaryWorker.perform(%Oban.Job{args: %{}})
|
||||
end)
|
||||
|
||||
# Verify no warning was logged
|
||||
assert log =~ "Backup summary for past 24 hours"
|
||||
refute log =~ "Backup failures detected"
|
||||
end
|
||||
end
|
||||
|
||||
# Helper function to create a device
|
||||
defp insert_device do
|
||||
user = user_fixture()
|
||||
{:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)
|
||||
{:ok, site} = Sites.create_site(%{name: "Test Site", organization_id: organization.id})
|
||||
|
||||
{:ok, device} =
|
||||
Devices.create_device(%{
|
||||
name: "Test Device #{System.unique_integer()}",
|
||||
ip_address: "192.168.1.#{:rand.uniform(254)}",
|
||||
site_id: site.id
|
||||
})
|
||||
|
||||
device
|
||||
end
|
||||
end
|
||||
205
test/towerops/workers/backup_timeout_worker_test.exs
Normal file
205
test/towerops/workers/backup_timeout_worker_test.exs
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
defmodule Towerops.Workers.BackupTimeoutWorkerTest do
|
||||
use Towerops.DataCase, async: true
|
||||
|
||||
import Towerops.AccountsFixtures
|
||||
|
||||
alias Towerops.Devices
|
||||
alias Towerops.Devices.BackupRequests
|
||||
alias Towerops.Organizations
|
||||
alias Towerops.Sites
|
||||
alias Towerops.Workers.BackupTimeoutWorker
|
||||
|
||||
describe "perform/1" do
|
||||
test "marks pending requests older than 5 minutes as timeout" do
|
||||
device = insert_device()
|
||||
|
||||
# Create a pending request from 10 minutes ago (older than 5 minute timeout)
|
||||
{:ok, old_request} = BackupRequests.create_request(device.id, "job_old")
|
||||
|
||||
old_request
|
||||
|> Ecto.Changeset.change(
|
||||
requested_at: DateTime.utc_now() |> DateTime.add(-600, :second) |> DateTime.truncate(:second)
|
||||
)
|
||||
|> Repo.update!()
|
||||
|
||||
# Execute the worker
|
||||
assert :ok = BackupTimeoutWorker.perform(%Oban.Job{args: %{}})
|
||||
|
||||
# Verify request was marked as timeout
|
||||
updated = Repo.reload(old_request)
|
||||
assert updated.status == "timeout"
|
||||
assert updated.error_message == "Request timed out"
|
||||
assert updated.completed_at
|
||||
end
|
||||
|
||||
test "does not mark recent pending requests" do
|
||||
device = insert_device()
|
||||
|
||||
# Create a pending request from 2 minutes ago (within 5 minute timeout)
|
||||
{:ok, recent_request} = BackupRequests.create_request(device.id, "job_recent")
|
||||
|
||||
recent_request
|
||||
|> Ecto.Changeset.change(
|
||||
requested_at: DateTime.utc_now() |> DateTime.add(-120, :second) |> DateTime.truncate(:second)
|
||||
)
|
||||
|> Repo.update!()
|
||||
|
||||
# Execute the worker
|
||||
assert :ok = BackupTimeoutWorker.perform(%Oban.Job{args: %{}})
|
||||
|
||||
# Verify request still pending
|
||||
updated = Repo.reload(recent_request)
|
||||
assert updated.status == "pending"
|
||||
assert is_nil(updated.completed_at)
|
||||
end
|
||||
|
||||
test "does not mark completed requests" do
|
||||
device = insert_device()
|
||||
|
||||
# Create an old request that already succeeded
|
||||
{:ok, old_request} = BackupRequests.create_request(device.id, "job_old")
|
||||
|
||||
old_request
|
||||
|> Ecto.Changeset.change(
|
||||
requested_at: DateTime.utc_now() |> DateTime.add(-600, :second) |> DateTime.truncate(:second)
|
||||
)
|
||||
|> Repo.update!()
|
||||
|
||||
{:ok, _} = BackupRequests.update_request_status(old_request.id, "success", nil)
|
||||
|
||||
# Execute the worker
|
||||
assert :ok = BackupTimeoutWorker.perform(%Oban.Job{args: %{}})
|
||||
|
||||
# Verify request still has success status
|
||||
updated = Repo.reload(old_request)
|
||||
assert updated.status == "success"
|
||||
end
|
||||
|
||||
test "handles multiple requests with mixed statuses" do
|
||||
device = insert_device()
|
||||
|
||||
# Create old pending request (should timeout)
|
||||
{:ok, old_pending} = BackupRequests.create_request(device.id, "job_old_pending")
|
||||
|
||||
old_pending
|
||||
|> Ecto.Changeset.change(
|
||||
requested_at: DateTime.utc_now() |> DateTime.add(-600, :second) |> DateTime.truncate(:second)
|
||||
)
|
||||
|> Repo.update!()
|
||||
|
||||
# Create recent pending request (should stay pending)
|
||||
{:ok, recent_pending} = BackupRequests.create_request(device.id, "job_recent_pending")
|
||||
|
||||
recent_pending
|
||||
|> Ecto.Changeset.change(
|
||||
requested_at: DateTime.utc_now() |> DateTime.add(-120, :second) |> DateTime.truncate(:second)
|
||||
)
|
||||
|> Repo.update!()
|
||||
|
||||
# Create old completed request (should stay success)
|
||||
{:ok, old_success} = BackupRequests.create_request(device.id, "job_old_success")
|
||||
|
||||
old_success
|
||||
|> Ecto.Changeset.change(
|
||||
requested_at: DateTime.utc_now() |> DateTime.add(-600, :second) |> DateTime.truncate(:second)
|
||||
)
|
||||
|> Repo.update!()
|
||||
|
||||
{:ok, _} = BackupRequests.update_request_status(old_success.id, "success", nil)
|
||||
|
||||
# Execute the worker
|
||||
assert :ok = BackupTimeoutWorker.perform(%Oban.Job{args: %{}})
|
||||
|
||||
# Verify only old pending was marked as timeout
|
||||
assert Repo.reload(old_pending).status == "timeout"
|
||||
assert Repo.reload(recent_pending).status == "pending"
|
||||
assert Repo.reload(old_success).status == "success"
|
||||
end
|
||||
|
||||
test "handles boundary case at exactly 5 minutes" do
|
||||
device = insert_device()
|
||||
|
||||
# Create a request at exactly 5 minutes (300 seconds) ago
|
||||
{:ok, boundary_request} = BackupRequests.create_request(device.id, "job_boundary")
|
||||
|
||||
boundary_request
|
||||
|> Ecto.Changeset.change(
|
||||
requested_at: DateTime.utc_now() |> DateTime.add(-300, :second) |> DateTime.truncate(:second)
|
||||
)
|
||||
|> Repo.update!()
|
||||
|
||||
# Execute the worker
|
||||
assert :ok = BackupTimeoutWorker.perform(%Oban.Job{args: %{}})
|
||||
|
||||
# The cutoff uses `< cutoff`, so exactly 5 minutes should NOT timeout
|
||||
updated = Repo.reload(boundary_request)
|
||||
assert updated.status == "pending"
|
||||
end
|
||||
|
||||
test "returns :ok when no requests need timeout" do
|
||||
device = insert_device()
|
||||
|
||||
# Create only recent request
|
||||
{:ok, recent_request} = BackupRequests.create_request(device.id, "job_recent")
|
||||
|
||||
recent_request
|
||||
|> Ecto.Changeset.change(
|
||||
requested_at: DateTime.utc_now() |> DateTime.add(-60, :second) |> DateTime.truncate(:second)
|
||||
)
|
||||
|> Repo.update!()
|
||||
|
||||
# Execute the worker
|
||||
assert :ok = BackupTimeoutWorker.perform(%Oban.Job{args: %{}})
|
||||
end
|
||||
|
||||
test "returns :ok when no requests exist at all" do
|
||||
# Execute the worker with no requests in the database
|
||||
assert :ok = BackupTimeoutWorker.perform(%Oban.Job{args: %{}})
|
||||
end
|
||||
|
||||
test "handles multiple devices with stale requests" do
|
||||
device1 = insert_device()
|
||||
device2 = insert_device()
|
||||
|
||||
# Create old pending requests for both devices
|
||||
{:ok, request1} = BackupRequests.create_request(device1.id, "job1")
|
||||
|
||||
request1
|
||||
|> Ecto.Changeset.change(
|
||||
requested_at: DateTime.utc_now() |> DateTime.add(-600, :second) |> DateTime.truncate(:second)
|
||||
)
|
||||
|> Repo.update!()
|
||||
|
||||
{:ok, request2} = BackupRequests.create_request(device2.id, "job2")
|
||||
|
||||
request2
|
||||
|> Ecto.Changeset.change(
|
||||
requested_at: DateTime.utc_now() |> DateTime.add(-700, :second) |> DateTime.truncate(:second)
|
||||
)
|
||||
|> Repo.update!()
|
||||
|
||||
# Execute the worker
|
||||
assert :ok = BackupTimeoutWorker.perform(%Oban.Job{args: %{}})
|
||||
|
||||
# Verify both were marked as timeout
|
||||
assert Repo.reload(request1).status == "timeout"
|
||||
assert Repo.reload(request2).status == "timeout"
|
||||
end
|
||||
end
|
||||
|
||||
# Helper function to create a device
|
||||
defp insert_device do
|
||||
user = user_fixture()
|
||||
{:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)
|
||||
{:ok, site} = Sites.create_site(%{name: "Test Site", organization_id: organization.id})
|
||||
|
||||
{:ok, device} =
|
||||
Devices.create_device(%{
|
||||
name: "Test Device #{System.unique_integer()}",
|
||||
ip_address: "192.168.1.#{:rand.uniform(254)}",
|
||||
site_id: site.id
|
||||
})
|
||||
|
||||
device
|
||||
end
|
||||
end
|
||||
238
test/towerops/workers/login_history_cleanup_worker_test.exs
Normal file
238
test/towerops/workers/login_history_cleanup_worker_test.exs
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
defmodule Towerops.Workers.LoginHistoryCleanupWorkerTest do
|
||||
use Towerops.DataCase, async: true
|
||||
|
||||
alias Towerops.Accounts.LoginAttempt
|
||||
alias Towerops.Workers.LoginHistoryCleanupWorker
|
||||
|
||||
describe "perform/1" do
|
||||
test "deletes active login attempts older than retention period" do
|
||||
# Create an old active record (400 days old, past default 365 retention)
|
||||
old_attempt =
|
||||
insert_login_attempt(%{
|
||||
inserted_at: DateTime.add(DateTime.utc_now(), -400, :day),
|
||||
anonymized_at: nil
|
||||
})
|
||||
|
||||
# Execute the worker
|
||||
assert {:ok, %{deleted: 1, anonymized_deleted: 0}} =
|
||||
LoginHistoryCleanupWorker.perform(%Oban.Job{args: %{}})
|
||||
|
||||
# Verify old active record was deleted
|
||||
refute Repo.get(LoginAttempt, old_attempt.id)
|
||||
end
|
||||
|
||||
test "keeps active login attempts within retention period" do
|
||||
# Create a recent active record (100 days old, within default 365 retention)
|
||||
recent_attempt =
|
||||
insert_login_attempt(%{
|
||||
inserted_at: DateTime.add(DateTime.utc_now(), -100, :day),
|
||||
anonymized_at: nil
|
||||
})
|
||||
|
||||
# Execute the worker
|
||||
assert {:ok, %{deleted: 0, anonymized_deleted: 0}} =
|
||||
LoginHistoryCleanupWorker.perform(%Oban.Job{args: %{}})
|
||||
|
||||
# Verify recent active record still exists
|
||||
assert Repo.get(LoginAttempt, recent_attempt.id)
|
||||
end
|
||||
|
||||
test "deletes anonymized records older than 90 days" do
|
||||
# Create an anonymized record (100 days after anonymization)
|
||||
anon_date = DateTime.add(DateTime.utc_now(), -100, :day)
|
||||
|
||||
old_anon_attempt =
|
||||
insert_login_attempt(%{
|
||||
inserted_at: DateTime.add(anon_date, -200, :day),
|
||||
anonymized_at: anon_date
|
||||
})
|
||||
|
||||
# Execute the worker
|
||||
assert {:ok, %{deleted: 0, anonymized_deleted: 1}} =
|
||||
LoginHistoryCleanupWorker.perform(%Oban.Job{args: %{}})
|
||||
|
||||
# Verify old anonymized record was deleted
|
||||
refute Repo.get(LoginAttempt, old_anon_attempt.id)
|
||||
end
|
||||
|
||||
test "keeps anonymized records within 90 day period" do
|
||||
# Create an anonymized record (30 days after anonymization)
|
||||
anon_date = DateTime.add(DateTime.utc_now(), -30, :day)
|
||||
|
||||
recent_anon_attempt =
|
||||
insert_login_attempt(%{
|
||||
inserted_at: DateTime.add(anon_date, -200, :day),
|
||||
anonymized_at: anon_date
|
||||
})
|
||||
|
||||
# Execute the worker
|
||||
assert {:ok, %{deleted: 0, anonymized_deleted: 0}} =
|
||||
LoginHistoryCleanupWorker.perform(%Oban.Job{args: %{}})
|
||||
|
||||
# Verify recent anonymized record still exists
|
||||
assert Repo.get(LoginAttempt, recent_anon_attempt.id)
|
||||
end
|
||||
|
||||
test "handles mixed active and anonymized records" do
|
||||
# Create old active record (should be deleted)
|
||||
old_active =
|
||||
insert_login_attempt(%{
|
||||
inserted_at: DateTime.add(DateTime.utc_now(), -400, :day),
|
||||
anonymized_at: nil
|
||||
})
|
||||
|
||||
# Create recent active record (should be kept)
|
||||
recent_active =
|
||||
insert_login_attempt(%{
|
||||
inserted_at: DateTime.add(DateTime.utc_now(), -100, :day),
|
||||
anonymized_at: nil
|
||||
})
|
||||
|
||||
# Create old anonymized record (should be deleted)
|
||||
old_anon =
|
||||
insert_login_attempt(%{
|
||||
inserted_at: DateTime.add(DateTime.utc_now(), -500, :day),
|
||||
anonymized_at: DateTime.add(DateTime.utc_now(), -100, :day)
|
||||
})
|
||||
|
||||
# Create recent anonymized record (should be kept)
|
||||
recent_anon =
|
||||
insert_login_attempt(%{
|
||||
inserted_at: DateTime.add(DateTime.utc_now(), -400, :day),
|
||||
anonymized_at: DateTime.add(DateTime.utc_now(), -30, :day)
|
||||
})
|
||||
|
||||
# Execute the worker
|
||||
assert {:ok, %{deleted: 1, anonymized_deleted: 1}} =
|
||||
LoginHistoryCleanupWorker.perform(%Oban.Job{args: %{}})
|
||||
|
||||
# Verify old records were deleted
|
||||
refute Repo.get(LoginAttempt, old_active.id)
|
||||
refute Repo.get(LoginAttempt, old_anon.id)
|
||||
|
||||
# Verify recent records still exist
|
||||
assert Repo.get(LoginAttempt, recent_active.id)
|
||||
assert Repo.get(LoginAttempt, recent_anon.id)
|
||||
end
|
||||
|
||||
test "returns zero counts when no records need deletion" do
|
||||
# Create only recent records
|
||||
insert_login_attempt(%{
|
||||
inserted_at: DateTime.add(DateTime.utc_now(), -10, :day),
|
||||
anonymized_at: nil
|
||||
})
|
||||
|
||||
# Execute the worker
|
||||
assert {:ok, %{deleted: 0, anonymized_deleted: 0}} =
|
||||
LoginHistoryCleanupWorker.perform(%Oban.Job{args: %{}})
|
||||
end
|
||||
|
||||
test "works when no records exist at all" do
|
||||
# Execute the worker with no records in the database
|
||||
assert {:ok, %{deleted: 0, anonymized_deleted: 0}} =
|
||||
LoginHistoryCleanupWorker.perform(%Oban.Job{args: %{}})
|
||||
end
|
||||
|
||||
test "respects custom retention period from config" do
|
||||
# Temporarily override config (if Application.put_env works in tests)
|
||||
# Otherwise, this test documents expected behavior
|
||||
|
||||
# Create a record 200 days old (would be deleted if retention is 180 days)
|
||||
old_attempt =
|
||||
insert_login_attempt(%{
|
||||
inserted_at: DateTime.add(DateTime.utc_now(), -200, :day),
|
||||
anonymized_at: nil
|
||||
})
|
||||
|
||||
# With default 365 day retention, this should NOT be deleted
|
||||
assert {:ok, %{deleted: 0, anonymized_deleted: 0}} =
|
||||
LoginHistoryCleanupWorker.perform(%Oban.Job{args: %{}})
|
||||
|
||||
# Verify record still exists
|
||||
assert Repo.get(LoginAttempt, old_attempt.id)
|
||||
end
|
||||
|
||||
test "handles boundary case at exactly retention cutoff" do
|
||||
# Create a record at exactly 365 days old
|
||||
cutoff_date = DateTime.add(DateTime.utc_now(), -365, :day)
|
||||
|
||||
boundary_attempt =
|
||||
insert_login_attempt(%{
|
||||
inserted_at: cutoff_date,
|
||||
anonymized_at: nil
|
||||
})
|
||||
|
||||
# Execute the worker
|
||||
assert {:ok, %{deleted: count, anonymized_deleted: _}} =
|
||||
LoginHistoryCleanupWorker.perform(%Oban.Job{args: %{}})
|
||||
|
||||
# The query uses `<` not `<=`, so exactly at cutoff should NOT be deleted
|
||||
assert count == 0
|
||||
assert Repo.get(LoginAttempt, boundary_attempt.id)
|
||||
end
|
||||
|
||||
test "handles boundary case for anonymized at exactly 90 days" do
|
||||
# Create an anonymized record at exactly 90 days
|
||||
anon_date = DateTime.add(DateTime.utc_now(), -90, :day)
|
||||
|
||||
boundary_anon =
|
||||
insert_login_attempt(%{
|
||||
inserted_at: DateTime.add(anon_date, -100, :day),
|
||||
anonymized_at: anon_date
|
||||
})
|
||||
|
||||
# Execute the worker
|
||||
assert {:ok, %{deleted: _, anonymized_deleted: anon_count}} =
|
||||
LoginHistoryCleanupWorker.perform(%Oban.Job{args: %{}})
|
||||
|
||||
# The query uses `<` not `<=`, so exactly at 90 days should NOT be deleted
|
||||
assert anon_count == 0
|
||||
assert Repo.get(LoginAttempt, boundary_anon.id)
|
||||
end
|
||||
end
|
||||
|
||||
# Helper function to create login attempts
|
||||
defp insert_login_attempt(attrs) do
|
||||
default_attrs = %{
|
||||
email: "test#{System.unique_integer()}@example.com",
|
||||
success: true,
|
||||
method: "password",
|
||||
ip_address: "192.168.1.1"
|
||||
}
|
||||
|
||||
attrs =
|
||||
default_attrs
|
||||
|> Map.merge(attrs)
|
||||
|> Map.update(:inserted_at, DateTime.utc_now(), fn dt -> dt end)
|
||||
|
||||
# Truncate datetime values to remove microseconds (Ecto requirement)
|
||||
inserted_at = attrs |> Map.get(:inserted_at) |> DateTime.truncate(:second)
|
||||
|
||||
anonymized_at =
|
||||
case Map.get(attrs, :anonymized_at) do
|
||||
nil -> nil
|
||||
dt -> DateTime.truncate(dt, :second)
|
||||
end
|
||||
|
||||
# Remove timestamp fields from attrs (not castable)
|
||||
attrs_for_changeset =
|
||||
attrs
|
||||
|> Map.delete(:inserted_at)
|
||||
|> Map.delete(:anonymized_at)
|
||||
|
||||
changeset =
|
||||
%LoginAttempt{}
|
||||
|> LoginAttempt.changeset(attrs_for_changeset)
|
||||
|> Ecto.Changeset.put_change(:inserted_at, inserted_at)
|
||||
|
||||
# Add anonymized_at if provided
|
||||
changeset =
|
||||
if anonymized_at do
|
||||
Ecto.Changeset.put_change(changeset, :anonymized_at, anonymized_at)
|
||||
else
|
||||
changeset
|
||||
end
|
||||
|
||||
Repo.insert!(changeset)
|
||||
end
|
||||
end
|
||||
153
test/towerops/workers/session_cleanup_worker_test.exs
Normal file
153
test/towerops/workers/session_cleanup_worker_test.exs
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
defmodule Towerops.Workers.SessionCleanupWorkerTest do
|
||||
use Towerops.DataCase, async: true
|
||||
|
||||
import Towerops.AccountsFixtures
|
||||
|
||||
alias Towerops.Accounts
|
||||
alias Towerops.Accounts.BrowserSession
|
||||
alias Towerops.Workers.SessionCleanupWorker
|
||||
|
||||
describe "perform/1" do
|
||||
test "deletes expired browser sessions" do
|
||||
user = user_fixture()
|
||||
{_token, user_token} = Accounts.generate_user_session_token_with_record(user)
|
||||
|
||||
# Create an expired session (1 day ago)
|
||||
expired_session =
|
||||
insert_browser_session(%{
|
||||
user_id: user.id,
|
||||
user_token_id: user_token.id,
|
||||
expires_at: DateTime.add(DateTime.utc_now(), -1, :day)
|
||||
})
|
||||
|
||||
# Execute the worker
|
||||
assert {:ok, %{sessions_deleted: 1}} =
|
||||
SessionCleanupWorker.perform(%Oban.Job{args: %{}})
|
||||
|
||||
# Verify expired session was deleted
|
||||
refute Repo.get(BrowserSession, expired_session.id)
|
||||
end
|
||||
|
||||
test "keeps non-expired browser sessions" do
|
||||
user = user_fixture()
|
||||
{_token, user_token} = Accounts.generate_user_session_token_with_record(user)
|
||||
|
||||
# Create a valid session (expires in 1 day)
|
||||
valid_session =
|
||||
insert_browser_session(%{
|
||||
user_id: user.id,
|
||||
user_token_id: user_token.id,
|
||||
expires_at: DateTime.add(DateTime.utc_now(), 1, :day)
|
||||
})
|
||||
|
||||
# Execute the worker
|
||||
assert {:ok, %{sessions_deleted: 0}} =
|
||||
SessionCleanupWorker.perform(%Oban.Job{args: %{}})
|
||||
|
||||
# Verify valid session still exists
|
||||
assert Repo.get(BrowserSession, valid_session.id)
|
||||
end
|
||||
|
||||
test "deletes only expired sessions among mixed sessions" do
|
||||
user = user_fixture()
|
||||
{_token1, user_token1} = Accounts.generate_user_session_token_with_record(user)
|
||||
{_token2, user_token2} = Accounts.generate_user_session_token_with_record(user)
|
||||
{_token3, user_token3} = Accounts.generate_user_session_token_with_record(user)
|
||||
|
||||
# Create 2 expired sessions
|
||||
expired1 =
|
||||
insert_browser_session(%{
|
||||
user_id: user.id,
|
||||
user_token_id: user_token1.id,
|
||||
expires_at: DateTime.add(DateTime.utc_now(), -2, :day)
|
||||
})
|
||||
|
||||
expired2 =
|
||||
insert_browser_session(%{
|
||||
user_id: user.id,
|
||||
user_token_id: user_token2.id,
|
||||
expires_at: DateTime.add(DateTime.utc_now(), -1, :hour)
|
||||
})
|
||||
|
||||
# Create 1 valid session
|
||||
valid_session =
|
||||
insert_browser_session(%{
|
||||
user_id: user.id,
|
||||
user_token_id: user_token3.id,
|
||||
expires_at: DateTime.add(DateTime.utc_now(), 7, :day)
|
||||
})
|
||||
|
||||
# Execute the worker
|
||||
assert {:ok, %{sessions_deleted: 2}} =
|
||||
SessionCleanupWorker.perform(%Oban.Job{args: %{}})
|
||||
|
||||
# Verify expired sessions were deleted
|
||||
refute Repo.get(BrowserSession, expired1.id)
|
||||
refute Repo.get(BrowserSession, expired2.id)
|
||||
|
||||
# Verify valid session still exists
|
||||
assert Repo.get(BrowserSession, valid_session.id)
|
||||
end
|
||||
|
||||
test "returns zero count when no expired sessions exist" do
|
||||
user = user_fixture()
|
||||
{_token, user_token} = Accounts.generate_user_session_token_with_record(user)
|
||||
|
||||
# Create only valid sessions
|
||||
insert_browser_session(%{
|
||||
user_id: user.id,
|
||||
user_token_id: user_token.id,
|
||||
expires_at: DateTime.add(DateTime.utc_now(), 1, :day)
|
||||
})
|
||||
|
||||
# Execute the worker
|
||||
assert {:ok, %{sessions_deleted: 0}} =
|
||||
SessionCleanupWorker.perform(%Oban.Job{args: %{}})
|
||||
end
|
||||
|
||||
test "works when no sessions exist at all" do
|
||||
# Execute the worker with no sessions in the database
|
||||
assert {:ok, %{sessions_deleted: 0}} =
|
||||
SessionCleanupWorker.perform(%Oban.Job{args: %{}})
|
||||
end
|
||||
|
||||
test "handles sessions that expired exactly now" do
|
||||
user = user_fixture()
|
||||
{_token, user_token} = Accounts.generate_user_session_token_with_record(user)
|
||||
|
||||
now = DateTime.utc_now()
|
||||
|
||||
# Create a session that expires exactly now (should be considered expired)
|
||||
expired_now =
|
||||
insert_browser_session(%{
|
||||
user_id: user.id,
|
||||
user_token_id: user_token.id,
|
||||
expires_at: now
|
||||
})
|
||||
|
||||
# Execute the worker
|
||||
assert {:ok, %{sessions_deleted: count}} =
|
||||
SessionCleanupWorker.perform(%Oban.Job{args: %{}})
|
||||
|
||||
# The session should be deleted (expires_at < now, not <=)
|
||||
# Actually, checking the code: where([bs], bs.expires_at < ^now)
|
||||
# So expires_at == now should NOT be deleted
|
||||
assert count == 0
|
||||
assert Repo.get(BrowserSession, expired_now.id)
|
||||
end
|
||||
end
|
||||
|
||||
# Helper function to create browser sessions
|
||||
defp insert_browser_session(attrs) do
|
||||
default_attrs = %{
|
||||
ip_address: "192.168.1.1",
|
||||
last_activity_at: DateTime.utc_now()
|
||||
}
|
||||
|
||||
attrs = Map.merge(default_attrs, attrs)
|
||||
|
||||
%BrowserSession{}
|
||||
|> BrowserSession.create_changeset(attrs)
|
||||
|> Repo.insert!()
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue