package sat import ( "os" "path/filepath" "testing" ) // The shipped list has to be readable and consistent — it is embedded, so a // mistake in it is a mistake in every build. func TestShippedBirds(t *testing.T) { b := &Birds{} if err := b.parse(shippedBirds); err != nil { t.Fatalf("birds.json does not parse: %v", err) } if b.Len() < 5 { t.Fatalf("only %d satellites shipped", b.Len()) } for _, bird := range b.All() { if len(bird.Transponders) == 0 { t.Errorf("%s has no transponder", bird.Name) } for _, tr := range bird.Transponders { if tr.DownLo <= 0 { t.Errorf("%s / %s: no downlink", bird.Name, tr.Label) } if tr.DownHi != 0 && tr.DownHi <= tr.DownLo { t.Errorf("%s / %s: downlink passband runs backwards", bird.Name, tr.Label) } if tr.UpHi != 0 && tr.UpHi <= tr.UpLo { t.Errorf("%s / %s: uplink passband runs backwards", bird.Name, tr.Label) } // A linear transponder whose two passbands are different widths cannot // map one onto the other, and the split would drift across the pass. if tr.Linear() && (tr.DownHi-tr.DownLo) != (tr.UpHi-tr.UpLo) { t.Errorf("%s / %s: passbands are %d and %d Hz wide", bird.Name, tr.Label, tr.DownHi-tr.DownLo, tr.UpHi-tr.UpLo) } } } } func TestFindByAlias(t *testing.T) { b := &Birds{} if err := b.parse(shippedBirds); err != nil { t.Fatal(err) } // Every spelling on the left is one an operator or a feed actually uses. for _, tc := range []struct{ query, want string }{ {"AO-91", "AO-91"}, {"RADFXSAT (FOX-1B)", "AO-91"}, {"radfxsat", "AO-91"}, {"ISS (ZARYA)", "ISS (ZARYA)"}, {"ISS", "ISS (ZARYA)"}, {"SAUDISAT 1C (SO-50)", "SO-50"}, {"so 50", "SO-50"}, {"QO-100", "QO-100"}, {"ESHAIL-2", "QO-100"}, {"Es'hail 2", "QO-100"}, } { got, ok := b.Find(tc.query) if !ok { t.Errorf("%q was not found", tc.query) continue } if got.Name != tc.want { t.Errorf("%q found %q, wanted %q", tc.query, got.Name, tc.want) } } if _, ok := b.Find("NOAA 15"); ok { t.Error("a weather satellite should not carry an amateur frequency plan") } } // The uplink maths is the part that matters on the air: a station worked at one // end of an inverting transponder has to be answered at the other. func TestUplinkFor(t *testing.T) { inv := Transponder{ DownLo: 435800000, DownHi: 435900000, UpLo: 145900000, UpHi: 146000000, Inverting: true, } straight := Transponder{ DownLo: 29400000, DownHi: 29500000, UpLo: 145850000, UpHi: 145950000, } fm := Transponder{DownLo: 436795000, UpLo: 145850000} for _, tc := range []struct { name string tr Transponder down int64 want int64 }{ {"inverting, bottom of the downlink", inv, 435800000, 146000000}, {"inverting, top of the downlink", inv, 435900000, 145900000}, {"inverting, 30 kHz up", inv, 435830000, 145970000}, {"straight, bottom", straight, 29400000, 145850000}, {"straight, 25 kHz up", straight, 29425000, 145875000}, {"FM channel ignores the tuned downlink", fm, 436798000, 145850000}, {"below the passband is clamped", inv, 435700000, 146000000}, {"above the passband is clamped", inv, 436000000, 145900000}, } { if got := tc.tr.UplinkFor(tc.down); got != tc.want { t.Errorf("%s: got %d, wanted %d", tc.name, got, tc.want) } } // Receive-only: a beacon has nothing to answer on. if got := (Transponder{DownLo: 145800000}).UplinkFor(145800000); got != 0 { t.Errorf("a receive-only transponder gave an uplink of %d", got) } } // Whichever end the operator takes hold of, the pair has to agree. func TestDownlinkForRoundTrip(t *testing.T) { for _, tr := range []Transponder{ {DownLo: 435800000, DownHi: 435900000, UpLo: 145900000, UpHi: 146000000, Inverting: true}, {DownLo: 29400000, DownHi: 29500000, UpLo: 145850000, UpHi: 145950000}, } { for _, down := range []int64{tr.DownLo, tr.Centre(), tr.DownHi} { if got := tr.DownlinkFor(tr.UplinkFor(down)); got != down { t.Errorf("inverting=%v: %d → uplink → %d", tr.Inverting, down, got) } } } } func TestLoadBirdsWritesTheEditableCopy(t *testing.T) { dir := t.TempDir() b, err := LoadBirds(dir) if err != nil { t.Fatal(err) } shipped := b.Len() path := filepath.Join(dir, BirdsName) if _, err := os.Stat(path); err != nil { t.Fatalf("the editable copy was not written: %v", err) } // An operator's own list is what gets used from then on. mine := `[{"name":"MY-SAT","transponders":[{"label":"FM","mode":"FM","down_lo":1,"up_lo":2}]}]` if err := os.WriteFile(path, []byte(mine), 0o644); err != nil { t.Fatal(err) } b, err = LoadBirds(dir) if err != nil { t.Fatal(err) } if b.Len() != 1 { t.Fatalf("the operator's list was not used: %d satellites", b.Len()) } // And a broken one falls back without destroying what they wrote. if err := os.WriteFile(path, []byte("[{oops"), 0o644); err != nil { t.Fatal(err) } b, err = LoadBirds(dir) if err == nil { t.Error("a broken list was accepted silently") } if b.Len() != shipped { t.Errorf("the shipped list did not take over: %d satellites", b.Len()) } if data, _ := os.ReadFile(path); string(data) != "[{oops" { t.Error("the operator's broken file was overwritten") } }