3x-ui/internal/web/service/node_credentials_writeonly_test.go

168 lines
4.2 KiB
Go

package service
import (
"encoding/json"
"strings"
"testing"
"github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
)
func TestNodeCredentialsNeverMarshal(t *testing.T) {
raw, err := json.Marshal(&model.Node{
Id: 7,
Name: "node",
ApiToken: "plain-secret-token",
})
if err != nil {
t.Fatalf("marshal node: %v", err)
}
out := string(raw)
if strings.Contains(out, "plain-secret-token") || strings.Contains(out, "apiToken") {
t.Fatalf("model.Node JSON leaked api token field: %s", out)
}
}
func TestNodeViewExposesOnlyCredentialPresence(t *testing.T) {
setupConflictDB(t)
svc := &NodeService{}
reqToken := "write-only-secret"
view, err := svc.CreateFromRequest(&NodeMutationRequest{
Name: "node-view",
Scheme: "https",
Address: "127.0.0.1",
Port: 2096,
ApiToken: &reqToken,
Enable: true,
})
if err != nil {
t.Fatalf("create from request: %v", err)
}
if !view.HasApiToken {
t.Fatal("create view should report credential presence")
}
got, err := svc.GetViewById(view.Id)
if err != nil {
t.Fatalf("get view: %v", err)
}
raw, err := json.Marshal(got)
if err != nil {
t.Fatalf("marshal view: %v", err)
}
out := string(raw)
if !strings.Contains(out, `"hasApiToken":true`) {
t.Fatalf("view does not report credential presence: %s", out)
}
if strings.Contains(out, reqToken) || strings.Contains(out, "apiToken") {
t.Fatalf("NodeView leaked plaintext or apiToken key: %s", out)
}
}
func TestNodeCredentialMutationSemantics(t *testing.T) {
setupConflictDB(t)
svc := &NodeService{}
initial := "initial-token"
view, err := svc.CreateFromRequest(&NodeMutationRequest{
Name: "mut",
Scheme: "https",
Address: "127.0.0.1",
Port: 2096,
ApiToken: &initial,
Enable: true,
})
if err != nil {
t.Fatalf("create: %v", err)
}
before := rawStoredNodeToken(t, view.Id)
if before != initial {
t.Fatalf("stored token = %q, want %q", before, initial)
}
if err := svc.UpdateFromRequest(view.Id, &NodeMutationRequest{
Name: "mut-renamed",
Scheme: "https",
Address: "127.0.0.1",
Port: 2096,
Enable: true,
}); err != nil {
t.Fatalf("keep-token update: %v", err)
}
if after := rawStoredNodeToken(t, view.Id); after != before {
t.Fatalf("omitted token should keep existing token: %q -> %q", before, after)
}
blank := " "
if err := svc.UpdateFromRequest(view.Id, &NodeMutationRequest{
Name: "bad",
Scheme: "https",
Address: "127.0.0.1",
Port: 2096,
ApiToken: &blank,
Enable: true,
}); err == nil {
t.Fatal("blank apiToken must be rejected; omit to keep or clear explicitly")
}
next := "next-token"
if err := svc.UpdateFromRequest(view.Id, &NodeMutationRequest{
Name: "mut",
Scheme: "https",
Address: "127.0.0.1",
Port: 2096,
ApiToken: &next,
Enable: true,
}); err != nil {
t.Fatalf("replace token: %v", err)
}
if replaced := rawStoredNodeToken(t, view.Id); replaced != next {
t.Fatalf("replace token stored %q, want %q", replaced, next)
}
if err := svc.UpdateFromRequest(view.Id, &NodeMutationRequest{
Name: "mut",
Scheme: "https",
Address: "127.0.0.1",
Port: 2096,
ClearApiToken: true,
Enable: true,
}); err == nil {
t.Fatal("enabled non-mtls node must not clear apiToken")
}
if err := svc.UpdateFromRequest(view.Id, &NodeMutationRequest{
Name: "mut",
Scheme: "https",
Address: "127.0.0.1",
Port: 2096,
ClearApiToken: true,
Enable: false,
}); err != nil {
t.Fatalf("clear disabled token: %v", err)
}
if cleared := rawStoredNodeToken(t, view.Id); cleared != "" {
t.Fatalf("clear token left stored value %q", cleared)
}
if _, err := svc.CreateFromRequest(&NodeMutationRequest{
Name: "mtls-only",
Scheme: "https",
Address: "127.0.0.1",
Port: 2097,
Enable: true,
TlsVerifyMode: "mtls",
}); err != nil {
t.Fatalf("mtls create without token: %v", err)
}
}
func rawStoredNodeToken(t *testing.T, id int) string {
t.Helper()
var n model.Node
if err := database.GetDB().Select("api_token").Where("id = ?", id).First(&n).Error; err != nil {
t.Fatalf("load raw node token: %v", err)
}
return n.ApiToken
}