| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201 |
- 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: "mut-blank",
- Scheme: "https",
- Address: "127.0.0.1",
- Port: 2096,
- ApiToken: &blank,
- Enable: true,
- }); err != nil {
- t.Fatalf("blank apiToken should keep existing token on update: %v", err)
- }
- if afterBlank := rawStoredNodeToken(t, view.Id); afterBlank != before {
- t.Fatalf("blank token should keep existing token: %q -> %q", before, afterBlank)
- }
- 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 TestNodeUpdateRequiresTokenWhenNoStoredTokenAndMtlsDisabled(t *testing.T) {
- setupConflictDB(t)
- svc := &NodeService{}
- view, err := svc.CreateFromRequest(&NodeMutationRequest{
- Name: "mtls-empty",
- Scheme: "https",
- Address: "127.0.0.1",
- Port: 2098,
- Enable: true,
- TlsVerifyMode: "mtls",
- })
- if err != nil {
- t.Fatalf("create mtls node: %v", err)
- }
- blank := ""
- if err := svc.UpdateFromRequest(view.Id, &NodeMutationRequest{
- Name: "mtls-empty",
- Scheme: "https",
- Address: "127.0.0.1",
- Port: 2098,
- ApiToken: &blank,
- Enable: true,
- BasePath: "/",
- OutboundTag: "",
- }); err == nil {
- t.Fatal("enabled non-mtls node without stored token must be rejected")
- }
- }
- 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
- }
|