Files
forgetting/internal/ui/retire_test.go
T
maeneko 5b29993d5d
ci / check (push) Successful in 23m54s
Panel redesign, per-server I1 masking, node minimum version 0.5.0, installer output
- UI: settings tab with sections (server, API keys, Prometheus, audit, migrate,
  about); server settings moved there; master keys table rework (menu, chips,
  version, compact phone cards); three-state node version chip
- I1: random per-server template (DNS / QUIC Initial / STUN) kept in users.db,
  GET/POST /api/masking, change from the master keys banner
- Hub rejects nodes older than 0.5.0 (close 4026), /ui/nodes reports outdated
- install.sh: spinner for long commands (full output in the log), step progress
  bar, cards; installs older than 0.5.0 require a full reinstall with data reset
- Bump to Beta 0.5.0
2026-10-29 22:59:09 +03:00

103 lines
4.4 KiB
Go

package ui
import (
"context"
"crypto/ed25519"
"crypto/rand"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"time"
"github.com/coder/websocket"
"github.com/maeneko/forgetting/internal/jsjson"
)
// После переноса старый сервер выключают, когда новый адрес знают все: хаб сообщает его
// нодам в ready и запоминает подтверждение (core_ok), клиенты получают его в endpoints.
// Вкладка «Перенос» показывает, сколько осталось.
func TestRetireReadiness(t *testing.T) {
t.Setenv("SUB_HOST", "198.51.100.20")
p := newMovePanel(t, "off", "40500")
pub, priv, _ := ed25519.GenerateKey(rand.Reader)
for _, q := range []string{
"INSERT INTO nodes (id, name, pub, created_at) VALUES (0, 'Старый', 'x', 1)",
"INSERT INTO nodes (id, name, pub, created_at) VALUES (3, 'Frankfurt', '" + b64url.EncodeToString(pub) + "', 1)",
"INSERT INTO nodes (id, name, created_at) VALUES (4, 'Ещё не подключена', 1)",
"INSERT INTO master_keys (id, uuid, label, secret, created_at) VALUES (1, 'u1', 'Семья', x'01', 1)",
"INSERT INTO master_keys (id, uuid, label, secret, created_at) VALUES (2, 'u2', 'Не открыта', x'02', 1)",
"INSERT INTO master_keys (id, uuid, label, secret, created_at, revoked_at) VALUES (3, 'u3', 'Отозвана', x'03', 1, 1)",
"INSERT INTO devices (id, master_id, device_id, pub_key, auth_pub, created_at, last_seen) VALUES (5, 1, 'a', 'W5', 'A5', 1, 2000)",
"INSERT INTO devices (id, master_id, device_id, pub_key, auth_pub, created_at, last_seen) VALUES (6, 1, 'b', 'W6', 'A6', 1, 500)",
"INSERT INTO devices (id, master_id, device_id, pub_key, auth_pub, created_at) VALUES (7, 1, 'c', 'W7', 'A7', 1)",
"INSERT INTO devices (id, master_id, device_id, pub_key, auth_pub, created_at, last_seen) VALUES (8, 3, 'd', 'W8', 'A8', 1, 2000)",
} {
if _, err := p.a.db.Exec(q); err != nil {
t.Fatal(q, err)
}
}
srv := httptest.NewServer(http.HandlerFunc(p.a.nodes.serveWS))
defer srv.Close()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
ws, _, err := websocket.Dial(ctx, "ws"+strings.TrimPrefix(srv.URL, "http"), nil)
if err != nil {
t.Fatal(err)
}
defer ws.CloseNow()
read := func() jsjson.Object {
_, raw, err := ws.Read(ctx)
if err != nil {
t.Fatal(err)
}
v, _ := jsjson.Decode(raw)
o, _ := v.(jsjson.Object)
return o
}
write := func(o jsjson.Object) { ws.Write(ctx, websocket.MessageText, jsjson.MustMarshal(o)) }
nonce := read().Get2("nonce").(string)
write(jsjson.Object{{Key: "t", Val: "auth"}, {Key: "node_id", Val: 3},
{Key: "sig", Val: b64url.EncodeToString(ed25519.Sign(priv, authMsg(nonce, 3)))}, {Key: "version", Val: MinNodeVersion}})
if got := string(jsjson.MustMarshal(read())); got != `{"t":"ready","core":"198.51.100.20:40500"}` {
t.Fatalf("ready: %s", got)
}
retire := func() jsjson.Object {
_, o := p.call(t, p.a.migration.status, nil)
r, _ := o.Get2("retire").(jsjson.Object)
return r
}
if retire() != nil {
t.Error("готовность показана панели, которая ничего не принимала")
}
t.Setenv("MOVED_IN_AT", "1000")
if n := string(jsjson.MustMarshal(retire().Get2("nodes"))); n != `[{"id":3,"name":"Frankfurt","online":true,"ok":false}]` {
t.Errorf("до core_ok: %s", n)
}
write(jsjson.Object{{Key: "t", Val: "core_ok"}, {Key: "core", Val: "203.0.113.66:1"}}) // не наш адрес — мимо
write(jsjson.Object{{Key: "t", Val: "core_ok"}, {Key: "core", Val: "198.51.100.20:40500"}})
var r jsjson.Object
for range 50 {
if r = retire(); strings.Contains(string(jsjson.MustMarshal(r.Get2("nodes"))), `"ok":true`) {
break
}
time.Sleep(20 * time.Millisecond)
}
want := `{"since":1000,"home":"198.51.100.20:40500","old":{"exists":true,"online":false},` +
`"devices":{"total":3,"fresh":1},"unused_links":1,"nodes":[{"id":3,"name":"Frankfurt","online":true,"ok":true}]}`
if got := string(jsjson.MustMarshal(r)); got != want {
t.Errorf("готовность:\n got %s\nwant %s", got, want)
}
os.WriteFile(p.env, []byte("UI_PASS=x\nMOVED_IN_AT=1000\n"), 0o600)
w := httptest.NewRecorder()
p.a.migration.router().ServeHTTP(w, httptest.NewRequest("DELETE", "/retire", nil))
if raw, _ := os.ReadFile(p.env); strings.Contains(string(raw), "MOVED_IN_AT") || retire() != nil {
t.Errorf("отметка не убрана: %q", raw)
}
}