feat: Go port of the panel in panel-go/ — drop-in replacement for server.js
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:
@@ -0,0 +1,114 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestParseCookies(t *testing.T) {
|
||||
got := ParseCookies("a=1; b=two; ng_session=abc%20def; empty=; =x; novalue; a=last")
|
||||
want := map[string]string{
|
||||
"a": "last", // later duplicates overwrite, like Object.fromEntries
|
||||
"b": "two",
|
||||
"ng_session": "abc def",
|
||||
"empty": "",
|
||||
}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("ParseCookies = %v, want %v", got, want)
|
||||
}
|
||||
for k, v := range want {
|
||||
if got[k] != v {
|
||||
t.Errorf("cookie %q = %q, want %q", k, got[k], v)
|
||||
}
|
||||
}
|
||||
if _, ok := got[""]; ok {
|
||||
t.Errorf("empty key must be filtered out: %v", got)
|
||||
}
|
||||
if _, ok := got["novalue"]; ok {
|
||||
t.Errorf("pair without '=' must be skipped: %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCookiesDecodingAndEdges(t *testing.T) {
|
||||
if got := ParseCookies(""); len(got) != 0 {
|
||||
t.Errorf("empty header = %v, want empty", got)
|
||||
}
|
||||
got := ParseCookies("a%20b=c%3Dd; spaced = value ")
|
||||
if got["a b"] != "c=d" {
|
||||
t.Errorf("decoded key/value with '=' in value = %v", got)
|
||||
}
|
||||
if got["spaced"] != "value" {
|
||||
t.Errorf("trimmed pair = %v", got)
|
||||
}
|
||||
// Malformed escape: pair is skipped (documented deviation from JS throw).
|
||||
got = ParseCookies("bad=%zz; good=1")
|
||||
if _, ok := got["bad"]; ok {
|
||||
t.Errorf("malformed pair should be skipped: %v", got)
|
||||
}
|
||||
if got["good"] != "1" {
|
||||
t.Errorf("good pair after malformed = %v", got)
|
||||
}
|
||||
// '+' is literal in decodeURIComponent semantics, not a space.
|
||||
got = ParseCookies("plus=a+b")
|
||||
if got["plus"] != "a+b" {
|
||||
t.Errorf("plus handling = %q, want %q", got["plus"], "a+b")
|
||||
}
|
||||
}
|
||||
|
||||
func requestWithSession(id string) *http.Request {
|
||||
r := httptest.NewRequest("GET", "/", nil)
|
||||
if id != "" {
|
||||
r.Header.Set("Cookie", "ng_session="+id)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func TestSessionManagerLifecycle(t *testing.T) {
|
||||
m := NewSessionManager(12)
|
||||
if m.TTL != 12*time.Hour {
|
||||
t.Fatalf("TTL = %v, want 12h", m.TTL)
|
||||
}
|
||||
s := m.Create("user-1")
|
||||
if s.ID == "" || s.CSRF == "" || s.UserID != "user-1" {
|
||||
t.Fatalf("created session = %+v", s)
|
||||
}
|
||||
if !s.ExpiresAt.After(time.Now()) {
|
||||
t.Fatalf("ExpiresAt not in future: %v", s.ExpiresAt)
|
||||
}
|
||||
got := m.Get(requestWithSession(s.ID))
|
||||
if got != s {
|
||||
t.Fatalf("Get = %+v, want the created session", got)
|
||||
}
|
||||
if m.Get(requestWithSession("nope")) != nil {
|
||||
t.Errorf("Get unknown id should be nil")
|
||||
}
|
||||
if m.Get(requestWithSession("")) != nil {
|
||||
t.Errorf("Get without cookie should be nil")
|
||||
}
|
||||
m.Delete(requestWithSession(s.ID))
|
||||
if m.Get(requestWithSession(s.ID)) != nil {
|
||||
t.Errorf("Get after Delete should be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionManagerExpiryAndPrune(t *testing.T) {
|
||||
m := NewSessionManager(1)
|
||||
live := m.Create("live")
|
||||
dead := m.Create("dead")
|
||||
dead.ExpiresAt = time.Now().Add(-time.Minute)
|
||||
if m.Get(requestWithSession(dead.ID)) != nil {
|
||||
t.Errorf("expired session should read as nil")
|
||||
}
|
||||
// Expired session is also deleted by Get.
|
||||
dead2 := m.Create("dead2")
|
||||
dead2.ExpiresAt = time.Now().Add(-time.Minute)
|
||||
m.Prune()
|
||||
if m.Get(requestWithSession(dead2.ID)) != nil {
|
||||
t.Errorf("pruned session should be gone")
|
||||
}
|
||||
if m.Get(requestWithSession(live.ID)) != live {
|
||||
t.Errorf("live session must survive Prune")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user