more tests
This commit is contained in:
parent
5b84844e97
commit
1ce3763e5f
8 changed files with 2047 additions and 7 deletions
|
|
@ -81,7 +81,7 @@ defmodule Mix.Tasks.ImportProfiles do
|
|||
end
|
||||
|
||||
defp import_all_profiles(source_path) do
|
||||
Logger.info("Discovering YAML files in #{source_path}...")
|
||||
Mix.shell().info("Discovering YAML files in #{source_path}...")
|
||||
|
||||
detection_path = Path.join(source_path, "os_detection")
|
||||
|
||||
|
|
@ -91,29 +91,29 @@ defmodule Mix.Tasks.ImportProfiles do
|
|||
|> Path.wildcard()
|
||||
|> Enum.sort()
|
||||
|
||||
Logger.info("Found #{length(yaml_files)} YAML files")
|
||||
Mix.shell().info("Found #{length(yaml_files)} YAML files")
|
||||
|
||||
profile_names = Enum.map(yaml_files, &Path.basename(&1, ".yaml"))
|
||||
import_profiles_by_name(source_path, profile_names)
|
||||
end
|
||||
|
||||
defp import_specific_profiles(source_path, profiles) do
|
||||
Logger.info("Importing #{length(profiles)} specified profiles")
|
||||
Mix.shell().info("Importing #{length(profiles)} specified profiles")
|
||||
import_profiles_by_name(source_path, profiles)
|
||||
end
|
||||
|
||||
defp import_profiles_by_name(source_path, profile_names) do
|
||||
results =
|
||||
Enum.map(profile_names, fn profile_name ->
|
||||
Logger.info("Importing profile: #{profile_name}")
|
||||
Mix.shell().info("Importing profile: #{profile_name}")
|
||||
|
||||
case import_profile(source_path, profile_name) do
|
||||
{:ok, _profile} ->
|
||||
Logger.info("✓ #{profile_name}: created/updated successfully")
|
||||
Mix.shell().info("✓ #{profile_name}: created/updated successfully")
|
||||
{:ok, profile_name}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("✗ #{profile_name}: #{inspect(reason)}")
|
||||
Mix.shell().error("✗ #{profile_name}: #{inspect(reason)}")
|
||||
{:error, profile_name, reason}
|
||||
end
|
||||
end)
|
||||
|
|
@ -121,7 +121,7 @@ defmodule Mix.Tasks.ImportProfiles do
|
|||
successes = Enum.count(results, &match?({:ok, _}, &1))
|
||||
failures = Enum.count(results, &match?({:error, _, _}, &1))
|
||||
|
||||
Logger.info("Import complete: #{successes} succeeded, #{failures} failed")
|
||||
Mix.shell().info("Import complete: #{successes} succeeded, #{failures} failed")
|
||||
end
|
||||
|
||||
defp import_profile(source_path, profile_name) do
|
||||
|
|
|
|||
|
|
@ -40,6 +40,18 @@ defmodule Mix.Tasks.Oban.CancelStuckDiscovery do
|
|||
|
||||
1
|
||||
|
||||
:ok ->
|
||||
# Some versions of Oban return :ok instead of {:ok, job}
|
||||
Logger.info(
|
||||
"Cancelled stuck discovery job",
|
||||
job_id: job.id,
|
||||
device_id: get_in(job.args, ["device_id"]),
|
||||
state: job.state,
|
||||
attempted_at: job.attempted_at
|
||||
)
|
||||
|
||||
1
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error(
|
||||
"Failed to cancel job",
|
||||
|
|
|
|||
473
test/mix/tasks/gen_vendor_modules_test.exs
Normal file
473
test/mix/tasks/gen_vendor_modules_test.exs
Normal file
|
|
@ -0,0 +1,473 @@
|
|||
defmodule Mix.Tasks.GenVendorModulesTest do
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
import ExUnit.CaptureIO
|
||||
|
||||
alias Mix.Tasks.GenVendorModules
|
||||
|
||||
@moduletag :mix_task
|
||||
|
||||
setup do
|
||||
# Save original directory
|
||||
original_dir = File.cwd!()
|
||||
|
||||
# Create temporary directories for profiles and vendor modules
|
||||
temp_dir = Path.join(System.tmp_dir!(), "gen_vendor_modules_#{System.unique_integer([:positive])}")
|
||||
profiles_dir = Path.join([temp_dir, "priv", "profiles", "os_detection"])
|
||||
vendors_dir = Path.join([temp_dir, "lib", "towerops", "snmp", "profiles", "vendors"])
|
||||
File.mkdir_p!(profiles_dir)
|
||||
File.mkdir_p!(vendors_dir)
|
||||
|
||||
on_exit(fn ->
|
||||
File.cd!(original_dir)
|
||||
File.rm_rf!(temp_dir)
|
||||
end)
|
||||
|
||||
{:ok, temp_dir: temp_dir, profiles_dir: profiles_dir, vendors_dir: vendors_dir}
|
||||
end
|
||||
|
||||
describe "run/1 with dry-run" do
|
||||
test "reports vendors that would be generated in dry-run mode", %{
|
||||
temp_dir: temp_dir,
|
||||
profiles_dir: profiles_dir,
|
||||
vendors_dir: _vendors_dir
|
||||
} do
|
||||
# Create profile YAML with group
|
||||
File.write!(Path.join(profiles_dir, "mikrotik.yaml"), """
|
||||
os: mikrotik
|
||||
group: mikrotik
|
||||
text: MikroTik
|
||||
discovery:
|
||||
- sysObjectID:
|
||||
- .1.3.6.1.4.1.14988.
|
||||
""")
|
||||
|
||||
File.cd!(temp_dir, fn ->
|
||||
output =
|
||||
capture_io(fn ->
|
||||
GenVendorModules.run(["--dry-run"])
|
||||
end)
|
||||
|
||||
assert output =~ "Found 1 YAML profiles"
|
||||
assert output =~ "Grouped into 1 vendors"
|
||||
assert output =~ "1 new vendors to generate"
|
||||
assert output =~ "mikrotik.ex (Mikrotik)"
|
||||
assert output =~ "Profiles: mikrotik"
|
||||
assert output =~ "Enterprise OID: 1.3.6.1.4.1.14988"
|
||||
end)
|
||||
end
|
||||
|
||||
test "reports no work needed when all vendors exist", %{
|
||||
profiles_dir: profiles_dir,
|
||||
vendors_dir: vendors_dir,
|
||||
temp_dir: temp_dir
|
||||
} do
|
||||
# Create profile YAML
|
||||
File.write!(Path.join(profiles_dir, "mikrotik.yaml"), """
|
||||
os: mikrotik
|
||||
group: mikrotik
|
||||
text: MikroTik
|
||||
""")
|
||||
|
||||
# Create existing vendor module
|
||||
File.write!(Path.join(vendors_dir, "mikrotik.ex"), """
|
||||
defmodule Towerops.Snmp.Profiles.Vendors.Mikrotik do
|
||||
def profile_names, do: ["mikrotik"]
|
||||
end
|
||||
""")
|
||||
|
||||
File.cd!(temp_dir, fn ->
|
||||
output =
|
||||
capture_io(fn ->
|
||||
GenVendorModules.run(["--dry-run"])
|
||||
end)
|
||||
|
||||
assert output =~ "0 new vendors to generate"
|
||||
assert output =~ "No new vendors to generate!"
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
describe "run/1 without dry-run" do
|
||||
test "generates vendor module from profile with group", %{
|
||||
profiles_dir: profiles_dir,
|
||||
vendors_dir: vendors_dir,
|
||||
temp_dir: temp_dir
|
||||
} do
|
||||
File.write!(Path.join(profiles_dir, "cisco-ios.yaml"), """
|
||||
os: cisco-ios
|
||||
group: cisco
|
||||
text: Cisco IOS
|
||||
discovery:
|
||||
- sysObjectID:
|
||||
- .1.3.6.1.4.1.9.
|
||||
""")
|
||||
|
||||
File.cd!(temp_dir, fn ->
|
||||
output =
|
||||
capture_io(fn ->
|
||||
GenVendorModules.run([])
|
||||
end)
|
||||
|
||||
assert output =~ "Generated: lib/towerops/snmp/profiles/vendors/cisco.ex"
|
||||
assert output =~ "Generated 1 vendor modules"
|
||||
|
||||
# Verify file was created
|
||||
vendor_file = Path.join(vendors_dir, "cisco.ex")
|
||||
assert File.exists?(vendor_file)
|
||||
|
||||
# Verify file content
|
||||
content = File.read!(vendor_file)
|
||||
assert content =~ "defmodule Towerops.Snmp.Profiles.Vendors.Cisco do"
|
||||
assert content =~ "@moduledoc"
|
||||
assert content =~ "SNMP profile for Cisco devices"
|
||||
assert content =~ "@behaviour Towerops.Snmp.Profiles.Vendors.Vendor"
|
||||
assert content =~ "def profile_names, do: [\"cisco-ios\"]"
|
||||
assert content =~ "# Enterprise OID: 1.3.6.1.4.1.9"
|
||||
assert content =~ "def detect_hardware(client_opts)"
|
||||
assert content =~ "def wireless_oid_defs"
|
||||
assert content =~ "def discover_wireless_sensors(client_opts)"
|
||||
end)
|
||||
end
|
||||
|
||||
test "generates vendor module from profile without group using filename inference", %{
|
||||
temp_dir: temp_dir,
|
||||
profiles_dir: profiles_dir,
|
||||
vendors_dir: vendors_dir
|
||||
} do
|
||||
File.write!(Path.join(profiles_dir, "cisco_ios.yaml"), """
|
||||
os: cisco-ios
|
||||
text: Cisco IOS
|
||||
""")
|
||||
|
||||
File.cd!(temp_dir, fn ->
|
||||
output =
|
||||
capture_io(fn ->
|
||||
GenVendorModules.run([])
|
||||
end)
|
||||
|
||||
# Should infer cisco from filename
|
||||
assert output =~ "Generated: lib/towerops/snmp/profiles/vendors/cisco.ex"
|
||||
|
||||
vendor_file = Path.join(vendors_dir, "cisco.ex")
|
||||
assert File.exists?(vendor_file)
|
||||
end)
|
||||
end
|
||||
|
||||
test "groups multiple profiles into single vendor module", %{
|
||||
profiles_dir: profiles_dir,
|
||||
vendors_dir: vendors_dir,
|
||||
temp_dir: temp_dir
|
||||
} do
|
||||
File.write!(Path.join(profiles_dir, "cisco-ios.yaml"), """
|
||||
os: cisco-ios
|
||||
group: cisco
|
||||
text: Cisco IOS
|
||||
""")
|
||||
|
||||
File.write!(Path.join(profiles_dir, "cisco-nxos.yaml"), """
|
||||
os: cisco-nxos
|
||||
group: cisco
|
||||
text: Cisco NX-OS
|
||||
""")
|
||||
|
||||
File.write!(Path.join(profiles_dir, "cisco-asa.yaml"), """
|
||||
os: cisco-asa
|
||||
group: cisco
|
||||
text: Cisco ASA
|
||||
""")
|
||||
|
||||
File.cd!(temp_dir, fn ->
|
||||
output =
|
||||
capture_io(fn ->
|
||||
GenVendorModules.run([])
|
||||
end)
|
||||
|
||||
assert output =~ "Generated 1 vendor modules"
|
||||
|
||||
vendor_file = Path.join(vendors_dir, "cisco.ex")
|
||||
content = File.read!(vendor_file)
|
||||
|
||||
# Should include all three profiles
|
||||
assert content =~ ~s(["cisco-asa", "cisco-ios", "cisco-nxos"])
|
||||
end)
|
||||
end
|
||||
|
||||
test "generates modules for multiple vendors", %{
|
||||
profiles_dir: profiles_dir,
|
||||
vendors_dir: vendors_dir,
|
||||
temp_dir: temp_dir
|
||||
} do
|
||||
File.write!(Path.join(profiles_dir, "mikrotik.yaml"), """
|
||||
os: mikrotik
|
||||
group: mikrotik
|
||||
text: MikroTik
|
||||
""")
|
||||
|
||||
File.write!(Path.join(profiles_dir, "ubnt.yaml"), """
|
||||
os: ubnt
|
||||
group: ubiquiti
|
||||
text: Ubiquiti
|
||||
""")
|
||||
|
||||
File.cd!(temp_dir, fn ->
|
||||
output =
|
||||
capture_io(fn ->
|
||||
GenVendorModules.run([])
|
||||
end)
|
||||
|
||||
assert output =~ "Generated 2 vendor modules"
|
||||
assert File.exists?(Path.join(vendors_dir, "mikrotik.ex"))
|
||||
assert File.exists?(Path.join(vendors_dir, "ubiquiti.ex"))
|
||||
end)
|
||||
end
|
||||
|
||||
test "generates module with specific group filter", %{
|
||||
profiles_dir: profiles_dir,
|
||||
vendors_dir: vendors_dir,
|
||||
temp_dir: temp_dir
|
||||
} do
|
||||
File.write!(Path.join(profiles_dir, "mikrotik.yaml"), """
|
||||
os: mikrotik
|
||||
group: mikrotik
|
||||
text: MikroTik
|
||||
""")
|
||||
|
||||
File.write!(Path.join(profiles_dir, "cisco.yaml"), """
|
||||
os: cisco
|
||||
group: cisco
|
||||
text: Cisco
|
||||
""")
|
||||
|
||||
File.cd!(temp_dir, fn ->
|
||||
output =
|
||||
capture_io(fn ->
|
||||
GenVendorModules.run(["--group", "mikrotik"])
|
||||
end)
|
||||
|
||||
assert output =~ "Generated 1 vendor modules"
|
||||
assert File.exists?(Path.join(vendors_dir, "mikrotik.ex"))
|
||||
refute File.exists?(Path.join(vendors_dir, "cisco.ex"))
|
||||
end)
|
||||
end
|
||||
|
||||
test "handles vendor names starting with numbers", %{
|
||||
profiles_dir: profiles_dir,
|
||||
vendors_dir: vendors_dir,
|
||||
temp_dir: temp_dir
|
||||
} do
|
||||
File.write!(Path.join(profiles_dir, "4rf.yaml"), """
|
||||
os: 4rf
|
||||
group: 4rf
|
||||
text: 4RF
|
||||
""")
|
||||
|
||||
File.cd!(temp_dir, fn ->
|
||||
output =
|
||||
capture_io(fn ->
|
||||
GenVendorModules.run([])
|
||||
end)
|
||||
|
||||
# Should prefix with "n" for filename and "N" for module
|
||||
assert output =~ "Generated: lib/towerops/snmp/profiles/vendors/n4rf.ex"
|
||||
|
||||
vendor_file = Path.join(vendors_dir, "n4rf.ex")
|
||||
assert File.exists?(vendor_file)
|
||||
|
||||
content = File.read!(vendor_file)
|
||||
assert content =~ "defmodule Towerops.Snmp.Profiles.Vendors.N4rf do"
|
||||
end)
|
||||
end
|
||||
|
||||
test "handles vendor names with special characters", %{
|
||||
profiles_dir: profiles_dir,
|
||||
vendors_dir: vendors_dir,
|
||||
temp_dir: temp_dir
|
||||
} do
|
||||
File.write!(Path.join(profiles_dir, "device.yaml"), """
|
||||
os: test-device
|
||||
group: hp-aruba
|
||||
text: HP Aruba
|
||||
""")
|
||||
|
||||
File.cd!(temp_dir, fn ->
|
||||
output =
|
||||
capture_io(fn ->
|
||||
GenVendorModules.run([])
|
||||
end)
|
||||
|
||||
# Should convert to snake_case for filename
|
||||
assert output =~ "Generated: lib/towerops/snmp/profiles/vendors/hp_aruba.ex"
|
||||
|
||||
vendor_file = Path.join(vendors_dir, "hp_aruba.ex")
|
||||
assert File.exists?(vendor_file)
|
||||
|
||||
content = File.read!(vendor_file)
|
||||
assert content =~ "defmodule Towerops.Snmp.Profiles.Vendors.HpAruba do"
|
||||
end)
|
||||
end
|
||||
|
||||
test "extracts enterprise OID from sysObjectID", %{
|
||||
profiles_dir: profiles_dir,
|
||||
vendors_dir: vendors_dir,
|
||||
temp_dir: temp_dir
|
||||
} do
|
||||
File.write!(Path.join(profiles_dir, "test.yaml"), """
|
||||
os: test
|
||||
group: testvendor
|
||||
text: Test Vendor
|
||||
discovery:
|
||||
- sysObjectID:
|
||||
- .1.3.6.1.4.1.12345.1.2.3
|
||||
""")
|
||||
|
||||
File.cd!(temp_dir, fn ->
|
||||
capture_io(fn ->
|
||||
GenVendorModules.run([])
|
||||
end)
|
||||
|
||||
vendor_file = Path.join(vendors_dir, "testvendor.ex")
|
||||
content = File.read!(vendor_file)
|
||||
|
||||
assert content =~ "# Enterprise OID: 1.3.6.1.4.1.12345"
|
||||
end)
|
||||
end
|
||||
|
||||
test "handles profile without enterprise OID", %{
|
||||
profiles_dir: profiles_dir,
|
||||
vendors_dir: vendors_dir,
|
||||
temp_dir: temp_dir
|
||||
} do
|
||||
File.write!(Path.join(profiles_dir, "generic.yaml"), """
|
||||
os: generic
|
||||
group: generic
|
||||
text: Generic Device
|
||||
""")
|
||||
|
||||
File.cd!(temp_dir, fn ->
|
||||
capture_io(fn ->
|
||||
GenVendorModules.run([])
|
||||
end)
|
||||
|
||||
vendor_file = Path.join(vendors_dir, "generic.ex")
|
||||
content = File.read!(vendor_file)
|
||||
|
||||
assert content =~ "# Enterprise OID: unknown - uses standard MIB OIDs"
|
||||
end)
|
||||
end
|
||||
|
||||
test "includes standard CPU sensor in wireless_oid_defs", %{
|
||||
profiles_dir: profiles_dir,
|
||||
vendors_dir: vendors_dir,
|
||||
temp_dir: temp_dir
|
||||
} do
|
||||
File.write!(Path.join(profiles_dir, "test.yaml"), """
|
||||
os: test
|
||||
group: test
|
||||
text: Test
|
||||
""")
|
||||
|
||||
File.cd!(temp_dir, fn ->
|
||||
capture_io(fn ->
|
||||
GenVendorModules.run([])
|
||||
end)
|
||||
|
||||
vendor_file = Path.join(vendors_dir, "test.ex")
|
||||
content = File.read!(vendor_file)
|
||||
|
||||
assert content =~ "wireless_oid_defs do"
|
||||
assert content =~ "1.3.6.1.2.1.25.3.3.1.2.1"
|
||||
assert content =~ "CPU Load"
|
||||
assert content =~ "sensor_type: \"load\""
|
||||
end)
|
||||
end
|
||||
|
||||
test "skips profiles that cannot be grouped to a vendor", %{
|
||||
profiles_dir: profiles_dir,
|
||||
vendors_dir: _vendors_dir,
|
||||
temp_dir: temp_dir
|
||||
} do
|
||||
File.write!(Path.join(profiles_dir, "unknown_device.yaml"), """
|
||||
os: unknown-device
|
||||
text: Unknown Device
|
||||
""")
|
||||
|
||||
File.cd!(temp_dir, fn ->
|
||||
output =
|
||||
capture_io(fn ->
|
||||
GenVendorModules.run([])
|
||||
end)
|
||||
|
||||
assert output =~ "0 new vendors to generate"
|
||||
assert output =~ "No new vendors to generate!"
|
||||
end)
|
||||
end
|
||||
|
||||
test "includes instructions after generation", %{
|
||||
profiles_dir: profiles_dir,
|
||||
vendors_dir: _vendors_dir,
|
||||
temp_dir: temp_dir
|
||||
} do
|
||||
File.write!(Path.join(profiles_dir, "test.yaml"), """
|
||||
os: test
|
||||
group: test
|
||||
text: Test
|
||||
""")
|
||||
|
||||
File.cd!(temp_dir, fn ->
|
||||
output =
|
||||
capture_io(fn ->
|
||||
GenVendorModules.run([])
|
||||
end)
|
||||
|
||||
assert output =~ "Run `mix compile` to check for errors"
|
||||
assert output =~ "Run `mix test` to verify"
|
||||
assert output =~ "Add aliases to registry.ex"
|
||||
assert output =~ "Add vendors to @vendors list in registry.ex"
|
||||
assert output =~ "Write tests for each vendor"
|
||||
assert output =~ "Add vendor-specific OIDs if available"
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
describe "run/1 error handling" do
|
||||
test "skips profiles with invalid YAML", %{profiles_dir: profiles_dir, vendors_dir: vendors_dir, temp_dir: temp_dir} do
|
||||
File.write!(Path.join(profiles_dir, "valid.yaml"), """
|
||||
os: valid
|
||||
group: valid
|
||||
text: Valid
|
||||
""")
|
||||
|
||||
File.write!(Path.join(profiles_dir, "invalid.yaml"), """
|
||||
this is: [invalid yaml that won't parse
|
||||
""")
|
||||
|
||||
File.cd!(temp_dir, fn ->
|
||||
output =
|
||||
capture_io(fn ->
|
||||
GenVendorModules.run([])
|
||||
end)
|
||||
|
||||
# Should still generate valid vendor
|
||||
assert output =~ "Generated 1 vendor modules"
|
||||
assert File.exists?(Path.join(vendors_dir, "valid.ex"))
|
||||
end)
|
||||
end
|
||||
|
||||
test "skips profiles without os field", %{profiles_dir: profiles_dir, vendors_dir: _vendors_dir, temp_dir: temp_dir} do
|
||||
File.write!(Path.join(profiles_dir, "no_os.yaml"), """
|
||||
group: test
|
||||
text: Test
|
||||
""")
|
||||
|
||||
File.cd!(temp_dir, fn ->
|
||||
output =
|
||||
capture_io(fn ->
|
||||
GenVendorModules.run([])
|
||||
end)
|
||||
|
||||
assert output =~ "0 new vendors to generate"
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
335
test/mix/tasks/gen_vendor_tests_test.exs
Normal file
335
test/mix/tasks/gen_vendor_tests_test.exs
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
defmodule Mix.Tasks.GenVendorTestsTest do
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
import ExUnit.CaptureIO
|
||||
|
||||
alias Mix.Tasks.GenVendorTests
|
||||
|
||||
@moduletag :mix_task
|
||||
|
||||
setup do
|
||||
# Save original directory
|
||||
original_dir = File.cwd!()
|
||||
|
||||
# Create temporary directories for vendor modules and tests
|
||||
temp_dir = Path.join(System.tmp_dir!(), "vendor_tests_#{System.unique_integer([:positive])}")
|
||||
vendors_dir = Path.join(temp_dir, "lib/towerops/snmp/profiles/vendors")
|
||||
tests_dir = Path.join(temp_dir, "test/towerops/snmp/profiles/vendors")
|
||||
File.mkdir_p!(vendors_dir)
|
||||
File.mkdir_p!(tests_dir)
|
||||
|
||||
on_exit(fn ->
|
||||
File.cd!(original_dir)
|
||||
File.rm_rf!(temp_dir)
|
||||
end)
|
||||
|
||||
{:ok, temp_dir: temp_dir, vendors_dir: vendors_dir, tests_dir: tests_dir}
|
||||
end
|
||||
|
||||
describe "run/1 with dry-run" do
|
||||
test "reports vendors without tests in dry-run mode", %{
|
||||
vendors_dir: vendors_dir,
|
||||
tests_dir: _tests_dir,
|
||||
temp_dir: temp_dir
|
||||
} do
|
||||
# Create vendor module
|
||||
File.write!(Path.join(vendors_dir, "mikrotik.ex"), """
|
||||
defmodule Towerops.Snmp.Profiles.Vendors.Mikrotik do
|
||||
@behaviour Towerops.Snmp.Profiles.Vendors.Vendor
|
||||
|
||||
@impl true
|
||||
def profile_names, do: ["mikrotik"]
|
||||
end
|
||||
""")
|
||||
|
||||
# Change to temp directory to run task
|
||||
File.cd!(temp_dir, fn ->
|
||||
output =
|
||||
capture_io(fn ->
|
||||
GenVendorTests.run(["--dry-run"])
|
||||
end)
|
||||
|
||||
assert output =~ "Found 1 vendor modules"
|
||||
assert output =~ "Found 0 existing tests"
|
||||
assert output =~ "1 vendors need tests"
|
||||
assert output =~ "Would generate: test/towerops/snmp/profiles/vendors/mikrotik_test.exs"
|
||||
end)
|
||||
end
|
||||
|
||||
test "reports no work needed when all vendors have tests", %{
|
||||
vendors_dir: vendors_dir,
|
||||
tests_dir: tests_dir,
|
||||
temp_dir: temp_dir
|
||||
} do
|
||||
# Create vendor module
|
||||
File.write!(Path.join(vendors_dir, "mikrotik.ex"), """
|
||||
defmodule Towerops.Snmp.Profiles.Vendors.Mikrotik do
|
||||
def profile_names, do: ["mikrotik"]
|
||||
end
|
||||
""")
|
||||
|
||||
# Create corresponding test
|
||||
File.write!(Path.join(tests_dir, "mikrotik_test.exs"), """
|
||||
defmodule Towerops.Snmp.Profiles.Vendors.MikrotikTest do
|
||||
use ExUnit.Case
|
||||
end
|
||||
""")
|
||||
|
||||
File.cd!(temp_dir, fn ->
|
||||
output =
|
||||
capture_io(fn ->
|
||||
GenVendorTests.run(["--dry-run"])
|
||||
end)
|
||||
|
||||
assert output =~ "All vendor modules have tests!"
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
describe "run/1 without dry-run" do
|
||||
test "generates test file for vendor without test", %{
|
||||
vendors_dir: vendors_dir,
|
||||
tests_dir: tests_dir,
|
||||
temp_dir: temp_dir
|
||||
} do
|
||||
# Create vendor module with profile_names
|
||||
File.write!(Path.join(vendors_dir, "cisco.ex"), """
|
||||
defmodule Towerops.Snmp.Profiles.Vendors.Cisco do
|
||||
@behaviour Towerops.Snmp.Profiles.Vendors.Vendor
|
||||
|
||||
@impl true
|
||||
def profile_names, do: ["cisco-ios", "cisco-nxos"]
|
||||
end
|
||||
""")
|
||||
|
||||
File.cd!(temp_dir, fn ->
|
||||
output =
|
||||
capture_io(fn ->
|
||||
GenVendorTests.run([])
|
||||
end)
|
||||
|
||||
assert output =~ "Generated: test/towerops/snmp/profiles/vendors/cisco_test.exs"
|
||||
assert output =~ "Generated 1 test files"
|
||||
|
||||
# Verify test file was created
|
||||
test_file = Path.join(tests_dir, "cisco_test.exs")
|
||||
assert File.exists?(test_file)
|
||||
|
||||
content = File.read!(test_file)
|
||||
assert content =~ "defmodule Towerops.Snmp.Profiles.Vendors.CiscoTest do"
|
||||
assert content =~ "use Towerops.DataCase, async: true"
|
||||
assert content =~ "alias Towerops.Snmp.Profiles.Vendors.Cisco"
|
||||
assert content =~ ~s{profile_names() == ["cisco-ios", "cisco-nxos"]}
|
||||
assert content =~ "describe \"profile_names/0\" do"
|
||||
assert content =~ "describe \"detect_hardware/1\" do"
|
||||
assert content =~ "describe \"wireless_oid_defs/0\" do"
|
||||
assert content =~ "describe \"discover_wireless_sensors/1\" do"
|
||||
end)
|
||||
end
|
||||
|
||||
test "generates tests for multiple vendors", %{vendors_dir: vendors_dir, tests_dir: tests_dir, temp_dir: temp_dir} do
|
||||
# Create multiple vendor modules
|
||||
File.write!(Path.join(vendors_dir, "mikrotik.ex"), """
|
||||
defmodule Towerops.Snmp.Profiles.Vendors.Mikrotik do
|
||||
def profile_names, do: ["mikrotik"]
|
||||
end
|
||||
""")
|
||||
|
||||
File.write!(Path.join(vendors_dir, "ubiquiti.ex"), """
|
||||
defmodule Towerops.Snmp.Profiles.Vendors.Ubiquiti do
|
||||
def profile_names, do: ["ubnt"]
|
||||
end
|
||||
""")
|
||||
|
||||
File.write!(Path.join(vendors_dir, "cisco.ex"), """
|
||||
defmodule Towerops.Snmp.Profiles.Vendors.Cisco do
|
||||
def profile_names, do: ["cisco"]
|
||||
end
|
||||
""")
|
||||
|
||||
File.cd!(temp_dir, fn ->
|
||||
output =
|
||||
capture_io(fn ->
|
||||
GenVendorTests.run([])
|
||||
end)
|
||||
|
||||
assert output =~ "Generated 3 test files"
|
||||
assert File.exists?(Path.join(tests_dir, "mikrotik_test.exs"))
|
||||
assert File.exists?(Path.join(tests_dir, "ubiquiti_test.exs"))
|
||||
assert File.exists?(Path.join(tests_dir, "cisco_test.exs"))
|
||||
end)
|
||||
end
|
||||
|
||||
test "skips vendors that already have tests", %{vendors_dir: vendors_dir, tests_dir: tests_dir, temp_dir: temp_dir} do
|
||||
# Create vendor modules
|
||||
File.write!(Path.join(vendors_dir, "mikrotik.ex"), """
|
||||
defmodule Towerops.Snmp.Profiles.Vendors.Mikrotik do
|
||||
def profile_names, do: ["mikrotik"]
|
||||
end
|
||||
""")
|
||||
|
||||
File.write!(Path.join(vendors_dir, "cisco.ex"), """
|
||||
defmodule Towerops.Snmp.Profiles.Vendors.Cisco do
|
||||
def profile_names, do: ["cisco"]
|
||||
end
|
||||
""")
|
||||
|
||||
# Create test for mikrotik (but not cisco)
|
||||
File.write!(Path.join(tests_dir, "mikrotik_test.exs"), """
|
||||
defmodule Towerops.Snmp.Profiles.Vendors.MikrotikTest do
|
||||
use ExUnit.Case
|
||||
end
|
||||
""")
|
||||
|
||||
File.cd!(temp_dir, fn ->
|
||||
output =
|
||||
capture_io(fn ->
|
||||
GenVendorTests.run([])
|
||||
end)
|
||||
|
||||
assert output =~ "Found 2 vendor modules"
|
||||
assert output =~ "Found 1 existing tests"
|
||||
assert output =~ "1 vendors need tests"
|
||||
assert output =~ "Generated 1 test files"
|
||||
|
||||
# Only cisco test should be generated
|
||||
assert File.exists?(Path.join(tests_dir, "cisco_test.exs"))
|
||||
|
||||
# Mikrotik test should be the old one (not regenerated)
|
||||
mikrotik_test = File.read!(Path.join(tests_dir, "mikrotik_test.exs"))
|
||||
assert mikrotik_test =~ "use ExUnit.Case"
|
||||
refute mikrotik_test =~ "use Towerops.DataCase"
|
||||
end)
|
||||
end
|
||||
|
||||
test "excludes registry and vendor files from generation", %{
|
||||
vendors_dir: vendors_dir,
|
||||
tests_dir: tests_dir,
|
||||
temp_dir: temp_dir
|
||||
} do
|
||||
# Create vendor modules
|
||||
File.write!(Path.join(vendors_dir, "registry.ex"), """
|
||||
defmodule Towerops.Snmp.Profiles.Vendors.Registry do
|
||||
end
|
||||
""")
|
||||
|
||||
File.write!(Path.join(vendors_dir, "vendor.ex"), """
|
||||
defmodule Towerops.Snmp.Profiles.Vendors.Vendor do
|
||||
end
|
||||
""")
|
||||
|
||||
File.write!(Path.join(vendors_dir, "mikrotik.ex"), """
|
||||
defmodule Towerops.Snmp.Profiles.Vendors.Mikrotik do
|
||||
def profile_names, do: ["mikrotik"]
|
||||
end
|
||||
""")
|
||||
|
||||
File.cd!(temp_dir, fn ->
|
||||
output =
|
||||
capture_io(fn ->
|
||||
GenVendorTests.run([])
|
||||
end)
|
||||
|
||||
# Should only count mikrotik as vendor module
|
||||
assert output =~ "Found 1 vendor modules"
|
||||
assert output =~ "Generated 1 test files"
|
||||
|
||||
# Only mikrotik test should be generated
|
||||
assert File.exists?(Path.join(tests_dir, "mikrotik_test.exs"))
|
||||
refute File.exists?(Path.join(tests_dir, "registry_test.exs"))
|
||||
refute File.exists?(Path.join(tests_dir, "vendor_test.exs"))
|
||||
end)
|
||||
end
|
||||
|
||||
test "handles vendor module without profile_names function", %{
|
||||
vendors_dir: vendors_dir,
|
||||
tests_dir: tests_dir,
|
||||
temp_dir: temp_dir
|
||||
} do
|
||||
# Create vendor module without profile_names
|
||||
File.write!(Path.join(vendors_dir, "custom.ex"), """
|
||||
defmodule Towerops.Snmp.Profiles.Vendors.Custom do
|
||||
# No profile_names function
|
||||
end
|
||||
""")
|
||||
|
||||
File.cd!(temp_dir, fn ->
|
||||
output =
|
||||
capture_io(fn ->
|
||||
GenVendorTests.run([])
|
||||
end)
|
||||
|
||||
assert output =~ "Generated 1 test files"
|
||||
|
||||
# Test should be generated with empty profile list
|
||||
test_file = Path.join(tests_dir, "custom_test.exs")
|
||||
assert File.exists?(test_file)
|
||||
|
||||
content = File.read!(test_file)
|
||||
assert content =~ "assert Custom.profile_names() == []"
|
||||
end)
|
||||
end
|
||||
|
||||
test "generates proper module name for snake_case files", %{
|
||||
vendors_dir: vendors_dir,
|
||||
tests_dir: tests_dir,
|
||||
temp_dir: temp_dir
|
||||
} do
|
||||
# Create vendor module with snake_case filename
|
||||
File.write!(Path.join(vendors_dir, "palo_alto.ex"), """
|
||||
defmodule Towerops.Snmp.Profiles.Vendors.PaloAlto do
|
||||
def profile_names, do: ["panos"]
|
||||
end
|
||||
""")
|
||||
|
||||
File.cd!(temp_dir, fn ->
|
||||
_output =
|
||||
capture_io(fn ->
|
||||
GenVendorTests.run([])
|
||||
end)
|
||||
|
||||
# Test file should be palo_alto_test.exs
|
||||
test_file = Path.join(tests_dir, "palo_alto_test.exs")
|
||||
assert File.exists?(test_file)
|
||||
|
||||
# Module should be PaloAltoTest
|
||||
content = File.read!(test_file)
|
||||
assert content =~ "defmodule Towerops.Snmp.Profiles.Vendors.PaloAltoTest do"
|
||||
assert content =~ "alias Towerops.Snmp.Profiles.Vendors.PaloAlto"
|
||||
end)
|
||||
end
|
||||
|
||||
test "generates tests with proper expect counts for wireless_oid_defs", %{
|
||||
vendors_dir: vendors_dir,
|
||||
tests_dir: tests_dir,
|
||||
temp_dir: temp_dir
|
||||
} do
|
||||
# Create vendor module with multiple OID defs
|
||||
File.write!(Path.join(vendors_dir, "test_vendor.ex"), """
|
||||
defmodule Towerops.Snmp.Profiles.Vendors.TestVendor do
|
||||
def profile_names, do: ["test"]
|
||||
|
||||
def wireless_oid_defs do
|
||||
[
|
||||
%{oid: "1.2.3.4.0", sensor_descr: "Temp", sensor_type: "temperature", sensor_unit: "C", sensor_divisor: 1},
|
||||
%{oid: "1.2.3.5.0", sensor_descr: "Voltage", sensor_type: "voltage", sensor_unit: "V", sensor_divisor: 1},
|
||||
%{oid: "1.2.3.6.0", sensor_descr: "Power", sensor_type: "power", sensor_unit: "W", sensor_divisor: 1}
|
||||
]
|
||||
end
|
||||
end
|
||||
""")
|
||||
|
||||
File.cd!(temp_dir, fn ->
|
||||
capture_io(fn ->
|
||||
GenVendorTests.run([])
|
||||
end)
|
||||
|
||||
test_file = Path.join(tests_dir, "test_vendor_test.exs")
|
||||
content = File.read!(test_file)
|
||||
|
||||
# Should verify list has >= 1 items (not hardcoded count)
|
||||
assert content =~ "assert length(defs) >= 1"
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
312
test/mix/tasks/geoip.import_test.exs
Normal file
312
test/mix/tasks/geoip.import_test.exs
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
defmodule Mix.Tasks.Geoip.ImportTest do
|
||||
use Towerops.DataCase, async: true
|
||||
|
||||
import ExUnit.CaptureIO
|
||||
import Mix.Tasks.Geoip.Import, only: [load_locations: 1, load_blocks: 2, run: 1]
|
||||
|
||||
alias Towerops.GeoIP.Block
|
||||
alias Towerops.GeoIP.Location
|
||||
alias Towerops.Repo
|
||||
|
||||
setup do
|
||||
# Create temporary directory for test CSV files
|
||||
temp_dir = Path.join(System.tmp_dir!(), "geoip_test_#{System.unique_integer([:positive])}")
|
||||
File.mkdir_p!(temp_dir)
|
||||
|
||||
on_exit(fn ->
|
||||
File.rm_rf!(temp_dir)
|
||||
end)
|
||||
|
||||
{:ok, temp_dir: temp_dir}
|
||||
end
|
||||
|
||||
describe "load_locations/1" do
|
||||
test "imports locations from CSV file", %{temp_dir: temp_dir} do
|
||||
csv_file = Path.join(temp_dir, "locations.csv")
|
||||
|
||||
File.write!(csv_file, """
|
||||
geoname_id,locale_code,continent_code,continent_name,country_iso_code,country_name,subdivision_1_iso_code,subdivision_1_name,subdivision_2_iso_code,subdivision_2_name,city_name,metro_code,time_zone,is_in_european_union
|
||||
5391959,en,NA,North America,US,United States,CA,California,,,San Francisco,807,America/Los_Angeles,0
|
||||
6167865,en,NA,North America,CA,Canada,ON,Ontario,,,Toronto,0,America/Toronto,0
|
||||
2643743,en,EU,Europe,GB,United Kingdom,ENG,England,,,London,0,Europe/London,1
|
||||
""")
|
||||
|
||||
{count, geoname_ids} = load_locations(csv_file)
|
||||
|
||||
assert count == 3
|
||||
assert MapSet.size(geoname_ids) == 3
|
||||
assert MapSet.member?(geoname_ids, 5_391_959)
|
||||
assert MapSet.member?(geoname_ids, 6_167_865)
|
||||
assert MapSet.member?(geoname_ids, 2_643_743)
|
||||
|
||||
# Verify database records
|
||||
locations = Repo.all(Location)
|
||||
assert length(locations) == 3
|
||||
|
||||
san_francisco = Enum.find(locations, &(&1.geoname_id == 5_391_959))
|
||||
assert san_francisco.country_code == "US"
|
||||
assert san_francisco.country_name == "United States"
|
||||
assert san_francisco.city_name == "San Francisco"
|
||||
assert san_francisco.subdivision_1_name == "California"
|
||||
end
|
||||
|
||||
test "skips locations without country code", %{temp_dir: temp_dir} do
|
||||
csv_file = Path.join(temp_dir, "locations.csv")
|
||||
|
||||
File.write!(csv_file, """
|
||||
geoname_id,locale_code,continent_code,continent_name,country_iso_code,country_name,subdivision_1_iso_code,subdivision_1_name,subdivision_2_iso_code,subdivision_2_name,city_name,metro_code,time_zone,is_in_european_union
|
||||
5391959,en,NA,North America,US,United States,CA,California,,,San Francisco,807,America/Los_Angeles,0
|
||||
9999999,en,NA,North America,,,,,,,,0,,0
|
||||
""")
|
||||
|
||||
{count, geoname_ids} = load_locations(csv_file)
|
||||
|
||||
assert count == 1
|
||||
assert MapSet.size(geoname_ids) == 1
|
||||
assert MapSet.member?(geoname_ids, 5_391_959)
|
||||
end
|
||||
|
||||
test "skips locations without geoname_id", %{temp_dir: temp_dir} do
|
||||
csv_file = Path.join(temp_dir, "locations.csv")
|
||||
|
||||
File.write!(csv_file, """
|
||||
geoname_id,locale_code,continent_code,continent_name,country_iso_code,country_name,subdivision_1_iso_code,subdivision_1_name,subdivision_2_iso_code,subdivision_2_name,city_name,metro_code,time_zone,is_in_european_union
|
||||
5391959,en,NA,North America,US,United States,CA,California,,,San Francisco,807,America/Los_Angeles,0
|
||||
,en,NA,North America,CA,Canada,ON,Ontario,,,Toronto,0,America/Toronto,0
|
||||
""")
|
||||
|
||||
{count, geoname_ids} = load_locations(csv_file)
|
||||
|
||||
assert count == 1
|
||||
assert MapSet.size(geoname_ids) == 1
|
||||
end
|
||||
|
||||
test "handles empty optional fields", %{temp_dir: temp_dir} do
|
||||
csv_file = Path.join(temp_dir, "locations.csv")
|
||||
|
||||
File.write!(csv_file, """
|
||||
geoname_id,locale_code,continent_code,continent_name,country_iso_code,country_name,subdivision_1_iso_code,subdivision_1_name,subdivision_2_iso_code,subdivision_2_name,city_name,metro_code,time_zone,is_in_european_union
|
||||
5391959,en,NA,North America,US,,,,,,,,America/Los_Angeles,0
|
||||
""")
|
||||
|
||||
{count, _geoname_ids} = load_locations(csv_file)
|
||||
assert count == 1
|
||||
|
||||
location = Repo.get_by!(Location, geoname_id: 5_391_959)
|
||||
assert location.country_name == nil
|
||||
assert location.city_name == nil
|
||||
assert location.subdivision_1_name == nil
|
||||
assert location.subdivision_2_name == nil
|
||||
end
|
||||
|
||||
test "processes large batches correctly", %{temp_dir: temp_dir} do
|
||||
csv_file = Path.join(temp_dir, "locations.csv")
|
||||
|
||||
# Generate 10,000 locations (more than batch size of 5,000)
|
||||
header =
|
||||
"geoname_id,locale_code,continent_code,continent_name,country_iso_code,country_name,subdivision_1_iso_code,subdivision_1_name,subdivision_2_iso_code,subdivision_2_name,city_name,metro_code,time_zone,is_in_european_union\n"
|
||||
|
||||
rows =
|
||||
Enum.map(1..10_000, fn i ->
|
||||
"#{i},en,NA,North America,US,United States,CA,California,,,City#{i},807,America/Los_Angeles,0"
|
||||
end)
|
||||
|
||||
File.write!(csv_file, header <> Enum.join(rows, "\n") <> "\n")
|
||||
|
||||
{count, geoname_ids} = load_locations(csv_file)
|
||||
|
||||
assert count == 10_000
|
||||
assert MapSet.size(geoname_ids) == 10_000
|
||||
assert Repo.aggregate(Location, :count) == 10_000
|
||||
end
|
||||
end
|
||||
|
||||
describe "load_blocks/2" do
|
||||
test "imports IP blocks from CSV file", %{temp_dir: temp_dir} do
|
||||
csv_file = Path.join(temp_dir, "blocks.csv")
|
||||
|
||||
# First, create some locations
|
||||
location1 = insert(:geoip_location, geoname_id: 5_391_959, country_code: "US")
|
||||
location2 = insert(:geoip_location, geoname_id: 6_167_865, country_code: "CA")
|
||||
|
||||
valid_geoname_ids = MapSet.new([location1.geoname_id, location2.geoname_id])
|
||||
|
||||
File.write!(csv_file, """
|
||||
network,geoname_id,registered_country_geoname_id,represented_country_geoname_id,is_anonymous_proxy,is_satellite_provider,postal_code,latitude,longitude,accuracy_radius
|
||||
1.0.0.0/24,5391959,5391959,,,0,94102,37.7749,-122.4194,1000
|
||||
1.0.1.0/24,6167865,6167865,,,0,M5H,43.6532,-79.3832,1000
|
||||
1.0.2.0/24,5391959,5391959,,,0,94102,37.7749,-122.4194,1000
|
||||
""")
|
||||
|
||||
count = load_blocks(csv_file, valid_geoname_ids)
|
||||
|
||||
assert count == 3
|
||||
|
||||
# Verify database records
|
||||
blocks = Repo.all(Block)
|
||||
assert length(blocks) == 3
|
||||
|
||||
block1 = Enum.find(blocks, &(&1.network == "1.0.0.0/24"))
|
||||
assert block1.geoname_id == 5_391_959
|
||||
assert block1.start_ip_int == 16_777_216
|
||||
assert block1.end_ip_int == 16_777_471
|
||||
end
|
||||
|
||||
test "filters out blocks with invalid geoname_id", %{temp_dir: temp_dir} do
|
||||
csv_file = Path.join(temp_dir, "blocks.csv")
|
||||
|
||||
location = insert(:geoip_location, geoname_id: 5_391_959, country_code: "US")
|
||||
valid_geoname_ids = MapSet.new([location.geoname_id])
|
||||
|
||||
File.write!(csv_file, """
|
||||
network,geoname_id,registered_country_geoname_id,represented_country_geoname_id,is_anonymous_proxy,is_satellite_provider,postal_code,latitude,longitude,accuracy_radius
|
||||
1.0.0.0/24,5391959,5391959,,,0,94102,37.7749,-122.4194,1000
|
||||
1.0.1.0/24,9999999,9999999,,,0,M5H,43.6532,-79.3832,1000
|
||||
""")
|
||||
|
||||
count = load_blocks(csv_file, valid_geoname_ids)
|
||||
|
||||
assert count == 1
|
||||
assert Repo.aggregate(Block, :count) == 1
|
||||
end
|
||||
|
||||
test "uses registered_country_geoname_id as fallback", %{temp_dir: temp_dir} do
|
||||
csv_file = Path.join(temp_dir, "blocks.csv")
|
||||
|
||||
location = insert(:geoip_location, geoname_id: 5_391_959, country_code: "US")
|
||||
valid_geoname_ids = MapSet.new([location.geoname_id])
|
||||
|
||||
File.write!(csv_file, """
|
||||
network,geoname_id,registered_country_geoname_id,represented_country_geoname_id,is_anonymous_proxy,is_satellite_provider,postal_code,latitude,longitude,accuracy_radius
|
||||
1.0.0.0/24,,5391959,,,0,94102,37.7749,-122.4194,1000
|
||||
""")
|
||||
|
||||
count = load_blocks(csv_file, valid_geoname_ids)
|
||||
|
||||
assert count == 1
|
||||
|
||||
block = Repo.one!(Block)
|
||||
assert block.geoname_id == 5_391_959
|
||||
assert block.registered_country_geoname_id == 5_391_959
|
||||
end
|
||||
|
||||
test "skips blocks with no valid geoname_id", %{temp_dir: temp_dir} do
|
||||
csv_file = Path.join(temp_dir, "blocks.csv")
|
||||
valid_geoname_ids = MapSet.new()
|
||||
|
||||
File.write!(csv_file, """
|
||||
network,geoname_id,registered_country_geoname_id,represented_country_geoname_id,is_anonymous_proxy,is_satellite_provider,postal_code,latitude,longitude,accuracy_radius
|
||||
1.0.0.0/24,,,,,0,94102,37.7749,-122.4194,1000
|
||||
""")
|
||||
|
||||
count = load_blocks(csv_file, valid_geoname_ids)
|
||||
|
||||
assert count == 0
|
||||
assert Repo.aggregate(Block, :count) == 0
|
||||
end
|
||||
|
||||
test "calculates IP ranges correctly", %{temp_dir: temp_dir} do
|
||||
csv_file = Path.join(temp_dir, "blocks.csv")
|
||||
|
||||
location = insert(:geoip_location, geoname_id: 5_391_959, country_code: "US")
|
||||
valid_geoname_ids = MapSet.new([location.geoname_id])
|
||||
|
||||
File.write!(csv_file, """
|
||||
network,geoname_id,registered_country_geoname_id,represented_country_geoname_id,is_anonymous_proxy,is_satellite_provider,postal_code,latitude,longitude,accuracy_radius
|
||||
192.168.1.0/24,5391959,5391959,,,0,94102,37.7749,-122.4194,1000
|
||||
10.0.0.0/8,5391959,5391959,,,0,94102,37.7749,-122.4194,1000
|
||||
172.16.0.0/12,5391959,5391959,,,0,94102,37.7749,-122.4194,1000
|
||||
""")
|
||||
|
||||
count = load_blocks(csv_file, valid_geoname_ids)
|
||||
assert count == 3
|
||||
|
||||
blocks = Repo.all(Block)
|
||||
|
||||
# 192.168.1.0/24 = 192.168.1.0 to 192.168.1.255
|
||||
block1 = Enum.find(blocks, &(&1.network == "192.168.1.0/24"))
|
||||
assert block1.start_ip_int == 3_232_235_776
|
||||
assert block1.end_ip_int == 3_232_236_031
|
||||
|
||||
# 10.0.0.0/8 = 10.0.0.0 to 10.255.255.255
|
||||
block2 = Enum.find(blocks, &(&1.network == "10.0.0.0/8"))
|
||||
assert block2.start_ip_int == 167_772_160
|
||||
assert block2.end_ip_int == 184_549_375
|
||||
|
||||
# 172.16.0.0/12 = 172.16.0.0 to 172.31.255.255
|
||||
block3 = Enum.find(blocks, &(&1.network == "172.16.0.0/12"))
|
||||
assert block3.start_ip_int == 2_886_729_728
|
||||
assert block3.end_ip_int == 2_887_778_303
|
||||
end
|
||||
|
||||
test "processes large batches correctly", %{temp_dir: temp_dir} do
|
||||
csv_file = Path.join(temp_dir, "blocks.csv")
|
||||
|
||||
location = insert(:geoip_location, geoname_id: 5_391_959, country_code: "US")
|
||||
valid_geoname_ids = MapSet.new([location.geoname_id])
|
||||
|
||||
# Generate 10,000 blocks (more than batch size of 5,000)
|
||||
header =
|
||||
"network,geoname_id,registered_country_geoname_id,represented_country_geoname_id,is_anonymous_proxy,is_satellite_provider,postal_code,latitude,longitude,accuracy_radius\n"
|
||||
|
||||
rows =
|
||||
Enum.map(1..10_000, fn i ->
|
||||
# Use sequential /32 blocks
|
||||
octet1 = div(i, 256 * 256)
|
||||
octet2 = rem(div(i, 256), 256)
|
||||
octet3 = rem(i, 256)
|
||||
"#{octet1}.#{octet2}.#{octet3}.0/32,5391959,5391959,,,0,94102,37.7749,-122.4194,1000"
|
||||
end)
|
||||
|
||||
File.write!(csv_file, header <> Enum.join(rows, "\n") <> "\n")
|
||||
|
||||
count = load_blocks(csv_file, valid_geoname_ids)
|
||||
|
||||
assert count == 10_000
|
||||
assert Repo.aggregate(Block, :count) == 10_000
|
||||
end
|
||||
end
|
||||
|
||||
describe "run/1 with invalid arguments" do
|
||||
test "shows usage help when no directory is provided" do
|
||||
output = capture_io(:stderr, fn -> run([]) end)
|
||||
assert output =~ "Usage: mix geoip.import <directory>"
|
||||
end
|
||||
|
||||
test "shows usage help when too many arguments are provided" do
|
||||
output = capture_io(:stderr, fn -> run(["dir1", "dir2"]) end)
|
||||
assert output =~ "Usage: mix geoip.import <directory>"
|
||||
end
|
||||
|
||||
test "requires TOWEROPS_KEY env var for production mode" do
|
||||
System.delete_env("TOWEROPS_KEY")
|
||||
|
||||
assert catch_exit(
|
||||
capture_io(:stderr, fn ->
|
||||
run(["~/geoip", "--production"])
|
||||
end)
|
||||
) == {:shutdown, 1}
|
||||
end
|
||||
end
|
||||
|
||||
# Helper to insert location for tests
|
||||
defp insert(:geoip_location, attrs) do
|
||||
defaults = %{
|
||||
country_code: "US",
|
||||
country_name: "United States",
|
||||
city_name: "Test City",
|
||||
subdivision_1_name: nil,
|
||||
subdivision_2_name: nil,
|
||||
latitude: nil,
|
||||
longitude: nil,
|
||||
inserted_at: DateTime.truncate(DateTime.utc_now(), :second),
|
||||
updated_at: DateTime.truncate(DateTime.utc_now(), :second)
|
||||
}
|
||||
|
||||
attrs = Map.merge(defaults, Map.new(attrs))
|
||||
|
||||
%Location{}
|
||||
|> Ecto.Changeset.change(attrs)
|
||||
|> Repo.insert!()
|
||||
end
|
||||
end
|
||||
455
test/mix/tasks/import_profiles_test.exs
Normal file
455
test/mix/tasks/import_profiles_test.exs
Normal file
|
|
@ -0,0 +1,455 @@
|
|||
defmodule Mix.Tasks.ImportProfilesTest do
|
||||
use Towerops.DataCase, async: true
|
||||
|
||||
import ExUnit.CaptureIO
|
||||
|
||||
alias Mix.Tasks.ImportProfiles
|
||||
alias Towerops.Profiles
|
||||
alias Towerops.Profiles.DeviceOid
|
||||
alias Towerops.Profiles.SensorOid
|
||||
alias Towerops.Repo
|
||||
|
||||
setup do
|
||||
# Create temporary directory for test YAML files
|
||||
temp_dir = Path.join(System.tmp_dir!(), "profiles_test_#{System.unique_integer([:positive])}")
|
||||
detection_dir = Path.join([temp_dir, "os_detection"])
|
||||
discovery_dir = Path.join([temp_dir, "os_discovery"])
|
||||
File.mkdir_p!(detection_dir)
|
||||
File.mkdir_p!(discovery_dir)
|
||||
|
||||
on_exit(fn ->
|
||||
File.rm_rf!(temp_dir)
|
||||
end)
|
||||
|
||||
{:ok, temp_dir: temp_dir, detection_dir: detection_dir, discovery_dir: discovery_dir}
|
||||
end
|
||||
|
||||
describe "run/1 with valid profiles" do
|
||||
test "imports a basic profile from YAML", %{
|
||||
detection_dir: detection_dir,
|
||||
discovery_dir: discovery_dir,
|
||||
temp_dir: temp_dir
|
||||
} do
|
||||
# Create a simple detection file
|
||||
detection_yaml = Path.join(detection_dir, "mikrotik.yaml")
|
||||
|
||||
File.write!(detection_yaml, """
|
||||
os: mikrotik
|
||||
text: MikroTik
|
||||
priority: 100
|
||||
discovery:
|
||||
- sysDescr:
|
||||
- RouterOS
|
||||
- sysObjectID:
|
||||
- .1.3.6.1.4.1.14988.
|
||||
""")
|
||||
|
||||
# Create a simple discovery file
|
||||
discovery_yaml = Path.join(discovery_dir, "mikrotik.yaml")
|
||||
|
||||
File.write!(discovery_yaml, """
|
||||
modules:
|
||||
os:
|
||||
serial: SNMPv2-MIB::sysContact.0
|
||||
version: SNMPv2-MIB::sysName.0
|
||||
sensors:
|
||||
temperature:
|
||||
data:
|
||||
- oid: MIKROTIK-MIB::mtxrHlTemperature.0
|
||||
descr: System Temperature
|
||||
unit: C
|
||||
divisor: 10
|
||||
""")
|
||||
|
||||
output =
|
||||
capture_io(fn ->
|
||||
ImportProfiles.run(["--source-path", temp_dir])
|
||||
end)
|
||||
|
||||
assert output =~ "✓ mikrotik: created/updated successfully"
|
||||
assert output =~ "Import complete: 1 succeeded, 0 failed"
|
||||
|
||||
# Verify profile was created
|
||||
profile = Profiles.get_profile("mikrotik")
|
||||
assert profile
|
||||
assert profile.vendor == "MikroTik"
|
||||
assert profile.priority == 100
|
||||
assert profile.enabled == true
|
||||
|
||||
# Verify device OIDs were created
|
||||
device_oids = Repo.all(from o in DeviceOid, where: o.profile_id == ^profile.id)
|
||||
assert length(device_oids) == 2
|
||||
|
||||
serial_oid = Enum.find(device_oids, &(&1.field == "serial_number"))
|
||||
assert serial_oid.mib_name == "SNMPv2-MIB::sysContact.0"
|
||||
|
||||
version_oid = Enum.find(device_oids, &(&1.field == "firmware_version"))
|
||||
assert version_oid.mib_name == "SNMPv2-MIB::sysName.0"
|
||||
|
||||
# Verify sensor OIDs were created
|
||||
sensor_oids = Repo.all(from o in SensorOid, where: o.profile_id == ^profile.id)
|
||||
assert length(sensor_oids) == 1
|
||||
|
||||
temp_sensor = List.first(sensor_oids)
|
||||
assert temp_sensor.sensor_type == "temperature"
|
||||
assert temp_sensor.mib_name == "MIKROTIK-MIB::mtxrHlTemperature.0"
|
||||
assert temp_sensor.sensor_descr == "System Temperature"
|
||||
assert temp_sensor.sensor_unit == "C"
|
||||
assert temp_sensor.sensor_divisor == 10
|
||||
end
|
||||
|
||||
test "updates existing profile", %{detection_dir: detection_dir, temp_dir: temp_dir} do
|
||||
# Create initial profile
|
||||
{:ok, profile} =
|
||||
Profiles.create_profile(%{
|
||||
name: "cisco",
|
||||
vendor: "Cisco Old",
|
||||
detection_pattern: "old pattern",
|
||||
priority: 50,
|
||||
enabled: true
|
||||
})
|
||||
|
||||
# Create YAML file with updated info
|
||||
detection_yaml = Path.join(detection_dir, "cisco.yaml")
|
||||
|
||||
File.write!(detection_yaml, """
|
||||
os: cisco
|
||||
text: Cisco Updated
|
||||
priority: 200
|
||||
discovery:
|
||||
- sysDescr:
|
||||
- Cisco IOS
|
||||
""")
|
||||
|
||||
output =
|
||||
capture_io(fn ->
|
||||
ImportProfiles.run(["--source-path", temp_dir])
|
||||
end)
|
||||
|
||||
assert output =~ "✓ cisco: created/updated successfully"
|
||||
|
||||
# Verify profile was updated
|
||||
updated_profile = Profiles.get_profile("cisco")
|
||||
assert updated_profile.id == profile.id
|
||||
assert updated_profile.vendor == "Cisco Updated"
|
||||
assert updated_profile.priority == 200
|
||||
end
|
||||
|
||||
test "imports specific profiles only", %{detection_dir: detection_dir, temp_dir: temp_dir} do
|
||||
# Create multiple detection files
|
||||
File.write!(Path.join(detection_dir, "mikrotik.yaml"), """
|
||||
os: mikrotik
|
||||
text: MikroTik
|
||||
priority: 100
|
||||
""")
|
||||
|
||||
File.write!(Path.join(detection_dir, "cisco.yaml"), """
|
||||
os: cisco
|
||||
text: Cisco
|
||||
priority: 100
|
||||
""")
|
||||
|
||||
File.write!(Path.join(detection_dir, "ubiquiti.yaml"), """
|
||||
os: ubiquiti
|
||||
text: Ubiquiti
|
||||
priority: 100
|
||||
""")
|
||||
|
||||
output =
|
||||
capture_io(fn ->
|
||||
ImportProfiles.run(["--source-path", temp_dir, "--profiles", "mikrotik,cisco"])
|
||||
end)
|
||||
|
||||
assert output =~ "Importing 2 specified profiles"
|
||||
assert output =~ "✓ mikrotik"
|
||||
assert output =~ "✓ cisco"
|
||||
refute output =~ "ubiquiti"
|
||||
|
||||
# Verify only specified profiles were created
|
||||
assert Profiles.get_profile("mikrotik")
|
||||
assert Profiles.get_profile("cisco")
|
||||
assert Profiles.get_profile("ubiquiti") == nil
|
||||
end
|
||||
|
||||
test "handles profile without discovery file", %{detection_dir: detection_dir, temp_dir: temp_dir} do
|
||||
detection_yaml = Path.join(detection_dir, "generic.yaml")
|
||||
|
||||
File.write!(detection_yaml, """
|
||||
os: generic
|
||||
text: Generic Device
|
||||
priority: 1
|
||||
""")
|
||||
|
||||
output =
|
||||
capture_io(fn ->
|
||||
ImportProfiles.run(["--source-path", temp_dir])
|
||||
end)
|
||||
|
||||
assert output =~ "✓ generic: created/updated successfully"
|
||||
|
||||
profile = Profiles.get_profile("generic")
|
||||
assert profile
|
||||
assert profile.vendor == "Generic Device"
|
||||
|
||||
# Should have no device OIDs or sensor OIDs
|
||||
assert Repo.aggregate(from(o in DeviceOid, where: o.profile_id == ^profile.id), :count) == 0
|
||||
assert Repo.aggregate(from(o in SensorOid, where: o.profile_id == ^profile.id), :count) == 0
|
||||
end
|
||||
|
||||
test "extracts detection patterns from sysDescr", %{detection_dir: detection_dir, temp_dir: temp_dir} do
|
||||
detection_yaml = Path.join(detection_dir, "test.yaml")
|
||||
|
||||
File.write!(detection_yaml, """
|
||||
os: test
|
||||
text: Test Device
|
||||
discovery:
|
||||
- sysDescr:
|
||||
- "Test Pattern 1"
|
||||
- "Test Pattern 2"
|
||||
""")
|
||||
|
||||
capture_io(fn ->
|
||||
ImportProfiles.run(["--source-path", temp_dir])
|
||||
end)
|
||||
|
||||
profile = Profiles.get_profile("test")
|
||||
assert profile.detection_pattern == "Test Pattern 1"
|
||||
end
|
||||
|
||||
test "extracts detection OID from sysObjectID", %{detection_dir: detection_dir, temp_dir: temp_dir} do
|
||||
detection_yaml = Path.join(detection_dir, "test.yaml")
|
||||
|
||||
File.write!(detection_yaml, """
|
||||
os: test
|
||||
text: Test Device
|
||||
discovery:
|
||||
- sysObjectID:
|
||||
- ".1.3.6.1.4.1.9."
|
||||
- ".1.3.6.1.4.1.10."
|
||||
""")
|
||||
|
||||
capture_io(fn ->
|
||||
ImportProfiles.run(["--source-path", temp_dir])
|
||||
end)
|
||||
|
||||
profile = Profiles.get_profile("test")
|
||||
assert profile.detection_oid == ".1.3.6.1.4.1.9."
|
||||
end
|
||||
|
||||
test "imports all standard device OID fields", %{
|
||||
detection_dir: detection_dir,
|
||||
discovery_dir: discovery_dir,
|
||||
temp_dir: temp_dir
|
||||
} do
|
||||
File.write!(Path.join(detection_dir, "test.yaml"), """
|
||||
os: test
|
||||
text: Test Device
|
||||
""")
|
||||
|
||||
File.write!(Path.join(discovery_dir, "test.yaml"), """
|
||||
modules:
|
||||
os:
|
||||
serial: DEVICE-MIB::serialNumber.0
|
||||
version: DEVICE-MIB::firmwareVersion.0
|
||||
hardware: DEVICE-MIB::hardwareModel.0
|
||||
lat: DEVICE-MIB::latitude.0
|
||||
long: DEVICE-MIB::longitude.0
|
||||
features: DEVICE-MIB::features.0
|
||||
sysName: SNMPv2-MIB::sysName.0
|
||||
""")
|
||||
|
||||
capture_io(fn ->
|
||||
ImportProfiles.run(["--source-path", temp_dir])
|
||||
end)
|
||||
|
||||
profile = Profiles.get_profile("test")
|
||||
device_oids = Repo.all(from o in DeviceOid, where: o.profile_id == ^profile.id)
|
||||
|
||||
assert length(device_oids) == 7
|
||||
|
||||
assert Enum.find(device_oids, &(&1.field == "serial_number")).mib_name == "DEVICE-MIB::serialNumber.0"
|
||||
assert Enum.find(device_oids, &(&1.field == "firmware_version")).mib_name == "DEVICE-MIB::firmwareVersion.0"
|
||||
assert Enum.find(device_oids, &(&1.field == "hardware")).mib_name == "DEVICE-MIB::hardwareModel.0"
|
||||
assert Enum.find(device_oids, &(&1.field == "latitude")).mib_name == "DEVICE-MIB::latitude.0"
|
||||
assert Enum.find(device_oids, &(&1.field == "longitude")).mib_name == "DEVICE-MIB::longitude.0"
|
||||
assert Enum.find(device_oids, &(&1.field == "features")).mib_name == "DEVICE-MIB::features.0"
|
||||
assert Enum.find(device_oids, &(&1.field == "sys_name_oid")).mib_name == "SNMPv2-MIB::sysName.0"
|
||||
end
|
||||
|
||||
test "imports all sensor types", %{detection_dir: detection_dir, discovery_dir: discovery_dir, temp_dir: temp_dir} do
|
||||
File.write!(Path.join(detection_dir, "test.yaml"), """
|
||||
os: test
|
||||
text: Test Device
|
||||
""")
|
||||
|
||||
File.write!(Path.join(discovery_dir, "test.yaml"), """
|
||||
modules:
|
||||
sensors:
|
||||
temperature:
|
||||
data:
|
||||
- oid: DEVICE-MIB::temp.0
|
||||
descr: Temperature
|
||||
unit: C
|
||||
voltage:
|
||||
data:
|
||||
- oid: DEVICE-MIB::voltage.0
|
||||
descr: Voltage
|
||||
unit: V
|
||||
current:
|
||||
data:
|
||||
- oid: DEVICE-MIB::current.0
|
||||
descr: Current
|
||||
unit: A
|
||||
power:
|
||||
data:
|
||||
- oid: DEVICE-MIB::power.0
|
||||
descr: Power
|
||||
unit: W
|
||||
fanspeed:
|
||||
data:
|
||||
- oid: DEVICE-MIB::fan.0
|
||||
descr: Fan Speed
|
||||
unit: RPM
|
||||
""")
|
||||
|
||||
capture_io(fn ->
|
||||
ImportProfiles.run(["--source-path", temp_dir])
|
||||
end)
|
||||
|
||||
profile = Profiles.get_profile("test")
|
||||
sensor_oids = Repo.all(from o in SensorOid, where: o.profile_id == ^profile.id)
|
||||
|
||||
assert length(sensor_oids) == 5
|
||||
|
||||
assert Enum.any?(sensor_oids, &(&1.sensor_type == "temperature"))
|
||||
assert Enum.any?(sensor_oids, &(&1.sensor_type == "voltage"))
|
||||
assert Enum.any?(sensor_oids, &(&1.sensor_type == "current"))
|
||||
assert Enum.any?(sensor_oids, &(&1.sensor_type == "power"))
|
||||
assert Enum.any?(sensor_oids, &(&1.sensor_type == "fanspeed"))
|
||||
end
|
||||
|
||||
test "skips table sensors with {{ $index }}", %{
|
||||
detection_dir: detection_dir,
|
||||
discovery_dir: discovery_dir,
|
||||
temp_dir: temp_dir
|
||||
} do
|
||||
File.write!(Path.join(detection_dir, "test.yaml"), """
|
||||
os: test
|
||||
text: Test Device
|
||||
""")
|
||||
|
||||
File.write!(Path.join(discovery_dir, "test.yaml"), """
|
||||
modules:
|
||||
sensors:
|
||||
temperature:
|
||||
data:
|
||||
- oid: DEVICE-MIB::temp.0
|
||||
descr: Scalar Temperature
|
||||
unit: C
|
||||
- oid: DEVICE-MIB::tempTable
|
||||
num_oid: .1.3.6.1.4.1.9999.1.2.{{ $index }}
|
||||
descr: Table Temperature
|
||||
unit: C
|
||||
""")
|
||||
|
||||
capture_io(fn ->
|
||||
ImportProfiles.run(["--source-path", temp_dir])
|
||||
end)
|
||||
|
||||
profile = Profiles.get_profile("test")
|
||||
sensor_oids = Repo.all(from o in SensorOid, where: o.profile_id == ^profile.id)
|
||||
|
||||
# Only scalar sensor should be imported
|
||||
assert length(sensor_oids) == 1
|
||||
assert List.first(sensor_oids).sensor_descr == "Scalar Temperature"
|
||||
end
|
||||
|
||||
test "replaces existing OIDs when reimporting", %{
|
||||
detection_dir: detection_dir,
|
||||
discovery_dir: discovery_dir,
|
||||
temp_dir: temp_dir
|
||||
} do
|
||||
# Create initial profile with OIDs
|
||||
{:ok, profile} = Profiles.create_profile(%{name: "test", vendor: "Test", priority: 100, enabled: true})
|
||||
{:ok, _} = Profiles.add_device_oid(profile.id, "serial_number", "OLD-MIB::serial.0")
|
||||
|
||||
{:ok, _} =
|
||||
Profiles.add_sensor_oid(profile.id, %{
|
||||
sensor_type: "temperature",
|
||||
mib_name: "OLD-MIB::temp.0",
|
||||
sensor_descr: "Old Temp",
|
||||
sensor_unit: "C",
|
||||
sensor_divisor: 1
|
||||
})
|
||||
|
||||
# Create new YAML with different OIDs
|
||||
File.write!(Path.join(detection_dir, "test.yaml"), """
|
||||
os: test
|
||||
text: Test Device
|
||||
""")
|
||||
|
||||
File.write!(Path.join(discovery_dir, "test.yaml"), """
|
||||
modules:
|
||||
os:
|
||||
serial: NEW-MIB::serialNumber.0
|
||||
sensors:
|
||||
voltage:
|
||||
data:
|
||||
- oid: NEW-MIB::voltage.0
|
||||
descr: New Voltage
|
||||
unit: V
|
||||
""")
|
||||
|
||||
capture_io(fn ->
|
||||
ImportProfiles.run(["--source-path", temp_dir])
|
||||
end)
|
||||
|
||||
# Old OIDs should be replaced
|
||||
device_oids = Repo.all(from o in DeviceOid, where: o.profile_id == ^profile.id)
|
||||
assert length(device_oids) == 1
|
||||
assert List.first(device_oids).mib_name == "NEW-MIB::serialNumber.0"
|
||||
|
||||
sensor_oids = Repo.all(from o in SensorOid, where: o.profile_id == ^profile.id)
|
||||
assert length(sensor_oids) == 1
|
||||
assert List.first(sensor_oids).mib_name == "NEW-MIB::voltage.0"
|
||||
assert List.first(sensor_oids).sensor_type == "voltage"
|
||||
end
|
||||
end
|
||||
|
||||
describe "run/1 error handling" do
|
||||
test "raises error when source directory doesn't exist" do
|
||||
assert_raise RuntimeError, "Source directory not found: /nonexistent/path", fn ->
|
||||
capture_io(fn ->
|
||||
ImportProfiles.run(["--source-path", "/nonexistent/path"])
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
test "reports failed imports", %{detection_dir: detection_dir, temp_dir: temp_dir} do
|
||||
# Create invalid YAML
|
||||
File.write!(Path.join(detection_dir, "invalid.yaml"), """
|
||||
this is not: [valid yaml: that will parse
|
||||
""")
|
||||
|
||||
output =
|
||||
capture_io([capture_prompt: false], fn ->
|
||||
ImportProfiles.run(["--source-path", temp_dir])
|
||||
end)
|
||||
|
||||
assert output =~ "✗ invalid" or output =~ "Import complete: 0 succeeded, 1 failed"
|
||||
assert output =~ "Import complete: 0 succeeded, 1 failed"
|
||||
end
|
||||
|
||||
test "uses default source path when not provided" do
|
||||
# This will fail because priv/profiles doesn't have test data,
|
||||
# but we can verify it tries to use the right path
|
||||
output =
|
||||
capture_io(fn ->
|
||||
ImportProfiles.run([])
|
||||
end)
|
||||
|
||||
# Should try to load from default location
|
||||
assert output =~ "Discovering YAML files" or output =~ "Found 0 YAML files" or output =~ "succeeded"
|
||||
end
|
||||
end
|
||||
end
|
||||
206
test/mix/tasks/oban.cancel_stuck_discovery_test.exs
Normal file
206
test/mix/tasks/oban.cancel_stuck_discovery_test.exs
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
defmodule Mix.Tasks.Oban.CancelStuckDiscoveryTest do
|
||||
use Towerops.DataCase, async: true
|
||||
|
||||
import ExUnit.CaptureIO
|
||||
import Mix.Tasks.Oban.CancelStuckDiscovery, only: [run: 1]
|
||||
|
||||
alias Towerops.Repo
|
||||
|
||||
describe "run/1" do
|
||||
test "cancels stuck discovery jobs in scheduled state" do
|
||||
# Insert a scheduled discovery job
|
||||
{:ok, job} =
|
||||
%{device_id: Ecto.UUID.generate()}
|
||||
|> Oban.Job.new(worker: "Towerops.Workers.DiscoveryWorker", queue: :discovery)
|
||||
|> Repo.insert()
|
||||
|
||||
assert job.state == "available"
|
||||
|
||||
# Change state to scheduled
|
||||
job = Repo.update!(Ecto.Changeset.change(job, state: "scheduled"))
|
||||
|
||||
output = capture_io(fn -> run([]) end)
|
||||
|
||||
# Verify job was cancelled
|
||||
updated_job = Repo.reload(job)
|
||||
assert updated_job.state == "cancelled"
|
||||
assert output =~ "Cancelled 1 stuck discovery jobs"
|
||||
end
|
||||
|
||||
test "cancels stuck discovery jobs in retryable state" do
|
||||
{:ok, job} =
|
||||
%{device_id: Ecto.UUID.generate()}
|
||||
|> Oban.Job.new(worker: "Towerops.Workers.DiscoveryWorker", queue: :discovery)
|
||||
|> Repo.insert()
|
||||
|
||||
# Change state to retryable
|
||||
job = Repo.update!(Ecto.Changeset.change(job, state: "retryable"))
|
||||
|
||||
output = capture_io(fn -> run([]) end)
|
||||
|
||||
updated_job = Repo.reload(job)
|
||||
assert updated_job.state == "cancelled"
|
||||
assert output =~ "Cancelled 1 stuck discovery jobs"
|
||||
end
|
||||
|
||||
test "cancels stuck discovery jobs in executing state" do
|
||||
{:ok, job} =
|
||||
%{device_id: Ecto.UUID.generate()}
|
||||
|> Oban.Job.new(worker: "Towerops.Workers.DiscoveryWorker", queue: :discovery)
|
||||
|> Repo.insert()
|
||||
|
||||
# Change state to executing
|
||||
job =
|
||||
Repo.update!(
|
||||
Ecto.Changeset.change(job,
|
||||
state: "executing",
|
||||
attempted_at: DateTime.utc_now()
|
||||
)
|
||||
)
|
||||
|
||||
output = capture_io(fn -> run([]) end)
|
||||
|
||||
updated_job = Repo.reload(job)
|
||||
assert updated_job.state == "cancelled"
|
||||
assert output =~ "Cancelled 1 stuck discovery jobs"
|
||||
end
|
||||
|
||||
test "cancels multiple stuck discovery jobs" do
|
||||
device_id1 = Ecto.UUID.generate()
|
||||
device_id2 = Ecto.UUID.generate()
|
||||
device_id3 = Ecto.UUID.generate()
|
||||
|
||||
# Create multiple stuck jobs
|
||||
{:ok, job1} =
|
||||
%{device_id: device_id1}
|
||||
|> Oban.Job.new(worker: "Towerops.Workers.DiscoveryWorker", queue: :discovery)
|
||||
|> Repo.insert()
|
||||
|
||||
{:ok, job2} =
|
||||
%{device_id: device_id2}
|
||||
|> Oban.Job.new(worker: "Towerops.Workers.DiscoveryWorker", queue: :discovery)
|
||||
|> Repo.insert()
|
||||
|
||||
{:ok, job3} =
|
||||
%{device_id: device_id3}
|
||||
|> Oban.Job.new(worker: "Towerops.Workers.DiscoveryWorker", queue: :discovery)
|
||||
|> Repo.insert()
|
||||
|
||||
# Make them stuck
|
||||
Repo.update!(Ecto.Changeset.change(job1, state: "scheduled"))
|
||||
Repo.update!(Ecto.Changeset.change(job2, state: "retryable"))
|
||||
Repo.update!(Ecto.Changeset.change(job3, state: "executing", attempted_at: DateTime.utc_now()))
|
||||
|
||||
output = capture_io(fn -> run([]) end)
|
||||
|
||||
# Verify all were cancelled
|
||||
assert Repo.reload(job1).state == "cancelled"
|
||||
assert Repo.reload(job2).state == "cancelled"
|
||||
assert Repo.reload(job3).state == "cancelled"
|
||||
assert output =~ "Cancelled 3 stuck discovery jobs"
|
||||
end
|
||||
|
||||
test "does not cancel completed discovery jobs" do
|
||||
{:ok, job} =
|
||||
%{device_id: Ecto.UUID.generate()}
|
||||
|> Oban.Job.new(worker: "Towerops.Workers.DiscoveryWorker", queue: :discovery)
|
||||
|> Repo.insert()
|
||||
|
||||
# Change state to completed
|
||||
job = Repo.update!(Ecto.Changeset.change(job, state: "completed"))
|
||||
|
||||
output = capture_io(fn -> run([]) end)
|
||||
|
||||
# Job should remain completed
|
||||
updated_job = Repo.reload(job)
|
||||
assert updated_job.state == "completed"
|
||||
assert output =~ "Cancelled 0 stuck discovery jobs"
|
||||
end
|
||||
|
||||
test "does not cancel cancelled discovery jobs" do
|
||||
{:ok, job} =
|
||||
%{device_id: Ecto.UUID.generate()}
|
||||
|> Oban.Job.new(worker: "Towerops.Workers.DiscoveryWorker", queue: :discovery)
|
||||
|> Repo.insert()
|
||||
|
||||
# Change state to cancelled
|
||||
job = Repo.update!(Ecto.Changeset.change(job, state: "cancelled"))
|
||||
|
||||
output = capture_io(fn -> run([]) end)
|
||||
|
||||
# Job should remain cancelled
|
||||
updated_job = Repo.reload(job)
|
||||
assert updated_job.state == "cancelled"
|
||||
assert output =~ "Cancelled 0 stuck discovery jobs"
|
||||
end
|
||||
|
||||
test "does not cancel jobs from other workers" do
|
||||
{:ok, discovery_job} =
|
||||
%{device_id: Ecto.UUID.generate()}
|
||||
|> Oban.Job.new(worker: "Towerops.Workers.DiscoveryWorker", queue: :discovery)
|
||||
|> Repo.insert()
|
||||
|
||||
{:ok, poller_job} =
|
||||
%{device_id: Ecto.UUID.generate()}
|
||||
|> Oban.Job.new(worker: "Towerops.Workers.DevicePollerWorker", queue: :pollers)
|
||||
|> Repo.insert()
|
||||
|
||||
# Make both stuck
|
||||
discovery_job = Repo.update!(Ecto.Changeset.change(discovery_job, state: "scheduled"))
|
||||
poller_job = Repo.update!(Ecto.Changeset.change(poller_job, state: "scheduled"))
|
||||
|
||||
output = capture_io(fn -> run([]) end)
|
||||
|
||||
# Only discovery job should be cancelled
|
||||
assert Repo.reload(discovery_job).state == "cancelled"
|
||||
assert Repo.reload(poller_job).state == "scheduled"
|
||||
assert output =~ "Cancelled 1 stuck discovery jobs"
|
||||
end
|
||||
|
||||
test "reports zero cancellations when no stuck jobs exist" do
|
||||
output = capture_io(fn -> run([]) end)
|
||||
assert output =~ "Cancelled 0 stuck discovery jobs"
|
||||
end
|
||||
|
||||
test "handles jobs with no device_id in args" do
|
||||
{:ok, job} =
|
||||
%{some_other_field: "value"}
|
||||
|> Oban.Job.new(worker: "Towerops.Workers.DiscoveryWorker", queue: :discovery)
|
||||
|> Repo.insert()
|
||||
|
||||
job = Repo.update!(Ecto.Changeset.change(job, state: "scheduled"))
|
||||
|
||||
output = capture_io(fn -> run([]) end)
|
||||
|
||||
updated_job = Repo.reload(job)
|
||||
assert updated_job.state == "cancelled"
|
||||
assert output =~ "Cancelled 1 stuck discovery jobs"
|
||||
end
|
||||
|
||||
test "continues on cancellation failure for one job" do
|
||||
# Create two stuck jobs
|
||||
{:ok, job1} =
|
||||
%{device_id: Ecto.UUID.generate()}
|
||||
|> Oban.Job.new(worker: "Towerops.Workers.DiscoveryWorker", queue: :discovery)
|
||||
|> Repo.insert()
|
||||
|
||||
{:ok, job2} =
|
||||
%{device_id: Ecto.UUID.generate()}
|
||||
|> Oban.Job.new(worker: "Towerops.Workers.DiscoveryWorker", queue: :discovery)
|
||||
|> Repo.insert()
|
||||
|
||||
job1 = Repo.update!(Ecto.Changeset.change(job1, state: "scheduled"))
|
||||
job2 = Repo.update!(Ecto.Changeset.change(job2, state: "scheduled"))
|
||||
|
||||
# Delete job1 to simulate a cancellation failure (job not found)
|
||||
Repo.delete!(job1)
|
||||
|
||||
output = capture_io(fn -> run([]) end)
|
||||
|
||||
# job2 should still be cancelled
|
||||
assert Repo.reload(job2).state == "cancelled"
|
||||
# Should report 1 successful cancellation (job2 only)
|
||||
assert output =~ "Cancelled 1 stuck discovery jobs"
|
||||
end
|
||||
end
|
||||
end
|
||||
247
test/mix/tasks/populate_english_test.exs
Normal file
247
test/mix/tasks/populate_english_test.exs
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
defmodule Mix.Tasks.PopulateEnglishTest do
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
import ExUnit.CaptureIO
|
||||
import Mix.Tasks.PopulateEnglish, only: [run: 1]
|
||||
|
||||
setup do
|
||||
# Create temporary directory for test files
|
||||
temp_dir = Path.join(System.tmp_dir!(), "populate_english_test_#{System.unique_integer([:positive])}")
|
||||
po_dir = Path.join([temp_dir, "priv", "gettext", "en", "LC_MESSAGES"])
|
||||
File.mkdir_p!(po_dir)
|
||||
|
||||
on_exit(fn ->
|
||||
File.rm_rf!(temp_dir)
|
||||
end)
|
||||
|
||||
{:ok, temp_dir: temp_dir, po_dir: po_dir}
|
||||
end
|
||||
|
||||
describe "run/1" do
|
||||
test "populates empty msgstr with msgid", %{po_dir: po_dir} do
|
||||
po_file = Path.join(po_dir, "test.po")
|
||||
|
||||
File.write!(po_file, """
|
||||
msgid "Hello"
|
||||
msgstr ""
|
||||
|
||||
msgid "Goodbye"
|
||||
msgstr ""
|
||||
""")
|
||||
|
||||
# Change to temp directory so the task finds the files
|
||||
File.cd!(Path.dirname(Path.dirname(Path.dirname(Path.dirname(po_dir)))), fn ->
|
||||
capture_io(fn -> run([]) end)
|
||||
end)
|
||||
|
||||
content = File.read!(po_file)
|
||||
assert content =~ ~s(msgid "Hello"\nmsgstr "Hello")
|
||||
assert content =~ ~s(msgid "Goodbye"\nmsgstr "Goodbye")
|
||||
end
|
||||
|
||||
test "preserves existing non-empty msgstr", %{po_dir: po_dir} do
|
||||
po_file = Path.join(po_dir, "test.po")
|
||||
|
||||
File.write!(po_file, """
|
||||
msgid "Hello"
|
||||
msgstr "Hola"
|
||||
|
||||
msgid "Goodbye"
|
||||
msgstr ""
|
||||
""")
|
||||
|
||||
File.cd!(Path.dirname(Path.dirname(Path.dirname(Path.dirname(po_dir)))), fn ->
|
||||
capture_io(fn -> run([]) end)
|
||||
end)
|
||||
|
||||
content = File.read!(po_file)
|
||||
assert content =~ ~s(msgid "Hello"\nmsgstr "Hola")
|
||||
assert content =~ ~s(msgid "Goodbye"\nmsgstr "Goodbye")
|
||||
end
|
||||
|
||||
test "handles multiline msgid and msgstr", %{po_dir: po_dir} do
|
||||
po_file = Path.join(po_dir, "test.po")
|
||||
|
||||
File.write!(po_file, """
|
||||
msgid ""
|
||||
"This is a "
|
||||
"multiline string"
|
||||
msgstr ""
|
||||
""")
|
||||
|
||||
File.cd!(Path.dirname(Path.dirname(Path.dirname(Path.dirname(po_dir)))), fn ->
|
||||
capture_io(fn -> run([]) end)
|
||||
end)
|
||||
|
||||
content = File.read!(po_file)
|
||||
assert content =~ ~s(msgstr ""\n"This is a "\n"multiline string")
|
||||
end
|
||||
|
||||
test "preserves comments and metadata", %{po_dir: po_dir} do
|
||||
po_file = Path.join(po_dir, "test.po")
|
||||
|
||||
File.write!(po_file, """
|
||||
# Comment line
|
||||
## Another comment
|
||||
|
||||
msgid "Hello"
|
||||
msgstr ""
|
||||
""")
|
||||
|
||||
File.cd!(Path.dirname(Path.dirname(Path.dirname(Path.dirname(po_dir)))), fn ->
|
||||
capture_io(fn -> run([]) end)
|
||||
end)
|
||||
|
||||
content = File.read!(po_file)
|
||||
assert content =~ "# Comment line"
|
||||
assert content =~ "## Another comment"
|
||||
assert content =~ ~s(msgid "Hello"\nmsgstr "Hello")
|
||||
end
|
||||
|
||||
test "skips plural forms", %{po_dir: po_dir} do
|
||||
po_file = Path.join(po_dir, "test.po")
|
||||
|
||||
File.write!(po_file, """
|
||||
msgid "One item"
|
||||
msgid_plural "%{count} items"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "Regular message"
|
||||
msgstr ""
|
||||
""")
|
||||
|
||||
File.cd!(Path.dirname(Path.dirname(Path.dirname(Path.dirname(po_dir)))), fn ->
|
||||
capture_io(fn -> run([]) end)
|
||||
end)
|
||||
|
||||
content = File.read!(po_file)
|
||||
# Plural forms should not be modified
|
||||
assert content =~ ~s(msgstr[0] "")
|
||||
assert content =~ ~s(msgstr[1] "")
|
||||
# Regular message should be populated
|
||||
assert content =~ ~s(msgid "Regular message"\nmsgstr "Regular message")
|
||||
end
|
||||
|
||||
test "handles empty msgid", %{po_dir: po_dir} do
|
||||
po_file = Path.join(po_dir, "test.po")
|
||||
|
||||
File.write!(po_file, """
|
||||
msgid ""
|
||||
msgstr ""
|
||||
|
||||
msgid "Hello"
|
||||
msgstr ""
|
||||
""")
|
||||
|
||||
File.cd!(Path.dirname(Path.dirname(Path.dirname(Path.dirname(po_dir)))), fn ->
|
||||
capture_io(fn -> run([]) end)
|
||||
end)
|
||||
|
||||
content = File.read!(po_file)
|
||||
# Empty msgid should not be populated
|
||||
lines = String.split(content, "\n")
|
||||
empty_msgid_index = Enum.find_index(lines, &(&1 == ~s(msgid "")))
|
||||
assert Enum.at(lines, empty_msgid_index + 1) == ~s(msgstr "")
|
||||
# Non-empty msgid should be populated
|
||||
assert content =~ ~s(msgid "Hello"\nmsgstr "Hello")
|
||||
end
|
||||
|
||||
test "handles escaped characters in strings", %{po_dir: po_dir} do
|
||||
po_file = Path.join(po_dir, "test.po")
|
||||
|
||||
File.write!(po_file, """
|
||||
msgid "Line one\\nLine two"
|
||||
msgstr ""
|
||||
|
||||
msgid "Tab\\there"
|
||||
msgstr ""
|
||||
|
||||
msgid "Quote \\"here\\""
|
||||
msgstr ""
|
||||
""")
|
||||
|
||||
File.cd!(Path.dirname(Path.dirname(Path.dirname(Path.dirname(po_dir)))), fn ->
|
||||
capture_io(fn -> run([]) end)
|
||||
end)
|
||||
|
||||
content = File.read!(po_file)
|
||||
assert content =~ ~s(msgid "Line one\\nLine two"\nmsgstr "Line one\\nLine two")
|
||||
assert content =~ ~s(msgid "Tab\\there"\nmsgstr "Tab\\there")
|
||||
assert content =~ ~s(msgid "Quote \\"here\\""\nmsgstr "Quote \\"here\\"")
|
||||
end
|
||||
|
||||
test "processes multiple .po files", %{po_dir: po_dir} do
|
||||
po_file1 = Path.join(po_dir, "test1.po")
|
||||
po_file2 = Path.join(po_dir, "test2.po")
|
||||
|
||||
File.write!(po_file1, """
|
||||
msgid "Hello"
|
||||
msgstr ""
|
||||
""")
|
||||
|
||||
File.write!(po_file2, """
|
||||
msgid "Goodbye"
|
||||
msgstr ""
|
||||
""")
|
||||
|
||||
File.cd!(Path.dirname(Path.dirname(Path.dirname(Path.dirname(po_dir)))), fn ->
|
||||
output = capture_io(fn -> run([]) end)
|
||||
assert output =~ "2 English translation file(s)"
|
||||
end)
|
||||
|
||||
assert File.read!(po_file1) =~ ~s(msgid "Hello"\nmsgstr "Hello")
|
||||
assert File.read!(po_file2) =~ ~s(msgid "Goodbye"\nmsgstr "Goodbye")
|
||||
end
|
||||
|
||||
test "reports no changes when file already populated", %{po_dir: po_dir} do
|
||||
po_file = Path.join(po_dir, "test.po")
|
||||
|
||||
File.write!(po_file, """
|
||||
msgid "Hello"
|
||||
msgstr "Hello"
|
||||
""")
|
||||
|
||||
File.cd!(Path.dirname(Path.dirname(Path.dirname(Path.dirname(po_dir)))), fn ->
|
||||
output = capture_io(fn -> run([]) end)
|
||||
assert output =~ "No changes: test.po"
|
||||
end)
|
||||
end
|
||||
|
||||
test "reports updated when file is modified", %{po_dir: po_dir} do
|
||||
po_file = Path.join(po_dir, "test.po")
|
||||
|
||||
File.write!(po_file, """
|
||||
msgid "Hello"
|
||||
msgstr ""
|
||||
""")
|
||||
|
||||
File.cd!(Path.dirname(Path.dirname(Path.dirname(Path.dirname(po_dir)))), fn ->
|
||||
output = capture_io(fn -> run([]) end)
|
||||
assert output =~ "Updated: test.po"
|
||||
end)
|
||||
end
|
||||
|
||||
test "handles missing directory gracefully" do
|
||||
temp_dir = Path.join(System.tmp_dir!(), "nonexistent_#{System.unique_integer([:positive])}")
|
||||
File.mkdir_p!(temp_dir)
|
||||
|
||||
try do
|
||||
File.cd!(temp_dir, fn ->
|
||||
assert_raise Mix.Error, fn ->
|
||||
capture_io(fn -> run([]) end)
|
||||
end
|
||||
end)
|
||||
after
|
||||
File.rm_rf!(temp_dir)
|
||||
end
|
||||
end
|
||||
|
||||
test "reports when no .po files found", %{po_dir: po_dir} do
|
||||
File.cd!(Path.dirname(Path.dirname(Path.dirname(Path.dirname(po_dir)))), fn ->
|
||||
output = capture_io(fn -> run([]) end)
|
||||
assert output =~ "No .po files found"
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue