Beacons can now record a directional bearing ("omni" or 0-360 degrees)
and a beamwidth in degrees. The keying list grows from {on_off, fsk}
to include FM Voice, WSPR, and the full Q65A/B/C/D/E × 15/30/60/120
family, grouped via optgroups in the submission form.
337 lines
10 KiB
Elixir
337 lines
10 KiB
Elixir
defmodule Microwaveprop.BeaconsTest do
|
|
use Microwaveprop.DataCase
|
|
|
|
import Microwaveprop.AccountsFixtures
|
|
import Microwaveprop.BeaconsFixtures
|
|
|
|
alias Microwaveprop.Beacons
|
|
alias Microwaveprop.Beacons.Beacon
|
|
|
|
@invalid_attrs %{
|
|
frequency_mhz: nil,
|
|
callsign: nil,
|
|
grid: nil,
|
|
lat: nil,
|
|
lon: nil,
|
|
power_mw: nil,
|
|
height_ft: nil
|
|
}
|
|
|
|
describe "list_beacons/0" do
|
|
test "returns only approved beacons" do
|
|
u1 = user_fixture()
|
|
u2 = user_fixture()
|
|
{:ok, b1} = Beacons.create_beacon(u1, valid_beacon_attrs())
|
|
{:ok, b1} = Beacons.approve_beacon(b1)
|
|
_pending = beacon_fixture(u2, callsign: "W5TX")
|
|
assert [^b1] = Beacons.list_beacons()
|
|
end
|
|
end
|
|
|
|
describe "list_pending_beacons/0" do
|
|
test "returns only unapproved beacons" do
|
|
u1 = user_fixture()
|
|
u2 = user_fixture()
|
|
{:ok, approved} = Beacons.create_beacon(u1, valid_beacon_attrs())
|
|
{:ok, _approved} = Beacons.approve_beacon(approved)
|
|
pending = beacon_fixture(u2, callsign: "W5TX")
|
|
assert [^pending] = Beacons.list_pending_beacons()
|
|
end
|
|
end
|
|
|
|
describe "approve_beacon/1" do
|
|
test "marks the beacon as approved and broadcasts an update" do
|
|
user = user_fixture()
|
|
beacon = beacon_fixture(user)
|
|
refute beacon.approved
|
|
Beacons.subscribe_beacons()
|
|
assert {:ok, approved} = Beacons.approve_beacon(beacon)
|
|
assert approved.approved == true
|
|
assert_receive {:updated, %Beacon{approved: true}}
|
|
end
|
|
end
|
|
|
|
describe "get_beacon!/1" do
|
|
test "returns the beacon with given id" do
|
|
user = user_fixture()
|
|
beacon = beacon_fixture(user)
|
|
assert Beacons.get_beacon!(beacon.id).id == beacon.id
|
|
end
|
|
end
|
|
|
|
describe "create_beacon/2" do
|
|
test "with valid data creates a beacon and records the creator" do
|
|
user = user_fixture()
|
|
attrs = valid_beacon_attrs()
|
|
|
|
assert {:ok, %Beacon{} = beacon} = Beacons.create_beacon(user, attrs)
|
|
assert beacon.frequency_mhz == attrs.frequency_mhz
|
|
assert beacon.callsign == "W5HN"
|
|
assert beacon.lat == attrs.lat
|
|
assert beacon.lon == attrs.lon
|
|
assert beacon.power_mw == attrs.power_mw
|
|
assert beacon.height_ft == attrs.height_ft
|
|
assert beacon.user_id == user.id
|
|
end
|
|
|
|
test "anonymous submission creates a beacon with no user_id, unapproved" do
|
|
attrs = valid_beacon_attrs()
|
|
assert {:ok, %Beacon{} = beacon} = Beacons.create_beacon(nil, attrs)
|
|
assert beacon.user_id == nil
|
|
assert beacon.approved == false
|
|
end
|
|
|
|
test "new beacons default to unapproved" do
|
|
user = user_fixture()
|
|
assert {:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs())
|
|
assert beacon.approved == false
|
|
end
|
|
|
|
test "ignores client-supplied approved value" do
|
|
user = user_fixture()
|
|
attrs = Map.put(valid_beacon_attrs(), :approved, true)
|
|
assert {:ok, beacon} = Beacons.create_beacon(user, attrs)
|
|
assert beacon.approved == false
|
|
end
|
|
|
|
test "derives grid from lat/lon when grid is omitted" do
|
|
user = user_fixture()
|
|
# DFW is EM12
|
|
{:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs(lat: 32.897, lon: -97.038))
|
|
assert String.starts_with?(beacon.grid, "EM12")
|
|
end
|
|
|
|
test "derives lat/lon from grid when lat/lon are omitted" do
|
|
user = user_fixture()
|
|
|
|
attrs =
|
|
[grid: "EM12kp"]
|
|
|> valid_beacon_attrs()
|
|
|> Map.drop([:lat, :lon])
|
|
|
|
assert {:ok, beacon} = Beacons.create_beacon(user, attrs)
|
|
assert beacon.grid == "EM12kp"
|
|
assert_in_delta beacon.lat, 32.6458, 0.01
|
|
assert_in_delta beacon.lon, -97.125, 0.01
|
|
end
|
|
|
|
test "keeps an explicitly provided valid grid" do
|
|
user = user_fixture()
|
|
{:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs(grid: "EM13"))
|
|
assert beacon.grid == "EM13"
|
|
end
|
|
|
|
test "rejects an invalid grid" do
|
|
user = user_fixture()
|
|
|
|
assert {:error, changeset} =
|
|
Beacons.create_beacon(user, valid_beacon_attrs(grid: "ZZ99"))
|
|
|
|
assert "is not a valid Maidenhead grid" in errors_on(changeset).grid
|
|
end
|
|
|
|
test "defaults on_the_air to true" do
|
|
user = user_fixture()
|
|
{:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs())
|
|
assert beacon.on_the_air == true
|
|
end
|
|
|
|
test "accepts on_the_air: false" do
|
|
user = user_fixture()
|
|
{:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs(on_the_air: false))
|
|
assert beacon.on_the_air == false
|
|
end
|
|
|
|
test "defaults keying to on_off" do
|
|
user = user_fixture()
|
|
{:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs())
|
|
assert beacon.keying == "on_off"
|
|
end
|
|
|
|
test "accepts keying: fsk" do
|
|
user = user_fixture()
|
|
{:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs(keying: "fsk"))
|
|
assert beacon.keying == "fsk"
|
|
end
|
|
|
|
test "rejects an unknown keying" do
|
|
user = user_fixture()
|
|
|
|
assert {:error, changeset} =
|
|
Beacons.create_beacon(user, valid_beacon_attrs(keying: "cw"))
|
|
|
|
assert "is invalid" in errors_on(changeset).keying
|
|
end
|
|
|
|
test "accepts fm_voice keying" do
|
|
user = user_fixture()
|
|
{:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs(keying: "fm_voice"))
|
|
assert beacon.keying == "fm_voice"
|
|
end
|
|
|
|
test "accepts wspr keying" do
|
|
user = user_fixture()
|
|
{:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs(keying: "wspr"))
|
|
assert beacon.keying == "wspr"
|
|
end
|
|
|
|
test "accepts all Q65 keying variants" do
|
|
user = user_fixture()
|
|
|
|
variants =
|
|
for letter <- ~w(a b c d e), period <- ~w(15 30 60 120), do: "q65#{letter}_#{period}"
|
|
|
|
for keying <- variants do
|
|
assert {:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs(keying: keying))
|
|
assert beacon.keying == keying
|
|
end
|
|
end
|
|
|
|
test "defaults bearing to omni" do
|
|
user = user_fixture()
|
|
{:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs())
|
|
assert beacon.bearing == "omni"
|
|
end
|
|
|
|
test "normalizes empty bearing to omni" do
|
|
user = user_fixture()
|
|
{:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs(bearing: ""))
|
|
assert beacon.bearing == "omni"
|
|
end
|
|
|
|
test "normalizes 'Omni' to 'omni'" do
|
|
user = user_fixture()
|
|
{:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs(bearing: "Omni"))
|
|
assert beacon.bearing == "omni"
|
|
end
|
|
|
|
test "accepts a numeric bearing" do
|
|
user = user_fixture()
|
|
{:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs(bearing: "90"))
|
|
assert beacon.bearing == "90"
|
|
end
|
|
|
|
test "accepts bearing of 0 and 360" do
|
|
user = user_fixture()
|
|
{:ok, b0} = Beacons.create_beacon(user, valid_beacon_attrs(bearing: "0"))
|
|
assert b0.bearing == "0"
|
|
{:ok, b360} = Beacons.create_beacon(user, valid_beacon_attrs(bearing: "360"))
|
|
assert b360.bearing == "360"
|
|
end
|
|
|
|
test "rejects a non-numeric non-omni bearing" do
|
|
user = user_fixture()
|
|
|
|
assert {:error, changeset} =
|
|
Beacons.create_beacon(user, valid_beacon_attrs(bearing: "north"))
|
|
|
|
assert errors_on(changeset).bearing != []
|
|
end
|
|
|
|
test "rejects a bearing greater than 360" do
|
|
user = user_fixture()
|
|
|
|
assert {:error, changeset} =
|
|
Beacons.create_beacon(user, valid_beacon_attrs(bearing: "400"))
|
|
|
|
assert errors_on(changeset).bearing != []
|
|
end
|
|
|
|
test "rejects a negative bearing" do
|
|
user = user_fixture()
|
|
|
|
assert {:error, changeset} =
|
|
Beacons.create_beacon(user, valid_beacon_attrs(bearing: "-10"))
|
|
|
|
assert errors_on(changeset).bearing != []
|
|
end
|
|
|
|
test "stores beamwidth_deg" do
|
|
user = user_fixture()
|
|
{:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs(beamwidth_deg: 30.0))
|
|
assert beacon.beamwidth_deg == 30.0
|
|
end
|
|
|
|
test "rejects a non-positive beamwidth" do
|
|
user = user_fixture()
|
|
|
|
assert {:error, changeset} =
|
|
Beacons.create_beacon(user, valid_beacon_attrs(beamwidth_deg: 0))
|
|
|
|
assert errors_on(changeset).beamwidth_deg != []
|
|
end
|
|
|
|
test "rejects beamwidth > 360" do
|
|
user = user_fixture()
|
|
|
|
assert {:error, changeset} =
|
|
Beacons.create_beacon(user, valid_beacon_attrs(beamwidth_deg: 400))
|
|
|
|
assert errors_on(changeset).beamwidth_deg != []
|
|
end
|
|
|
|
test "stores notes" do
|
|
user = user_fixture()
|
|
notes = "Antenna 20 ft AGL, horizontal polarization"
|
|
{:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs(notes: notes))
|
|
assert beacon.notes == notes
|
|
end
|
|
|
|
test "upcases the callsign" do
|
|
user = user_fixture()
|
|
{:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs(callsign: "w5hn"))
|
|
assert beacon.callsign == "W5HN"
|
|
end
|
|
|
|
test "rejects non-positive frequency" do
|
|
user = user_fixture()
|
|
|
|
assert {:error, changeset} =
|
|
Beacons.create_beacon(user, valid_beacon_attrs(frequency_mhz: 0))
|
|
|
|
assert "must be greater than 0" in errors_on(changeset).frequency_mhz
|
|
end
|
|
|
|
test "with invalid data returns error changeset" do
|
|
user = user_fixture()
|
|
assert {:error, %Ecto.Changeset{}} = Beacons.create_beacon(user, @invalid_attrs)
|
|
end
|
|
end
|
|
|
|
describe "update_beacon/2" do
|
|
test "with valid data updates the beacon" do
|
|
user = user_fixture()
|
|
beacon = beacon_fixture(user)
|
|
|
|
update_attrs = %{frequency_mhz: 24_192.1, power_mw: 5.0}
|
|
|
|
assert {:ok, %Beacon{} = beacon} = Beacons.update_beacon(beacon, update_attrs)
|
|
assert beacon.frequency_mhz == 24_192.1
|
|
assert beacon.power_mw == 5.0
|
|
end
|
|
|
|
test "with invalid data returns error changeset" do
|
|
user = user_fixture()
|
|
beacon = beacon_fixture(user)
|
|
assert {:error, %Ecto.Changeset{}} = Beacons.update_beacon(beacon, @invalid_attrs)
|
|
assert Beacons.get_beacon!(beacon.id).callsign == beacon.callsign
|
|
end
|
|
end
|
|
|
|
describe "delete_beacon/1" do
|
|
test "deletes the beacon" do
|
|
user = user_fixture()
|
|
beacon = beacon_fixture(user)
|
|
assert {:ok, %Beacon{}} = Beacons.delete_beacon(beacon)
|
|
assert_raise Ecto.NoResultsError, fn -> Beacons.get_beacon!(beacon.id) end
|
|
end
|
|
end
|
|
|
|
describe "change_beacon/1" do
|
|
test "returns a beacon changeset" do
|
|
user = user_fixture()
|
|
beacon = beacon_fixture(user)
|
|
assert %Ecto.Changeset{} = Beacons.change_beacon(beacon)
|
|
end
|
|
end
|
|
end
|