mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-15 04:53:00 +08:00
249 lines
11 KiB
Go
249 lines
11 KiB
Go
//go:build unix
|
|
|
|
package daemon
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/matthewlu070111/smart-srun/core/internal/config"
|
|
"github.com/matthewlu070111/smart-srun/core/internal/domain"
|
|
"github.com/matthewlu070111/smart-srun/core/internal/policy"
|
|
"github.com/matthewlu070111/smart-srun/core/internal/wireless"
|
|
)
|
|
|
|
const wizardJobID = "0123456789abcdef0123456789abcdef"
|
|
const idleWizardUCI = "wireless.radio1=wifi-device\nwireless.radio1.band='5g'\nwireless.home=wifi-iface\nwireless.home.device='radio1'\nwireless.home.mode='ap'\nwireless.home.ssid='HomeNet'\nwireless.home.network='lan'\n"
|
|
const wizardFirewallExport = "package firewall\nconfig zone 'wan_zone'\n option name 'wan'\n option input 'REJECT'\n option masq '1'\n list network 'wan'\n list network 'wwan'\nconfig forwarding 'lan_wan'\n option src 'lan'\n option dest 'wan'\n"
|
|
|
|
func wizardFixture(t *testing.T) *wirelessFixture {
|
|
t.Helper()
|
|
f := newWirelessFixture(t)
|
|
f.radio.clock = policy.SystemClock{}
|
|
f.radio.settleWait = 30 * time.Millisecond
|
|
f.radio.settlePoll = time.Millisecond
|
|
f.radio.wizardStore = f.store
|
|
f.device.answer(idleWizardUCI, "uci", "show", "wireless")
|
|
f.device.answer("network.lan=interface\nnetwork.lan.proto='static'\nnetwork.wwan=interface\nnetwork.wwan.proto='dhcp'\n", "uci", "show", "network")
|
|
f.device.answer(wizardFirewallExport, "uci", "-n", "export", "firewall")
|
|
f.device.answer(statusWithoutStation, "ubus", "call", "network.wireless", "status")
|
|
f.device.answer(scanResults, "ubus", iwinfoScan("phy1-ap0")...)
|
|
f.device.answer(associatedInfo, "ubus", iwinfoInfo("phy1-sta0")...)
|
|
f.device.answer(wwanUp, "ubus", "call", "network.interface.wwan", "status")
|
|
return f
|
|
}
|
|
|
|
func awaitWifi(t *testing.T, r *running, state string) WifiSetupView {
|
|
t.Helper()
|
|
deadline := time.Now().Add(3 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
var view WifiSetupView
|
|
if err := json.Unmarshal(r.call("setup_wifi.status", wifiJobParams{Job: wizardJobID, Session: "browser"}), &view); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if view.State == state {
|
|
return view
|
|
}
|
|
if view.State == "failed" && state != "failed" {
|
|
t.Fatalf("wifi failed: %s", view.Message)
|
|
}
|
|
time.Sleep(5 * time.Millisecond)
|
|
}
|
|
t.Fatalf("wifi did not reach %s", state)
|
|
return WifiSetupView{}
|
|
}
|
|
|
|
func startWizard(t *testing.T, failAssociation bool) (*running, *wirelessFixture, WifiSetupParams) {
|
|
t.Helper()
|
|
f := wizardFixture(t)
|
|
if !failAssociation {
|
|
f.store.onStage = func() { f.device.answer(statusWithStation, "ubus", "call", "network.wireless", "status") }
|
|
}
|
|
r := start(t, func(o *Options) { o.Runner = switchRunner{}; o.wizardDevice = f.radio })
|
|
p := WifiSetupParams{Job: wizardJobID, Session: "browser", SSID: "jxnu_stu", Encryption: "none"}
|
|
r.call("setup_wifi.start", p)
|
|
return r, f, p
|
|
}
|
|
|
|
func TestWifiWizardSavesExactlySettledConnectionAndCommitIsIdempotent(t *testing.T) {
|
|
r, f, p := startWizard(t, false)
|
|
view := awaitWifi(t, r, "ready")
|
|
if view.Iface != "wwan" || view.Radio != "radio1" {
|
|
t.Fatal("wrong settled uplink")
|
|
}
|
|
if !strings.Contains(string(r.call("setup_wifi.start", p)), "ready") {
|
|
t.Fatal("retry replaced job")
|
|
}
|
|
r.callExpectingError("setup_wifi.status", wifiJobParams{Job: p.Job, Session: "foreign"})
|
|
r.callExpectingError("setup_wifi.cancel", wifiJobParams{Job: p.Job, Session: "foreign"})
|
|
r.callExpectingError("config.apply", json.RawMessage(`{"expected_revision":0,"settings":{"enabled":true}}`))
|
|
zero := uint64(0)
|
|
user, password, wrongSSID, wrongKey := "student", " account secret ", "wrong SSID", "wrong wifi secret"
|
|
params := wifiAccountParams{Job: p.Job, Session: p.Session, ExpectedRevision: &zero, Account: &config.CampusPatch{UserID: &user, Password: &password, SSID: &wrongSSID, Key: &wrongKey}}
|
|
var saved ConfigWriteResult
|
|
if err := json.Unmarshal(r.call("setup_wifi.account", params), &saved); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if saved.Revision != 1 || saved.ID != "c1" || saved.Config.CampusAccounts[0].Password != "" {
|
|
t.Fatal("bad save result")
|
|
}
|
|
r.call("setup_wifi.commit", params)
|
|
awaitWifi(t, r, "done")
|
|
repo, err := config.Open(r.paths.ConfigFile())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg := repo.Snapshot()
|
|
if cfg.Revision != 1 || len(cfg.CampusAccounts) != 1 {
|
|
t.Fatal("duplicate save created another account")
|
|
}
|
|
account := cfg.CampusAccounts[0]
|
|
if account.SSID != "jxnu_stu" || account.Radio != "radio1" || account.Key != "" || account.Password != password || cfg.STAIface != "wwan" {
|
|
t.Fatal("saved browser fields instead of settled connection, or changed credential whitespace")
|
|
}
|
|
if _, found, _ := (wireless.Paths{Dir: r.paths.Recovery() + "/setup-wifi/wireless"}).LoadJournal(); found {
|
|
t.Fatal("retained secret backup after confirmation")
|
|
}
|
|
before := f.store.batches()
|
|
r.call("setup_wifi.cancel", wifiJobParams{Job: p.Job, Session: p.Session})
|
|
if f.store.batches() != before {
|
|
t.Fatal("cancel undid confirmed connection")
|
|
}
|
|
}
|
|
|
|
func TestWifiWizardCancelAndFailedAssociationRestoreOnlyOwnedOptions(t *testing.T) {
|
|
for _, fail := range []bool{false, true} {
|
|
t.Run(map[bool]string{false: "cancel", true: "association-failure"}[fail], func(t *testing.T) {
|
|
r, f, p := startWizard(t, fail)
|
|
if fail {
|
|
awaitWifi(t, r, "failed")
|
|
} else {
|
|
awaitWifi(t, r, "ready")
|
|
r.call("setup_wifi.cancel", wifiJobParams{Job: p.Job, Session: p.Session})
|
|
awaitWifi(t, r, "cancelled")
|
|
}
|
|
f.store.mu.Lock()
|
|
remaining := len(f.store.values)
|
|
f.store.mu.Unlock()
|
|
if remaining != 0 {
|
|
t.Fatal("temporary station survived rollback")
|
|
}
|
|
if r.status().ConfigRevision != 0 {
|
|
t.Fatal("temporary connection saved account configuration")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWifiWizardShutdownUndoesUnconfirmedConnection(t *testing.T) {
|
|
r, f, _ := startWizard(t, false)
|
|
awaitWifi(t, r, "ready")
|
|
r.stop()
|
|
if err := r.wait(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f.store.mu.Lock()
|
|
defer f.store.mu.Unlock()
|
|
if len(f.store.values) != 0 {
|
|
t.Fatal("service stop left temporary wireless changes")
|
|
}
|
|
}
|
|
|
|
func TestWifiWizardRefusesChangedAssociationAtSave(t *testing.T) {
|
|
r, f, p := startWizard(t, false)
|
|
awaitWifi(t, r, "ready")
|
|
f.device.answer(strings.ReplaceAll(associatedInfo, "jxnu_stu", "other-network"), "ubus", iwinfoInfo("phy1-sta0")...)
|
|
zero := uint64(0)
|
|
user := "student"
|
|
err := r.callExpectingError("setup_wifi.account", wifiAccountParams{Job: p.Job, Session: p.Session, ExpectedRevision: &zero, Account: &config.CampusPatch{UserID: &user}})
|
|
if codeOf(t, err) != domain.CodeConflict {
|
|
t.Fatal("wrong refusal")
|
|
}
|
|
if r.status().ConfigRevision != 0 {
|
|
t.Fatal("saved changed association")
|
|
}
|
|
}
|
|
|
|
func TestWifiWizardPlanProtectsActiveStationAndAmbiguousSecurity(t *testing.T) {
|
|
f := wizardFixture(t)
|
|
p := WifiSetupParams{Job: wizardJobID, SSID: "jxnu_stu", Encryption: "auto"}
|
|
f.device.answer(idleWizardUCI+"wireless.other=wifi-iface\nwireless.other.device='radio1'\nwireless.other.mode='sta'\nwireless.other.ssid='phone'\nwireless.other.network='wwan'\n", "uci", "show", "wireless")
|
|
if _, _, err := f.radio.wizardPlan(t.Context(), p); err == nil {
|
|
t.Fatal("would overwrite active phone uplink")
|
|
}
|
|
f.device.answer(idleWizardUCI, "uci", "show", "wireless")
|
|
f.device.answer(strings.ReplaceAll(scanResults, "HomeNet-5", "jxnu_stu"), "ubus", iwinfoScan("phy1-ap0")...)
|
|
if _, _, err := f.radio.wizardPlan(t.Context(), p); err == nil {
|
|
t.Fatal("auto chose between open/protected same-name networks")
|
|
}
|
|
p.Encryption = "none"
|
|
if _, _, err := f.radio.wizardPlan(t.Context(), p); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestWifiWizardExpiresWithoutBrowserPolling(t *testing.T) {
|
|
f := wizardFixture(t)
|
|
f.radio.clock = f.clock
|
|
f.store.onStage = func() { f.device.answer(statusWithStation, "ubus", "call", "network.wireless", "status") }
|
|
r := start(t, func(o *Options) { o.Runner = switchRunner{}; o.wizardDevice = f.radio; o.Clock = f.clock })
|
|
r.call("setup_wifi.start", WifiSetupParams{Job: wizardJobID, Session: "browser", SSID: "jxnu_stu", Encryption: "none"})
|
|
awaitWifi(t, r, "ready")
|
|
f.clock.Advance(16 * time.Minute)
|
|
awaitWifi(t, r, "cancelled")
|
|
f.store.mu.Lock()
|
|
defer f.store.mu.Unlock()
|
|
if len(f.store.values) != 0 {
|
|
t.Fatal("expired job left UCI changes")
|
|
}
|
|
}
|
|
|
|
func TestWifiWizardReusesVerifiedExistingStationWithoutWritingUCI(t *testing.T) {
|
|
f := wizardFixture(t)
|
|
f.device.answer(idleWizardUCI+"wireless.jxnu_sta_radio1=wifi-iface\nwireless.jxnu_sta_radio1.device='radio1'\nwireless.jxnu_sta_radio1.mode='sta'\nwireless.jxnu_sta_radio1.ssid='jxnu_stu'\nwireless.jxnu_sta_radio1.network='wwan'\nwireless.jxnu_sta_radio1.encryption='none'\n", "uci", "show", "wireless")
|
|
f.device.answer(statusWithStation, "ubus", "call", "network.wireless", "status")
|
|
r := start(t, func(o *Options) { o.Runner = switchRunner{}; o.wizardDevice = f.radio })
|
|
r.call("setup_wifi.start", WifiSetupParams{Job: wizardJobID, Session: "browser", SSID: "jxnu_stu", Encryption: "none"})
|
|
awaitWifi(t, r, "connected")
|
|
r.call("setup_wifi.cancel", wifiJobParams{Job: wizardJobID, Session: "browser"})
|
|
awaitWifi(t, r, "cancelled")
|
|
if f.store.batches() != 0 {
|
|
t.Fatal("reused connection was modified")
|
|
}
|
|
}
|
|
|
|
func TestWifiWizardCancelWhileAssociatingDoesNotLeaveChanges(t *testing.T) {
|
|
f := wizardFixture(t)
|
|
f.radio.settleWait = 30 * time.Second
|
|
r := start(t, func(o *Options) { o.Runner = switchRunner{}; o.wizardDevice = f.radio })
|
|
r.call("setup_wifi.start", WifiSetupParams{Job: wizardJobID, Session: "browser", SSID: "jxnu_stu", Encryption: "none"})
|
|
awaitWifi(t, r, "connecting")
|
|
r.call("setup_wifi.cancel", wifiJobParams{Job: wizardJobID, Session: "browser"})
|
|
awaitWifi(t, r, "cancelled")
|
|
f.store.mu.Lock()
|
|
defer f.store.mu.Unlock()
|
|
if len(f.store.values) != 0 {
|
|
t.Fatal("cancelled connection left changes")
|
|
}
|
|
}
|
|
|
|
func TestWifiWizardNewAccountBecomesCurrentDefault(t *testing.T) {
|
|
f := wizardFixture(t)
|
|
f.store.onStage = func() { f.device.answer(statusWithStation, "ubus", "call", "network.wireless", "status") }
|
|
r := start(t, func(o *Options) { o.Runner = switchRunner{}; o.wizardDevice = f.radio })
|
|
r.writeConfig("campus.upsert", `{"expected_revision":0,"account":{"user_id":"old","wired_iface":"wan"}}`)
|
|
r.call("setup_wifi.start", WifiSetupParams{Job: wizardJobID, Session: "browser", SSID: "jxnu_stu", Encryption: "none"})
|
|
awaitWifi(t, r, "ready")
|
|
one := uint64(1)
|
|
user := "new"
|
|
var saved ConfigWriteResult
|
|
if err := json.Unmarshal(r.call("setup_wifi.account", wifiAccountParams{Job: wizardJobID, Session: "browser", ExpectedRevision: &one, Account: &config.CampusPatch{UserID: &user}}), &saved); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if saved.ID != "c2" || saved.Config.Selection.ActiveCampusID != "c2" || saved.Config.Selection.DefaultCampusID != "c2" {
|
|
t.Fatal("wizard did not select the newly saved account")
|
|
}
|
|
}
|