feat: Go port of the panel in panel-go/ — drop-in replacement for server.js
CI / test (push) Canceled after 0s
CI / anytls-smoke (push) Canceled after 0s

Pure-stdlib Go implementation of the whole control plane: same
nexusgate.json schema (unknown fields preserved verbatim, explicit
null/false tri-states modeled), same HTTP API and error strings,
embedded frontend (NG_PUBLIC_DIR override), self-implemented scrypt
auth, X25519 Reality keys, panel-hosted keyStore and heartbeat
reconciliation included. Version stays 0.6.8.

Verified: Go tests 33/33 (scrypt + Reality vectors generated from
the Node implementation, subscription goldens byte-identical),
Node suite 52/52 untouched, differential E2E against the Node
panel 50/50 including two-way DB swap. See panel-go/PARITY.md
and panel-go/INSTALL-GO.md.
This commit is contained in:
aaaamoooo
2026-10-20 08:21:54 +00:00
parent abb1a4344b
commit 3951134940
37 changed files with 12183 additions and 0 deletions
+243
View File
@@ -0,0 +1,243 @@
package main
import (
"errors"
"os"
"path/filepath"
"strings"
"testing"
)
func newTestStore(t *testing.T) (*Store, string) {
t.Helper()
file := filepath.Join(t.TempDir(), "nested", "data.json")
s := NewStore(file)
if err := s.Init(); err != nil {
t.Fatalf("Init: %v", err)
}
return s, file
}
func TestInitCreatesMissingFile(t *testing.T) {
s, file := newTestStore(t)
info, err := os.Stat(file)
if err != nil {
t.Fatalf("data file not created: %v", err)
}
if perm := info.Mode().Perm(); perm != 0o600 {
t.Errorf("file perm = %o, want 600", perm)
}
if s.Data.SchemaVersion != 1 {
t.Errorf("SchemaVersion = %d", s.Data.SchemaVersion)
}
if s.Data.Settings.BrandName != "NexusGate" || s.Data.Settings.SessionHours != 12 ||
s.Data.Settings.ObservationTtlMinutes != 10 || s.Data.Settings.ActivityRetentionDays != 30 ||
s.Data.Settings.CompletedJobRetentionDays != 7 {
t.Errorf("settings defaults = %+v", s.Data.Settings)
}
if s.Data.CreatedAt == "" {
t.Errorf("CreatedAt empty")
}
for _, c := range collections {
if c.isNil(s.Data) {
t.Errorf("collection %s is nil after EmptyData", c.name)
}
}
if s.Data.KeyStore == nil {
t.Errorf("KeyStore nil")
}
}
func TestInitLoadPreservesUnknownFields(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "data.json")
raw := `{"schemaVersion":1,"createdAt":"2026-01-01T00:00:00.000Z",` +
`"settings":{"brandName":"NexusGate","sessionHours":12,"observationTtlMinutes":10,"activityRetentionDays":30,"completedJobRetentionDays":7,"futureSetting":true},` +
`"users":[{"id":"u1","username":"admin","passwordHash":"h","role":"admin","status":"active","createdAt":"2026-01-01T00:00:00.000Z","legacyNote":"keep-me"}],` +
`"servers":[],"agents":[],"enrollmentTokens":[],"customers":[],"nodes":[],"nodeClients":[],"jobs":[],"activity":[],"observations":[],` +
`"keyStore":{},"futureTop":{"x":1}}` + "\n"
// subscriptionAccess / subscriptionClients deliberately absent: Init must default them.
if err := os.WriteFile(file, []byte(raw), 0o600); err != nil {
t.Fatal(err)
}
s := NewStore(file)
if err := s.Init(); err != nil {
t.Fatalf("Init with unknown fields: %v", err)
}
if s.Data.SubscriptionAccess == nil || s.Data.SubscriptionClients == nil {
t.Fatalf("missing subscription collections not defaulted")
}
if _, err := s.Transaction(func(d *Data) (any, error) { return nil, nil }); err != nil {
t.Fatalf("no-op transaction: %v", err)
}
out, err := os.ReadFile(file)
if err != nil {
t.Fatal(err)
}
for _, needle := range []string{`"futureTop"`, `"legacyNote":"keep-me"`, `"futureSetting":true`} {
if !strings.Contains(string(out), needle) {
t.Errorf("round trip dropped %s; file: %s", needle, out)
}
}
}
func TestTransactionPersistAndResult(t *testing.T) {
s, file := newTestStore(t)
result, err := s.Transaction(func(d *Data) (any, error) {
d.Settings.BrandName = "Renamed"
return "the-result", nil
})
if err != nil {
t.Fatalf("Transaction: %v", err)
}
if result != "the-result" {
t.Errorf("result = %v", result)
}
if s.Data.Settings.BrandName != "Renamed" {
t.Errorf("in-memory data not updated")
}
reloaded := NewStore(file)
if err := reloaded.Init(); err != nil {
t.Fatalf("reload: %v", err)
}
if reloaded.Data.Settings.BrandName != "Renamed" {
t.Errorf("persisted brand = %q", reloaded.Data.Settings.BrandName)
}
}
func TestTransactionErrorDoesNotPersist(t *testing.T) {
s, file := newTestStore(t)
before, _ := os.ReadFile(file)
myErr := errors.New("boom")
_, err := s.Transaction(func(d *Data) (any, error) {
d.Settings.BrandName = "ShouldNotStick"
return nil, myErr
})
if !errors.Is(err, myErr) {
t.Fatalf("err = %v, want myErr", err)
}
after, _ := os.ReadFile(file)
if string(before) != string(after) {
t.Errorf("file changed despite transaction error")
}
if s.Data.Settings.BrandName == "ShouldNotStick" {
t.Errorf("in-memory data changed despite transaction error")
}
}
func TestTransactionSkipDoesNotPersist(t *testing.T) {
s, file := newTestStore(t)
before, _ := os.ReadFile(file)
result, err := s.Transaction(func(d *Data) (any, error) {
d.Settings.BrandName = "Skipped"
return "ignored", ErrSkip
})
if err != nil {
t.Fatalf("ErrSkip transaction err = %v", err)
}
if result != nil {
t.Errorf("ErrSkip result = %v, want nil", result)
}
after, _ := os.ReadFile(file)
if string(before) != string(after) {
t.Errorf("file changed despite ErrSkip")
}
if s.Data.Settings.BrandName == "Skipped" {
t.Errorf("in-memory data changed despite ErrSkip")
}
}
func TestReplaceRoundTrip(t *testing.T) {
s, file := newTestStore(t)
next := EmptyData()
next.Settings.BrandName = "Replaced"
next.Users = []*User{{ID: "u9", Username: "root", PasswordHash: "h", Role: "admin", Status: "active", CreatedAt: "2026-01-01T00:00:00.000Z"}}
next.SubscriptionAccess = nil // Replace must default these
next.SubscriptionClients = nil
next.KeyStore = nil
if err := s.Replace(next); err != nil {
t.Fatalf("Replace: %v", err)
}
// Mutating the caller's copy after Replace must not affect the store.
next.Settings.BrandName = "MutatedAfterwards"
snap := s.Snapshot()
if snap.Settings.BrandName != "Replaced" {
t.Errorf("snapshot brand = %q", snap.Settings.BrandName)
}
if len(snap.Users) != 1 || snap.Users[0].ID != "u9" {
t.Errorf("snapshot users = %+v", snap.Users)
}
if snap.SubscriptionAccess == nil || snap.KeyStore == nil {
t.Errorf("Replace did not default collections/keyStore")
}
reloaded := NewStore(file)
if err := reloaded.Init(); err != nil {
t.Fatalf("reload after Replace: %v", err)
}
if reloaded.Data.Settings.BrandName != "Replaced" || len(reloaded.Data.Users) != 1 {
t.Errorf("reloaded data does not match Replace input")
}
}
func TestValidateErrorMessages(t *testing.T) {
s, _ := newTestStore(t)
_, err := s.Transaction(func(d *Data) (any, error) {
d.SchemaVersion = 2
return nil, nil
})
if err == nil || err.Error() != "Unsupported or corrupt NexusGate data file" {
t.Errorf("schemaVersion err = %v", err)
}
_, err = s.Transaction(func(d *Data) (any, error) {
d.Nodes = nil
return nil, nil
})
if err == nil || err.Error() != "Invalid collection: nodes" {
t.Errorf("collection err = %v", err)
}
// Failed validations must not have persisted the bad drafts.
if s.Data.SchemaVersion != 1 || s.Data.Nodes == nil {
t.Errorf("store data corrupted by failed validation")
}
}
func TestInitRejectsCorruptFiles(t *testing.T) {
dir := t.TempDir()
bad := filepath.Join(dir, "bad.json")
if err := os.WriteFile(bad, []byte("{not json"), 0o600); err != nil {
t.Fatal(err)
}
if err := NewStore(bad).Init(); err == nil {
t.Errorf("corrupt JSON should error")
}
wrongVersion := filepath.Join(dir, "v2.json")
raw, _ := marshalJSON(EmptyData())
var v2 map[string]any
if err := unmarshalJSONUseNumber(raw, &v2); err != nil {
t.Fatal(err)
}
v2["schemaVersion"] = 2
raw2, _ := marshalJSON(v2)
if err := os.WriteFile(wrongVersion, raw2, 0o600); err != nil {
t.Fatal(err)
}
err := NewStore(wrongVersion).Init()
if err == nil || err.Error() != "Unsupported or corrupt NexusGate data file" {
t.Errorf("schemaVersion 2 err = %v", err)
}
}
func TestTransientAndViewDoNotPersist(t *testing.T) {
s, file := newTestStore(t)
before, _ := os.ReadFile(file)
s.Transient(func(d *Data) { d.Settings.BrandName = "InMemoryOnly" })
after, _ := os.ReadFile(file)
if string(before) != string(after) {
t.Errorf("Transient persisted to disk")
}
var seen string
s.View(func(d *Data) { seen = d.Settings.BrandName })
if seen != "InMemoryOnly" {
t.Errorf("View saw %q", seen)
}
}