Files
LinkPort/internal/api/redis_no_account_test.go
T
曾志威 9da95beb42
continuous-integration/drone Build is failing
feat: refine management UI, connection diagnostics and local startup
Migrate the workspace to React and Ant Design, improve account forms and responsive layouts, support outbound diagnostics and SSH fingerprint confirmation, and persist the management startup port. Preserve database migrations, remove S3 settings storage, and improve gateway audit handling.
2026-10-13 19:43:23 +08:00

177 lines
8.4 KiB
Go

package api
import (
"encoding/json"
"linkport/internal/gateway"
"linkport/internal/model"
"testing"
)
func createAccountlessRedisTarget(t *testing.T, f *fixture, name string) model.Target {
t.Helper()
w := f.request("POST", "/api/v1/targets", map[string]any{
"name": name, "type": "redis", "targetHost": "redis.invalid", "targetPort": 6379,
"targetTLS": false, "accounts": []any{},
}, true)
requireStatus(t, w, 201)
var view struct {
Config model.Target `json:"config"`
}
if err := json.Unmarshal(w.Body.Bytes(), &view); err != nil {
t.Fatal(err)
}
if len(view.Config.Accounts) != 0 {
t.Fatal("account-free Redis target response contains accounts")
}
return view.Config
}
func createRedisTargetWithAccount(t *testing.T, f *fixture) model.Target {
t.Helper()
w := f.request("POST", "/api/v1/targets", map[string]any{
"name": "Redis with account", "type": "redis", "targetHost": "redis-auth.invalid", "targetPort": 6379,
"targetTLS": false, "accounts": []any{map[string]any{"id": "configured", "remark": "Configured", "username": "default", "password": "fixture-secret"}},
}, true)
requireStatus(t, w, 201)
var view struct {
Config model.Target `json:"config"`
}
if err := json.Unmarshal(w.Body.Bytes(), &view); err != nil {
t.Fatal(err)
}
return view.Config
}
func TestRedisTargetWithoutAccountsDraftSaveTestAndHealth(t *testing.T) {
f := newFixture(t)
f.login()
h := &recordingDraftProtocol{}
manager := gateway.New(f.store, nil, map[string]model.Handler{"redis": h})
f.server.manager = manager
t.Cleanup(manager.Shutdown)
draft := draftConfig("redis")
draft["name"], draft["accounts"] = "Account-free Redis", []any{}
requireStatus(t, f.request("POST", "/api/v1/targets/test", map[string]any{"accountId": "", "config": draft}, true), 200)
if len(h.seen) != 1 || h.seen[0].TargetAccountID != "" || len(h.seen[0].TargetAccounts) != 0 || h.seen[0].TargetUser != "" || h.seen[0].TargetPassword != "" || h.seen[0].SSH.AuthMethod != "none" {
t.Fatalf("draft test did not use account-free Redis: %+v", h.seen)
}
target := createAccountlessRedisTarget(t, f, "Saved account-free Redis")
requireStatus(t, f.request("POST", "/api/v1/targets/"+target.ID+"/test", nil, true), 200)
requireStatus(t, f.request("POST", "/api/v1/targets/"+target.ID+"/reveal-password", map[string]string{"field": "targetPassword", "password": adminPassword}, true), 400)
requireStatus(t, f.request("POST", "/api/v1/monitoring/check", map[string]any{"targetId": target.ID, "accountId": "", "attempts": 1}, true), 200)
for _, seen := range h.seen {
if seen.TargetUser != "" || seen.TargetPassword != "" {
t.Fatal("account-free test invented Redis credentials")
}
}
withAccount := draftConfig("redis")
withAccount["accounts"] = []any{map[string]any{"id": "configured", "remark": "Configured", "username": "default"}}
requireStatus(t, f.request("POST", "/api/v1/targets/test", map[string]any{"accountId": "", "config": withAccount}, true), 400)
}
func TestRedisGatewayDefaultAndAccountOverrideAllowAccountlessTarget(t *testing.T) {
f := newFixture(t)
f.login()
h := &recordingDraftProtocol{}
manager := gateway.New(f.store, nil, map[string]model.Handler{"redis": h})
f.server.manager = manager
t.Cleanup(manager.Shutdown)
target := createAccountlessRedisTarget(t, f, "Gateway account-free Redis")
requireStatus(t, f.request("POST", "/api/v1/proxies/test", map[string]any{
"config": map[string]any{
"name": "Draft anonymous Redis gateway", "type": "redis", "listenHost": "127.0.0.1", "listenPort": freePort(t),
"targetId": target.ID, "gatewayAccounts": []any{},
},
}, true), 200)
anonymousResponse := f.request("POST", "/api/v1/proxies", map[string]any{
"name": "Anonymous Redis gateway", "type": "redis", "listenHost": "127.0.0.1", "listenPort": freePort(t),
"targetId": target.ID, "gatewayAccounts": []any{},
}, true)
requireStatus(t, anonymousResponse, 201)
anonymousGateway := readView(t, anonymousResponse).Config
if len(anonymousGateway.GatewayAccounts) != 0 || anonymousGateway.GatewayUser != "" || anonymousGateway.GatewayPassword != "" {
t.Fatalf("anonymous Redis gateway retained gateway credentials: %+v", anonymousGateway)
}
requireStatus(t, f.request("POST", "/api/v1/proxies/"+anonymousGateway.ID+"/test", nil, true), 200)
requireStatus(t, f.request("POST", "/api/v1/proxies/"+anonymousGateway.ID+"/start", nil, true), 200)
requireStatus(t, f.request("POST", "/api/v1/proxies/"+anonymousGateway.ID+"/stop", nil, true), 200)
defaultResponse := f.request("POST", "/api/v1/proxies", map[string]any{
"name": "Default Redis gateway", "type": "redis", "listenHost": "127.0.0.1", "listenPort": freePort(t),
"targetId": target.ID, "gatewayAccounts": []any{map[string]any{"id": "client", "username": "client", "password": "client-secret"}},
}, true)
requireStatus(t, defaultResponse, 201)
defaultGateway := readView(t, defaultResponse).Config
requireStatus(t, f.request("POST", "/api/v1/proxies/"+defaultGateway.ID+"/test", nil, true), 200)
requireStatus(t, f.request("POST", "/api/v1/proxies/"+defaultGateway.ID+"/start", nil, true), 200)
requireStatus(t, f.request("POST", "/api/v1/proxies/"+defaultGateway.ID+"/stop", nil, true), 200)
overrideResponse := f.request("POST", "/api/v1/proxies", map[string]any{
"name": "Mapped Redis gateway", "type": "redis", "listenHost": "127.0.0.1", "listenPort": freePort(t),
"gatewayAccounts": []model.GatewayAccount{{ID: "client", Username: "client", Password: "client-secret", TargetID: target.ID}},
}, true)
requireStatus(t, overrideResponse, 201)
overrideGateway := readView(t, overrideResponse).Config
requireStatus(t, f.request("POST", "/api/v1/proxies/"+overrideGateway.ID+"/test?gatewayAccountId=client", nil, true), 200)
for _, seen := range h.seen {
if seen.TargetID != target.ID || seen.TargetAccountID != "" || seen.TargetUser != "" || seen.TargetPassword != "" || !seen.ValidTargetAccountSelection() {
t.Fatalf("gateway test did not resolve account-free Redis target: %+v", seen)
}
}
}
func TestRedisGatewaySwitchDistinguishesAccountlessAndUnselectedAccount(t *testing.T) {
f := newFixture(t)
f.login()
target := createAccountlessRedisTarget(t, f, "Switch account-free Redis")
withAccount := createRedisTargetWithAccount(t, f)
w := f.request("POST", "/api/v1/proxies", map[string]any{
"name": "Switch Redis gateway", "type": "redis", "listenHost": "127.0.0.1", "listenPort": freePort(t),
"targetId": withAccount.ID, "targetAccountId": "configured", "gatewayAccounts": []any{map[string]any{"id": "client", "username": "client", "password": "client-secret"}},
}, true)
requireStatus(t, w, 201)
gatewayID := readView(t, w).Config.ID
path := "/api/v1/proxies/" + gatewayID + "/target"
requireStatus(t, f.request("POST", path, map[string]any{"targetId": target.ID, "targetAccountId": ""}, true), 200)
requireStatus(t, f.request("POST", path, map[string]any{"targetId": withAccount.ID, "targetAccountId": ""}, true), 400)
saved, err := f.store.Get(gatewayID)
if err != nil {
t.Fatal(err)
}
if saved.TargetID != target.ID || saved.TargetAccountID != "" || !saved.ValidTargetAccountSelection() {
t.Fatalf("rejected switch changed account-free binding: %+v", saved)
}
requireStatus(t, f.request("POST", "/api/v1/proxies", map[string]any{
"name": "Invalid Redis selection", "type": "redis", "listenHost": "127.0.0.1", "listenPort": freePort(t),
"targetId": withAccount.ID, "gatewayAccounts": []any{map[string]any{"id": "client", "username": "client", "password": "client-secret"}},
}, true), 400)
partialResponse := f.request("POST", "/api/v1/proxies", map[string]any{
"name": "Incremental Redis mapping", "type": "redis", "listenHost": "127.0.0.1", "listenPort": freePort(t),
"gatewayAccounts": []model.GatewayAccount{
{ID: "first", Username: "first", Password: "first-secret"},
{ID: "second", Username: "second", Password: "second-secret"},
},
}, true)
requireStatus(t, partialResponse, 201)
partialID := readView(t, partialResponse).Config.ID
requireStatus(t, f.request("POST", "/api/v1/proxies/"+partialID+"/target", map[string]any{
"gatewayAccountId": "first", "targetId": target.ID, "targetAccountId": "",
}, true), 200)
partial, err := f.store.Get(partialID)
if err != nil {
t.Fatal(err)
}
if partial.GatewayAccounts[0].TargetID != target.ID || partial.GatewayAccounts[0].TargetAccountID != "" || partial.GatewayAccounts[1].TargetID != "" {
t.Fatalf("incremental account mapping was not preserved: %+v", partial.GatewayAccounts)
}
}