Default the update repo to the Gitea host
Settles the first open question in claude.md: the update endpoint is gitea.apointless.space/bsncubed/ipswap. It was left empty — which disables the checker — only because the host was unconfirmed. Load unmarshals over the defaults, so an absent update_repo takes the new default while an explicitly empty one stays empty. That distinction is the off switch, and forks depend on it, so it is now pinned by a test. Adds the config package's first tests while here: the defaulting rules above, the repair path for a nonsensical mask style or poll interval, and the guarantee that a corrupt config.json still yields usable defaults rather than stopping the app from starting. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -106,10 +106,14 @@ localised and parsing it breaks on a non-English Windows.
|
|||||||
|
|
||||||
## Updates
|
## Updates
|
||||||
|
|
||||||
Set an update repository in Settings (`https://host/owner/repo`) and ipswap
|
ipswap checks `https://gitea.apointless.space/bsncubed/ipswap` by default. The
|
||||||
checks its Gitea releases API at startup, in a goroutine, behind a short
|
check runs at startup in a goroutine, behind a short timeout, and fails
|
||||||
timeout, failing silently to the log — a laptop on a customer site usually
|
silently to the log — a laptop on a customer site usually cannot reach the
|
||||||
cannot reach the host, and that is not an error worth showing.
|
host, and that is not an error worth showing.
|
||||||
|
|
||||||
|
Point it elsewhere, or clear it to switch the check off entirely, in Settings
|
||||||
|
(`update_repo` in `config.json`). Forks should change it: otherwise they will
|
||||||
|
offer their users an upstream binary.
|
||||||
|
|
||||||
A newer tag adds "Update available — vX.Y.Z" to the top of the tray menu.
|
A newer tag adds "Update available — vX.Y.Z" to the top of the tray menu.
|
||||||
Clicking it downloads the `.exe`, verifies its SHA256 against the release, and
|
Clicking it downloads the `.exe`, verifies its SHA256 against the release, and
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ type Settings struct {
|
|||||||
|
|
||||||
// UpdateRepo is the Gitea repo polled for releases, as
|
// UpdateRepo is the Gitea repo polled for releases, as
|
||||||
// "https://host/owner/repo". Empty disables the update checker entirely,
|
// "https://host/owner/repo". Empty disables the update checker entirely,
|
||||||
// which is the default until the host is confirmed.
|
// which is what a fork or a private build should set it to.
|
||||||
UpdateRepo string `json:"update_repo"`
|
UpdateRepo string `json:"update_repo"`
|
||||||
|
|
||||||
// ActivePollSeconds is how often the live adapter state is re-read to work
|
// ActivePollSeconds is how often the live adapter state is re-read to work
|
||||||
@@ -73,12 +73,16 @@ type Settings struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Default is the settings a fresh install starts with.
|
// Default is the settings a fresh install starts with.
|
||||||
|
// DefaultUpdateRepo is where release builds look for updates. Set it to "" in
|
||||||
|
// config.json (or clear the field in Settings) to turn the check off entirely.
|
||||||
|
const DefaultUpdateRepo = "https://gitea.apointless.space/bsncubed/ipswap"
|
||||||
|
|
||||||
func Default() Settings {
|
func Default() Settings {
|
||||||
return Settings{
|
return Settings{
|
||||||
MaskStyle: MaskPrefix,
|
MaskStyle: MaskPrefix,
|
||||||
StartWithWindows: false,
|
StartWithWindows: false,
|
||||||
CheckUpdates: true,
|
CheckUpdates: true,
|
||||||
UpdateRepo: "",
|
UpdateRepo: DefaultUpdateRepo,
|
||||||
ActivePollSeconds: 30,
|
ActivePollSeconds: 30,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoadMissingFileReturnsDefaults(t *testing.T) {
|
||||||
|
s, err := Load(filepath.Join(t.TempDir(), "config.json"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("a missing config must not be an error: %v", err)
|
||||||
|
}
|
||||||
|
if s != Default() {
|
||||||
|
t.Errorf("expected defaults, got %+v", s)
|
||||||
|
}
|
||||||
|
if s.UpdateRepo != DefaultUpdateRepo {
|
||||||
|
t.Errorf("update repo default = %q, want %q", s.UpdateRepo, DefaultUpdateRepo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// An absent field takes the default, but an explicitly empty one must stay
|
||||||
|
// empty — that is how the update check is turned off.
|
||||||
|
func TestExplicitEmptyUpdateRepoDisablesTheCheck(t *testing.T) {
|
||||||
|
path := filepath.Join(t.TempDir(), "config.json")
|
||||||
|
if err := os.WriteFile(path, []byte(`{"update_repo": ""}`), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := Load(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if s.UpdateRepo != "" {
|
||||||
|
t.Errorf("an explicit empty update_repo must be preserved, got %q", s.UpdateRepo)
|
||||||
|
}
|
||||||
|
// The rest of the file was absent, so it should still be defaulted.
|
||||||
|
if s.ActivePollSeconds != Default().ActivePollSeconds {
|
||||||
|
t.Errorf("unset fields should keep their defaults, got %+v", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadRepairsNonsense(t *testing.T) {
|
||||||
|
path := filepath.Join(t.TempDir(), "config.json")
|
||||||
|
if err := os.WriteFile(path, []byte(`{"mask_style":"wat","active_poll_seconds":0}`), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := Load(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if s.MaskStyle != MaskPrefix {
|
||||||
|
t.Errorf("an unknown mask style should fall back to prefix, got %q", s.MaskStyle)
|
||||||
|
}
|
||||||
|
if s.ActivePollSeconds != 30 {
|
||||||
|
t.Errorf("a zero poll interval should be repaired, got %d", s.ActivePollSeconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A corrupt config must not stop the app from starting.
|
||||||
|
func TestLoadCorruptFileReturnsDefaultsAndError(t *testing.T) {
|
||||||
|
path := filepath.Join(t.TempDir(), "config.json")
|
||||||
|
if err := os.WriteFile(path, []byte(`{not json`), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := Load(path)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected an error for a corrupt config")
|
||||||
|
}
|
||||||
|
if s != Default() {
|
||||||
|
t.Errorf("a corrupt config should still yield usable defaults, got %+v", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSaveRoundTrip(t *testing.T) {
|
||||||
|
path := filepath.Join(t.TempDir(), "nested", "config.json")
|
||||||
|
|
||||||
|
want := Default()
|
||||||
|
want.MaskStyle = MaskDotted
|
||||||
|
want.StartWithWindows = true
|
||||||
|
want.UpdateRepo = "https://example.com/o/r"
|
||||||
|
if err := want.Save(path); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := Load(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("round trip changed the settings:\n got %+v\nwant %+v", got, want)
|
||||||
|
}
|
||||||
|
if !got.DottedMasks() {
|
||||||
|
t.Error("DottedMasks should be true for MaskDotted")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user