mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-15 13:03:00 +08:00
262 lines
9.0 KiB
Go
262 lines
9.0 KiB
Go
package openwrt
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/matthewlu070111/smart-srun/core/internal/domain"
|
|
)
|
|
|
|
// A real scan from a real client radio on a real campus.
|
|
//
|
|
// The numbers below are not chosen: they are what the device returned, and the
|
|
// fixture is that output with the BSSIDs and the neighbours' network names
|
|
// renamed. Nothing here asserts a value this test invented.
|
|
func TestARealScanIsReadCorrectly(t *testing.T) {
|
|
results, err := ParseScanResults("phy1-sta0",
|
|
testdata(t, "kwrt-25.12/iwinfo-scan.json"))
|
|
if err != nil {
|
|
t.Fatalf("ParseScanResults: %v", err)
|
|
}
|
|
if len(results) != 10 {
|
|
t.Fatalf("read %d results, want the 10 the device reported", len(results))
|
|
}
|
|
|
|
first := results[0]
|
|
if first.SSID != "jxnu_stu" {
|
|
t.Errorf("SSID = %q, want jxnu_stu", first.SSID)
|
|
}
|
|
// Lower-cased here. iwinfo prints it upper-case and UCI stores it
|
|
// lower-case, and comparing the two forms is how a pinned access point
|
|
// looks missing while it is sitting right there.
|
|
if first.BSSID != "02:00:5e:00:53:02" {
|
|
t.Errorf("BSSID = %q, want it lower-cased", first.BSSID)
|
|
}
|
|
if first.Signal != -45 || first.Channel != 52 || first.Frequency != 5260 {
|
|
t.Errorf("first result = %+v", first)
|
|
}
|
|
if first.Encryption.Enabled {
|
|
t.Error("the campus network is open and was read as encrypted")
|
|
}
|
|
if !first.Joinable() {
|
|
t.Error("a Master-mode entry is joinable")
|
|
}
|
|
}
|
|
|
|
// The campus network really is open, and there really are several of them.
|
|
//
|
|
// This is the case the AP-selection policy exists for, and it is worth pinning
|
|
// down from real data rather than from a fixture somebody designed: eight
|
|
// access points share one SSID at signals from -45 to -90 dBm, so "which one"
|
|
// is a question that has to be answered every time this radio connects.
|
|
func TestTheRealScanHasManyAccessPointsSharingOneSSID(t *testing.T) {
|
|
results, err := ParseScanResults("phy1-sta0",
|
|
testdata(t, "kwrt-25.12/iwinfo-scan.json"))
|
|
if err != nil {
|
|
t.Fatalf("ParseScanResults: %v", err)
|
|
}
|
|
|
|
campus := 0
|
|
seen := map[string]bool{}
|
|
for _, result := range results {
|
|
if result.SSID != "jxnu_stu" {
|
|
continue
|
|
}
|
|
campus++
|
|
if seen[result.BSSID] {
|
|
t.Errorf("BSSID %s appeared twice", result.BSSID)
|
|
}
|
|
seen[result.BSSID] = true
|
|
if result.Encryption.Enabled {
|
|
t.Errorf("%s reported the campus network as encrypted", result.BSSID)
|
|
}
|
|
}
|
|
if campus < 2 {
|
|
t.Fatalf("found %d campus access points; this fixture exists because "+
|
|
"there are several", campus)
|
|
}
|
|
}
|
|
|
|
// A protected neighbour is read with its suites intact.
|
|
//
|
|
// The authentication list is what decides whether an account's credentials fit,
|
|
// so it has to survive parsing as a list. Collapsing it to a single name is
|
|
// where a mixed-mode access point stops being matchable by one of the two kinds
|
|
// of client it admits.
|
|
func TestAProtectedNeighbourKeepsItsSuites(t *testing.T) {
|
|
results, err := ParseScanResults("phy1-sta0",
|
|
testdata(t, "kwrt-25.12/iwinfo-scan.json"))
|
|
if err != nil {
|
|
t.Fatalf("ParseScanResults: %v", err)
|
|
}
|
|
|
|
var protected *ScanResult
|
|
for index := range results {
|
|
if results[index].Encryption.Enabled {
|
|
protected = &results[index]
|
|
break
|
|
}
|
|
}
|
|
if protected == nil {
|
|
t.Fatal("the fixture has no encrypted network; it had one when captured")
|
|
}
|
|
if len(protected.Encryption.Authentication) == 0 {
|
|
t.Fatal("an encrypted access point came back with no authentication suites")
|
|
}
|
|
if protected.Encryption.Authentication[0] != "psk" {
|
|
t.Errorf("authentication = %v, want the psk the device reported",
|
|
protected.Encryption.Authentication)
|
|
}
|
|
if len(protected.Encryption.WPA) != 2 {
|
|
t.Errorf("wpa = %v, want the two versions the device reported",
|
|
protected.Encryption.WPA)
|
|
}
|
|
}
|
|
|
|
// Output this cannot read is not an empty neighbourhood.
|
|
//
|
|
// "No access points found" is a fact with consequences -- it is the answer that
|
|
// tells a pinned account its access point is gone -- and a parser must not
|
|
// produce it for input it failed to understand. The M04 lesson, applied to a
|
|
// different parser: an object that is simply not a scan reply unmarshals
|
|
// happily into a struct of optional fields.
|
|
func TestUnreadableOutputIsNotAnEmptyNeighbourhood(t *testing.T) {
|
|
for name, input := range map[string]string{
|
|
"empty": ``,
|
|
"not json": `<html>gateway login</html>`,
|
|
"another method": `{"devices":["phy0-ap0"]}`,
|
|
"an error object": `{"error":{"code":-32000}}`,
|
|
"an unrelated array": `[{"ssid":"x"}]`,
|
|
} {
|
|
results, err := ParseScanResults("phy1-sta0", []byte(input))
|
|
if err == nil {
|
|
t.Errorf("%s: parsed into %d results instead of failing",
|
|
name, len(results))
|
|
continue
|
|
}
|
|
if code, _ := domain.CodeOf(err); code != domain.CodeInternal {
|
|
t.Errorf("%s: code = %s, want Internal", name, code)
|
|
}
|
|
}
|
|
}
|
|
|
|
// An empty list, on the other hand, is a real answer and must survive as one.
|
|
func TestAnEmptyScanIsAnAnswerRatherThanAnError(t *testing.T) {
|
|
results, err := ParseScanResults("phy1-sta0", []byte(`{"results":[]}`))
|
|
if err != nil {
|
|
t.Fatalf("an empty scan was reported as a failure: %v", err)
|
|
}
|
|
if len(results) != 0 {
|
|
t.Errorf("read %d results from an empty scan", len(results))
|
|
}
|
|
}
|
|
|
|
// Only an infrastructure access point can be joined by a client.
|
|
//
|
|
// iwinfo lists ad-hoc and mesh peers in the same array, with an SSID and a
|
|
// signal reading, so nothing downstream would notice them. The captured scan
|
|
// contains only Master entries -- this radio saw no mesh -- so the other modes
|
|
// are checked here against the rule rather than against the device.
|
|
func TestOnlyMasterModeEntriesAreJoinable(t *testing.T) {
|
|
for mode, joinable := range map[string]bool{
|
|
"Master": true,
|
|
"master": true,
|
|
"Ad-Hoc": false,
|
|
"Mesh": false,
|
|
"Client": false,
|
|
"Monitor": false,
|
|
"": false,
|
|
} {
|
|
if got := (ScanResult{Mode: mode}).Joinable(); got != joinable {
|
|
t.Errorf("Joinable(%q) = %v, want %v", mode, got, joinable)
|
|
}
|
|
}
|
|
}
|
|
|
|
// respondTruncated is a reply the output limit cut short.
|
|
func (r *recordingRunner) respondTruncated(stdout []byte, program string, args ...string) {
|
|
r.respond(stdout, program, args...)
|
|
key := r.key(program, args)
|
|
result := r.responses[key]
|
|
result.StdoutTruncated = true
|
|
r.responses[key] = result
|
|
}
|
|
|
|
const scanRequest = `{"device":"phy1-sta0"}`
|
|
|
|
// A truncated scan is refused, not used.
|
|
//
|
|
// This is the one that would be invisible. A cut-off reply is still valid JSON
|
|
// only by accident, but even when it parses it is a shorter list of access
|
|
// points -- and a shorter list is indistinguishable from a quieter
|
|
// neighbourhood. The access point that got cut off is then "gone", which for a
|
|
// pinned account is a failure with a confident and wrong explanation.
|
|
func TestATruncatedScanIsRefusedRatherThanShortened(t *testing.T) {
|
|
runner := &recordingRunner{}
|
|
// Deliberately still-parseable: the point is that truncation is refused on
|
|
// its own evidence, not because the remains happened not to parse.
|
|
runner.respondTruncated([]byte(`{"results":[{"ssid":"jxnu_stu","mode":"Master"}]}`),
|
|
"ubus", "call", "iwinfo", "scan", scanRequest)
|
|
|
|
results, err := NewAdapter(runner).Scan(t.Context(), "phy1-sta0")
|
|
if err == nil {
|
|
t.Fatalf("a truncated scan produced %d results", len(results))
|
|
}
|
|
if code := codeOf(t, err); code != domain.CodeInternal {
|
|
t.Errorf("code = %s, want Internal", code)
|
|
}
|
|
}
|
|
|
|
// A name that is not a device never reaches ubus.
|
|
func TestScanRefusesANameThatIsNotADevice(t *testing.T) {
|
|
runner := &recordingRunner{}
|
|
adapter := NewAdapter(runner)
|
|
|
|
for _, device := range []string{"", "phy1 sta0", `phy1","x":"`, "$(id)",
|
|
"phy1\nsta0"} {
|
|
if _, err := adapter.Scan(t.Context(), device); err == nil {
|
|
t.Errorf("Scan(%q) succeeded", device)
|
|
}
|
|
}
|
|
if len(runner.calls) != 0 {
|
|
t.Errorf("a name that is not a device was passed to ubus: %v", runner.calls)
|
|
}
|
|
}
|
|
|
|
// The request is one encoded argument, built by the JSON encoder rather than by
|
|
// pasting the device name into a string.
|
|
func TestTheScanRequestIsOneEncodedArgument(t *testing.T) {
|
|
runner := &recordingRunner{}
|
|
runner.respond(testdata(t, "kwrt-25.12/iwinfo-scan.json"),
|
|
"ubus", "call", "iwinfo", "scan", scanRequest)
|
|
|
|
results, err := NewAdapter(runner).Scan(t.Context(), "phy1-sta0")
|
|
if err != nil {
|
|
t.Fatalf("Scan: %v", err)
|
|
}
|
|
if len(results) != 10 {
|
|
t.Errorf("read %d results through the adapter", len(results))
|
|
}
|
|
if len(runner.calls) != 1 || len(runner.calls[0]) != 5 {
|
|
t.Fatalf("calls = %v", runner.calls)
|
|
}
|
|
if got := runner.calls[0][4]; got != scanRequest {
|
|
t.Errorf("request = %s", got)
|
|
}
|
|
}
|
|
|
|
// The signal in a scan goes through the same correction as the one in an info
|
|
// call, because it is the same signed char printed by the same encoders.
|
|
func TestAScanSignalPrintedAsUnsignedIsReadAsNegative(t *testing.T) {
|
|
results, err := ParseScanResults("phy1-sta0",
|
|
[]byte(`{"results":[{"ssid":"x","bssid":"AA:BB:CC:DD:EE:FF","mode":"Master","signal":4294967244}]}`))
|
|
if err != nil {
|
|
t.Fatalf("ParseScanResults: %v", err)
|
|
}
|
|
if results[0].Signal != -52 {
|
|
t.Errorf("Signal = %d, want -52", results[0].Signal)
|
|
}
|
|
if results[0].BSSID != "aa:bb:cc:dd:ee:ff" {
|
|
t.Errorf("BSSID = %q, want it lower-cased", results[0].BSSID)
|
|
}
|
|
}
|