Files
MyGoNavi/internal/nacos/api_version_test.go
Syngnat e5e80cbbe8 🐛 fix(nacos): 完善多版本连接与配置服务可靠性
- 支持 Nacos v1、v2、v3 就绪探测与受限账号连接
- 强化认证、缓存、单飞连接和关闭代际的并发边界
- 修复监听重复事件、导入身份碰撞及部分失败误报成功
- 补齐只读保护、结构化错误与六语言提示
2026-07-28 23:51:17 +08:00

1492 lines
48 KiB
Go

package nacos
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"net/url"
"regexp"
"strconv"
"strings"
"sync"
"sync/atomic"
"testing"
"GoNavi-Wails/internal/connection"
)
type recordedNacosAPIRequest struct {
method string
path string
values url.Values
header http.Header
}
type nacosAPIRequestRecorder struct {
mu sync.Mutex
requests []recordedNacosAPIRequest
}
func (r *nacosAPIRequestRecorder) record(request *http.Request) {
_ = request.ParseForm()
r.mu.Lock()
defer r.mu.Unlock()
r.requests = append(r.requests, recordedNacosAPIRequest{
method: request.Method,
path: request.URL.Path,
values: cloneNacosAPIValues(request.Form),
header: request.Header.Clone(),
})
}
func cloneNacosAPIValues(values url.Values) url.Values {
cloned := make(url.Values, len(values))
for key, entries := range values {
cloned[key] = append([]string(nil), entries...)
}
return cloned
}
func (r *nacosAPIRequestRecorder) countPath(path string) int {
r.mu.Lock()
defer r.mu.Unlock()
count := 0
for _, request := range r.requests {
if request.path == path {
count++
}
}
return count
}
func (r *nacosAPIRequestRecorder) last(method, path string) (recordedNacosAPIRequest, bool) {
r.mu.Lock()
defer r.mu.Unlock()
for i := len(r.requests) - 1; i >= 0; i-- {
if r.requests[i].method == method && r.requests[i].path == path {
return r.requests[i], true
}
}
return recordedNacosAPIRequest{}, false
}
func TestClientAPIFamilyMatrix(t *testing.T) {
tests := []struct {
name string
family nacosAPIFamily
configListGroupKey string
configListNamespaceKey string
configGroupKey string
configNamespaceKey string
serviceListGroupKey string
qualifiedNaming bool
}{
{
name: "v3 admin API",
family: nacosAPIV3,
configListGroupKey: "groupName",
configListNamespaceKey: "namespaceId",
configGroupKey: "groupName",
configNamespaceKey: "namespaceId",
serviceListGroupKey: "groupNameParam",
},
{
name: "v2 API after v3 returns 404",
family: nacosAPIV2,
configListGroupKey: "group",
configListNamespaceKey: "tenant",
configGroupKey: "group",
configNamespaceKey: "namespaceId",
serviceListGroupKey: "groupName",
},
{
name: "v1 API after v3 and v2 return 404",
family: nacosAPIV1,
configListGroupKey: "group",
configListNamespaceKey: "tenant",
configGroupKey: "group",
configNamespaceKey: "tenant",
serviceListGroupKey: "groupName",
qualifiedNaming: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
recorder := &nacosAPIRequestRecorder{}
server := httptest.NewServer(nacosAPIMatrixHandler(test.family, recorder))
defer server.Close()
client := connectAPIVersionTestClient(t, server)
defer client.Close()
if client.apiFamily != test.family {
t.Fatalf("detected API family = %d, want %d", client.apiFamily, test.family)
}
assertProbeSequenceAndCache(t, client, recorder, test.family)
exerciseNacosAPIFamily(t, client, recorder, test)
})
}
}
func TestClientAPIFamilyDetectionDoesNotFallbackOnForbidden(t *testing.T) {
recorder := &nacosAPIRequestRecorder{}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
recorder.record(request)
switch request.URL.Path {
case nacosV3ReadinessPath:
w.WriteHeader(http.StatusForbidden)
_, _ = io.WriteString(w, `{"code":403,"message":"no such api for this account"}`)
case routesForNacosAPI(nacosAPIV2).namespaceList, routesForNacosAPI(nacosAPIV1).namespaceList:
writeNacosResult(w, nacosAPIV2, []any{})
default:
http.NotFound(w, request)
}
}))
defer server.Close()
client := &ClientImpl{}
err := client.Connect(nacosAPITestConnectionConfig(t, server))
if err == nil {
t.Fatal("Connect unexpectedly succeeded after v3 returned 403")
}
if !strings.Contains(err.Error(), "403") {
t.Fatalf("Connect error = %q, want HTTP 403", err)
}
if got := recorder.countPath(nacosV3ReadinessPath); got == 0 {
t.Fatal("v3 probe was not requested")
}
if got := recorder.countPath(routesForNacosAPI(nacosAPIV2).namespaceList); got != 0 {
t.Fatalf("v2 probe count = %d, want 0 after forbidden", got)
}
if got := recorder.countPath(routesForNacosAPI(nacosAPIV1).namespaceList); got != 0 {
t.Fatalf("v1 probe count = %d, want 0 after forbidden", got)
}
}
func TestClientAPIFamilyDetectionUsesV2ReadinessWithoutNamespacePermission(t *testing.T) {
recorder := &nacosAPIRequestRecorder{}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
recorder.record(request)
switch request.URL.Path {
case nacosV3ReadinessPath:
http.NotFound(w, request)
case nacosV2ReadinessPath:
writeNacosResult(w, nacosAPIV2, "ok")
case routesForNacosAPI(nacosAPIV2).namespaceList:
http.Error(w, "namespace permission denied", http.StatusForbidden)
default:
http.NotFound(w, request)
}
}))
defer server.Close()
client := connectAPIVersionTestClient(t, server)
defer client.Close()
if client.apiFamily != nacosAPIV2 {
t.Fatalf("detected API family = %d, want v2", client.apiFamily)
}
if got := recorder.countPath(nacosV2ReadinessPath); got != 2 {
t.Fatalf("v2 readiness probe count = %d, want 2 for detection and ping", got)
}
if got := recorder.countPath(routesForNacosAPI(nacosAPIV2).namespaceList); got != 0 {
t.Fatalf("v2 namespace probe count = %d, want 0", got)
}
}
func TestClientAPIFamilyDetectionUsesV1ReadinessWithoutNamespacePermission(t *testing.T) {
recorder := &nacosAPIRequestRecorder{}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
recorder.record(request)
switch request.URL.Path {
case nacosV3ReadinessPath, nacosV2ReadinessPath, routesForNacosAPI(nacosAPIV2).namespaceList:
http.NotFound(w, request)
case nacosV1ReadinessPath:
_, _ = io.WriteString(w, "OK")
case routesForNacosAPI(nacosAPIV1).namespaceList:
http.Error(w, "namespace permission denied", http.StatusForbidden)
default:
http.NotFound(w, request)
}
}))
defer server.Close()
client := connectAPIVersionTestClient(t, server)
defer client.Close()
if client.apiFamily != nacosAPIV1 {
t.Fatalf("detected API family = %d, want v1", client.apiFamily)
}
if got := recorder.countPath(nacosV1ReadinessPath); got != 2 {
t.Fatalf("v1 readiness probe count = %d, want 2 for detection and ping", got)
}
if got := recorder.countPath(routesForNacosAPI(nacosAPIV1).namespaceList); got != 0 {
t.Fatalf("v1 namespace probe count = %d, want 0", got)
}
}
func TestClientAPIFamilyDetectionKeepsNacos22NamespaceFallback(t *testing.T) {
recorder := &nacosAPIRequestRecorder{}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
recorder.record(request)
switch request.URL.Path {
case nacosV3ReadinessPath, nacosV2ReadinessPath:
http.NotFound(w, request)
case routesForNacosAPI(nacosAPIV2).namespaceList:
writeNacosResult(w, nacosAPIV2, []any{})
default:
http.NotFound(w, request)
}
}))
defer server.Close()
client := connectAPIVersionTestClient(t, server)
defer client.Close()
if client.apiFamily != nacosAPIV2 {
t.Fatalf("detected API family = %d, want v2", client.apiFamily)
}
if got := recorder.countPath(nacosV2ReadinessPath); got != 2 {
t.Fatalf("v2 readiness probe count = %d, want 2 for detection and ping", got)
}
if got := recorder.countPath(routesForNacosAPI(nacosAPIV2).namespaceList); got != 2 {
t.Fatalf("v2 namespace probe count = %d, want 2 for detection and ping fallback", got)
}
}
func TestValidateNacosV1ReadinessProbe(t *testing.T) {
for _, body := range []string{"OK", " ok\r\n"} {
if err := validateNacosV1ReadinessProbe([]byte(body)); err != nil {
t.Fatalf("validate v1 readiness body %q: %v", body, err)
}
}
for _, body := range []string{"", "<html>console</html>", `{"code":200,"data":"OK"}`} {
if err := validateNacosV1ReadinessProbe([]byte(body)); err == nil {
t.Fatalf("validate v1 readiness body %q unexpectedly succeeded", body)
}
}
}
func TestValidateNacosAPIReadinessProbe(t *testing.T) {
for _, body := range []string{
`{"code":0,"message":"success","data":"ok"}`,
`{"code":200,"message":"success","data":" OK "}`,
} {
if err := validateNacosAPIReadinessProbe([]byte(body)); err != nil {
t.Fatalf("validate readiness body %q: %v", body, err)
}
}
for _, body := range []string{
`{"code":0,"message":"success","data":true}`,
`{"code":0,"message":"success","data":"ready"}`,
`{"code":0,"message":"success","data":null}`,
`{"code":0,"message":"success"}`,
} {
if err := validateNacosAPIReadinessProbe([]byte(body)); err == nil {
t.Fatalf("validate readiness body %q unexpectedly succeeded", body)
}
}
}
func TestClientReadinessRejectsNonOfficialSuccessPayload(t *testing.T) {
tests := []struct {
name string
invalidOnCall int
}{
{name: "detection", invalidOnCall: 1},
{name: "connect ping", invalidOnCall: 2},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var readinessRequests atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
switch request.URL.Path {
case nacosV3ReadinessPath:
requestNumber := int(readinessRequests.Add(1))
data := any("ok")
if requestNumber == test.invalidOnCall {
data = true
}
writeNacosResult(w, nacosAPIV3, data)
case routesForNacosAPI(nacosAPIV2).namespaceList:
writeNacosResult(w, nacosAPIV2, []any{})
default:
http.NotFound(w, request)
}
}))
defer server.Close()
client := &ClientImpl{}
err := client.Connect(nacosAPITestConnectionConfig(t, server))
if err == nil {
_ = client.Close()
t.Fatal("Connect unexpectedly accepted non-official readiness data")
}
if got := int(readinessRequests.Load()); got != test.invalidOnCall {
t.Fatalf("readiness requests = %d, want %d", got, test.invalidOnCall)
}
})
}
}
func TestClientPublicReadinessOmitsAccessToken(t *testing.T) {
const accessToken = "public-readiness-token"
var readinessMu sync.Mutex
var readinessTokens []string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
switch request.URL.Path {
case "/v3/auth/user/login":
writeNacosJSON(w, map[string]any{"accessToken": accessToken, "tokenTtl": 3600})
case nacosV3ReadinessPath:
readinessMu.Lock()
readinessTokens = append(readinessTokens, request.URL.Query().Get("accessToken"))
readinessMu.Unlock()
writeNacosResult(w, nacosAPIV3, "ok")
default:
http.NotFound(w, request)
}
}))
defer server.Close()
config := nacosAPITestConnectionConfig(t, server)
config.User = "nacos"
config.Password = "secret"
client := &ClientImpl{}
if err := client.Connect(config); err != nil {
t.Fatalf("Connect: %v", err)
}
defer client.Close()
readinessMu.Lock()
gotReadinessTokens := append([]string(nil), readinessTokens...)
readinessMu.Unlock()
if len(gotReadinessTokens) != 2 {
t.Fatalf("readiness requests = %d, want 2", len(gotReadinessTokens))
}
for index, token := range gotReadinessTokens {
if token != "" {
t.Fatalf("readiness request %d sent accessToken %q", index+1, token)
}
}
}
func TestClientReadinessRetriesWithTokenForAuthGatedProxy(t *testing.T) {
recorder := &nacosAPIRequestRecorder{}
var readinessMu sync.Mutex
var readinessTokens []string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
recorder.record(request)
switch request.URL.Path {
case "/v3/auth/user/login":
if request.Form.Get("username") != "nacos" || request.Form.Get("password") != "secret" {
http.Error(w, "unexpected credentials", http.StatusBadRequest)
return
}
writeNacosJSON(w, map[string]any{"accessToken": "v3-token", "tokenTtl": 3600})
case "/v1/auth/users/login":
http.Error(w, "unexpected legacy login", http.StatusInternalServerError)
case nacosV3ReadinessPath:
token := request.URL.Query().Get("accessToken")
readinessMu.Lock()
readinessTokens = append(readinessTokens, token)
readinessMu.Unlock()
if token != "v3-token" {
http.Error(w, "missing token", http.StatusForbidden)
return
}
writeNacosResult(w, nacosAPIV3, "ok")
default:
http.NotFound(w, request)
}
}))
defer server.Close()
config := nacosAPITestConnectionConfig(t, server)
config.User = "nacos"
config.Password = "secret"
client := &ClientImpl{}
if err := client.Connect(config); err != nil {
t.Fatalf("Connect: %v", err)
}
defer client.Close()
if got := recorder.countPath("/v3/auth/user/login"); got != 1 {
t.Fatalf("v3 login count = %d, want 1", got)
}
if got := recorder.countPath("/v1/auth/users/login"); got != 0 {
t.Fatalf("legacy login count = %d, want 0", got)
}
wantReadinessTokens := []string{"", "v3-token", "", "v3-token"}
readinessMu.Lock()
gotReadinessTokens := append([]string(nil), readinessTokens...)
readinessMu.Unlock()
if len(gotReadinessTokens) != len(wantReadinessTokens) {
t.Fatalf("readiness tokens = %#v, want %#v", gotReadinessTokens, wantReadinessTokens)
}
for index := range wantReadinessTokens {
if gotReadinessTokens[index] != wantReadinessTokens[index] {
t.Fatalf("readiness tokens = %#v, want %#v", gotReadinessTokens, wantReadinessTokens)
}
}
}
func TestClientAuthDoesNotFallbackAfterV3LoginForbidden(t *testing.T) {
recorder := &nacosAPIRequestRecorder{}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
recorder.record(request)
switch request.URL.Path {
case "/v3/auth/user/login":
http.Error(w, "invalid credentials", http.StatusForbidden)
case "/v1/auth/users/login":
writeNacosJSON(w, map[string]any{"accessToken": "legacy-token", "tokenTtl": 3600})
default:
http.NotFound(w, request)
}
}))
defer server.Close()
config := nacosAPITestConnectionConfig(t, server)
config.User = "nacos"
config.Password = "wrong"
client := &ClientImpl{}
err := client.Connect(config)
if err == nil {
t.Fatal("Connect unexpectedly succeeded after v3 login returned 403")
}
if !strings.Contains(err.Error(), "403") {
t.Fatalf("Connect error = %q, want HTTP 403", err)
}
if got := recorder.countPath("/v1/auth/users/login"); got != 0 {
t.Fatalf("legacy login count = %d, want 0 after forbidden", got)
}
}
func TestNacosV2ListServicesWithoutGroupUsesV1Catalog(t *testing.T) {
recorder := &nacosAPIRequestRecorder{}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
recorder.record(request)
switch request.URL.Path {
case nacosV3ReadinessPath:
http.NotFound(w, request)
case routesForNacosAPI(nacosAPIV2).namespaceList:
writeNacosResult(w, nacosAPIV2, []any{})
case routesForNacosAPI(nacosAPIV1).serviceList:
writeNacosJSON(w, map[string]any{
"count": 2,
"serviceList": []any{
map[string]any{"name": "orders", "groupName": "MKEFU"},
map[string]any{"name": "billing", "groupName": "FINANCE"},
},
})
default:
http.NotFound(w, request)
}
}))
defer server.Close()
client := connectAPIVersionTestClient(t, server)
defer client.Close()
page, err := client.ListServices(context.Background(), ServiceQuery{
NamespaceID: "dev-id", PageNo: 1, PageSize: 20,
})
if err != nil {
t.Fatalf("ListServices: %v", err)
}
want := []string{"MKEFU@@orders", "FINANCE@@billing"}
if page.Count != 2 || len(page.ServiceNames) != len(want) {
t.Fatalf("services = %#v", page)
}
for index := range want {
if page.ServiceNames[index] != want[index] {
t.Fatalf("serviceNames[%d] = %q, want %q", index, page.ServiceNames[index], want[index])
}
}
if got := recorder.countPath(routesForNacosAPI(nacosAPIV2).serviceList); got != 0 {
t.Fatalf("v2 single-group service list requests = %d, want 0", got)
}
request := mustLastNacosAPIRequest(t, recorder, http.MethodGet, routesForNacosAPI(nacosAPIV1).serviceList)
assertNacosAPIKeyPresent(t, request.values, "groupNameParam")
if request.values.Get("groupNameParam") != "" {
t.Fatalf("groupNameParam = %q, want empty", request.values.Get("groupNameParam"))
}
}
func TestNacosV3ListServicesFiltersExactGroupAcrossPages(t *testing.T) {
recorder := &nacosAPIRequestRecorder{}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
recorder.record(request)
switch request.URL.Path {
case nacosV3ReadinessPath:
writeNacosResult(w, nacosAPIV3, "ok")
case routesForNacosAPI(nacosAPIV3).serviceList:
pageNumber, _ := strconv.Atoi(request.Form.Get("pageNo"))
pageItems := []any{
map[string]any{"name": "checkout", "groupName": "PAY"},
map[string]any{"name": "billing", "groupName": "PAYMENT"},
}
if request.Form.Get("pageNo") == "2" {
pageItems = []any{
map[string]any{"name": "refund", "groupName": "PAY"},
map[string]any{"name": "payroll", "groupName": "PAYROLL"},
}
}
writeNacosResult(w, nacosAPIV3, map[string]any{
"totalCount": 4,
"pageNumber": pageNumber,
"pagesAvailable": 2,
"pageItems": pageItems,
})
default:
http.NotFound(w, request)
}
}))
defer server.Close()
client := connectAPIVersionTestClient(t, server)
defer client.Close()
page, err := client.ListServices(context.Background(), ServiceQuery{
NamespaceID: "dev-id",
GroupName: "PAY",
PageNo: 2,
PageSize: 1,
})
if err != nil {
t.Fatalf("ListServices: %v", err)
}
if page.Count != 2 || len(page.ServiceNames) != 1 || page.ServiceNames[0] != "PAY@@refund" {
t.Fatalf("exact group page = %#v", page)
}
recorder.mu.Lock()
requests := append([]recordedNacosAPIRequest(nil), recorder.requests...)
recorder.mu.Unlock()
serviceRequests := make([]recordedNacosAPIRequest, 0, 2)
for _, request := range requests {
if request.path == routesForNacosAPI(nacosAPIV3).serviceList {
serviceRequests = append(serviceRequests, request)
}
}
if len(serviceRequests) != 2 {
t.Fatalf("service list requests = %d, want 2", len(serviceRequests))
}
for index, request := range serviceRequests {
assertNacosAPIValues(t, request.values, map[string]string{
"namespaceId": "dev-id",
"groupNameParam": "PAY",
"pageNo": strconv.Itoa(index + 1),
"pageSize": strconv.Itoa(maxServicePageSize),
})
}
}
func TestNacosV3ListServicesEscapesExactGroupPattern(t *testing.T) {
const targetGroup = "PAY[1]"
recorder := &nacosAPIRequestRecorder{}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
recorder.record(request)
switch request.URL.Path {
case nacosV3ReadinessPath:
writeNacosResult(w, nacosAPIV3, "ok")
case routesForNacosAPI(nacosAPIV3).serviceList:
groupPattern, err := regexp.Compile(request.Form.Get("groupNameParam"))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
candidates := []nacosServiceItem{
{Name: "literal", GroupName: targetGroup},
{Name: "regex-lookalike", GroupName: "PAY1"},
{Name: "prefix", GroupName: targetGroup + "-ARCHIVE"},
}
pageItems := make([]nacosServiceItem, 0, len(candidates))
for _, candidate := range candidates {
if groupPattern.MatchString(candidate.GroupName) {
pageItems = append(pageItems, candidate)
}
}
writeNacosResult(w, nacosAPIV3, map[string]any{
"totalCount": len(pageItems),
"pageNumber": 1,
"pagesAvailable": 1,
"pageItems": pageItems,
})
default:
http.NotFound(w, request)
}
}))
defer server.Close()
client := connectAPIVersionTestClient(t, server)
defer client.Close()
page, err := client.ListServices(context.Background(), ServiceQuery{
NamespaceID: "dev-id",
GroupName: targetGroup,
PageNo: 1,
PageSize: 20,
})
if err != nil {
t.Fatalf("ListServices: %v", err)
}
if page.Count != 1 || len(page.ServiceNames) != 1 || page.ServiceNames[0] != targetGroup+"@@literal" {
t.Fatalf("exact group page = %#v", page)
}
request := mustLastNacosAPIRequest(t, recorder, http.MethodGet, routesForNacosAPI(nacosAPIV3).serviceList)
if got, want := request.values.Get("groupNameParam"), regexp.QuoteMeta(targetGroup); got != want {
t.Fatalf("groupNameParam = %q, want %q", got, want)
}
}
func TestCreateEphemeralServiceAPIVersionBoundary(t *testing.T) {
tests := []struct {
name string
family nacosAPIFamily
}{
{name: "v1 rejects before request", family: nacosAPIV1},
{name: "v2 forwards ephemeral", family: nacosAPIV2},
{name: "v3 forwards ephemeral", family: nacosAPIV3},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
recorder := &nacosAPIRequestRecorder{}
server := httptest.NewServer(nacosAPIMatrixHandler(test.family, recorder))
defer server.Close()
client := connectAPIVersionTestClient(t, server)
defer client.Close()
ephemeral := true
err := client.CreateService(context.Background(), CreateServiceRequest{
NamespaceID: "dev-id",
ServiceName: "orders",
GroupName: "MKEFU",
Ephemeral: &ephemeral,
})
routes := routesForNacosAPI(test.family)
if test.family == nacosAPIV1 {
if err == nil {
t.Fatal("expected v1 ephemeral service creation to fail")
}
if !strings.Contains(err.Error(), "Nacos v1") {
t.Fatalf("CreateService error = %q, want explicit Nacos v1 boundary", err)
}
if _, ok := recorder.last(http.MethodPost, routes.service); ok {
t.Fatal("v1 ephemeral service creation sent an HTTP request")
}
return
}
if err != nil {
t.Fatalf("CreateService: %v", err)
}
request := mustLastNacosAPIRequest(t, recorder, http.MethodPost, routes.service)
assertNacosAPIValues(t, request.values, map[string]string{"ephemeral": "true"})
})
}
}
func TestNacosV2GetConfigPreservesJSONContent(t *testing.T) {
recorder := &nacosAPIRequestRecorder{}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
recorder.record(request)
switch request.URL.Path {
case nacosV3ReadinessPath:
http.NotFound(w, request)
case routesForNacosAPI(nacosAPIV2).namespaceList:
writeNacosResult(w, nacosAPIV2, []any{})
case routesForNacosAPI(nacosAPIV2).config:
writeNacosResult(w, nacosAPIV2, `{"dataId":"inside-document","content":"literal-value"}`)
default:
http.NotFound(w, request)
}
}))
defer server.Close()
client := connectAPIVersionTestClient(t, server)
defer client.Close()
detail, err := client.GetConfig(context.Background(), "dev-id", "MKEFU", "app.json")
if err != nil {
t.Fatalf("GetConfig: %v", err)
}
want := `{"dataId":"inside-document","content":"literal-value"}`
if detail.Content != want || detail.DataID != "app.json" {
t.Fatalf("config detail = %#v, want content %q for app.json", detail, want)
}
}
func assertProbeSequenceAndCache(
t *testing.T,
client *ClientImpl,
recorder *nacosAPIRequestRecorder,
family nacosAPIFamily,
) {
t.Helper()
readinessPath := nacosV3ReadinessPath
v3Path := routesForNacosAPI(nacosAPIV3).namespaceList
v2Path := routesForNacosAPI(nacosAPIV2).namespaceList
v1Path := routesForNacosAPI(nacosAPIV1).namespaceList
wantBefore := map[nacosAPIFamily]map[string]int{
nacosAPIV3: {readinessPath: 2, v3Path: 0, v2Path: 0, v1Path: 0},
nacosAPIV2: {readinessPath: 1, v3Path: 0, v2Path: 2, v1Path: 0},
nacosAPIV1: {readinessPath: 1, v3Path: 0, v2Path: 1, v1Path: 2},
}[family]
for path, want := range wantBefore {
if got := recorder.countPath(path); got != want {
t.Fatalf("probe request count for %s = %d, want %d", path, got, want)
}
}
namespaces, err := client.ListNamespaces(context.Background())
if err != nil {
t.Fatalf("ListNamespaces with cached family: %v", err)
}
if len(namespaces) != 1 || namespaces[0].ID != "dev-id" || namespaces[0].ShowName != "Development" {
t.Fatalf("namespaces = %#v", namespaces)
}
selectedPath := routesForNacosAPI(family).namespaceList
for path, before := range wantBefore {
want := before
if path == selectedPath {
want++
}
if got := recorder.countPath(path); got != want {
t.Fatalf("cached namespace request count for %s = %d, want %d", path, got, want)
}
}
}
func exerciseNacosAPIFamily(
t *testing.T,
client *ClientImpl,
recorder *nacosAPIRequestRecorder,
test struct {
name string
family nacosAPIFamily
configListGroupKey string
configListNamespaceKey string
configGroupKey string
configNamespaceKey string
serviceListGroupKey string
qualifiedNaming bool
},
) {
t.Helper()
ctx := context.Background()
routes := routesForNacosAPI(test.family)
if err := client.CreateNamespace(ctx, CreateNamespaceRequest{
ID: "qa-id", ShowName: "QA", Description: "quality",
}); err != nil {
t.Fatalf("CreateNamespace: %v", err)
}
request := mustLastNacosAPIRequest(t, recorder, http.MethodPost, routes.namespace)
if test.family == nacosAPIV1 {
assertNacosAPIValues(t, request.values, map[string]string{
"customNamespaceId": "qa-id", "namespaceName": "QA", "namespaceDesc": "quality",
})
} else {
assertNacosAPIValues(t, request.values, map[string]string{
"namespaceId": "qa-id", "namespaceName": "QA", "namespaceDesc": "quality",
})
}
if err := client.UpdateNamespace(ctx, UpdateNamespaceRequest{
ID: "qa-id", ShowName: "QA 2", Description: "updated",
}); err != nil {
t.Fatalf("UpdateNamespace: %v", err)
}
request = mustLastNacosAPIRequest(t, recorder, http.MethodPut, routes.namespace)
if test.family == nacosAPIV1 {
assertNacosAPIValues(t, request.values, map[string]string{
"namespace": "qa-id", "namespaceShowName": "QA 2", "namespaceDesc": "updated",
})
} else {
assertNacosAPIValues(t, request.values, map[string]string{
"namespaceId": "qa-id", "namespaceName": "QA 2", "namespaceDesc": "updated",
})
}
if err := client.DeleteNamespace(ctx, "qa-id"); err != nil {
t.Fatalf("DeleteNamespace: %v", err)
}
request = mustLastNacosAPIRequest(t, recorder, http.MethodDelete, routes.namespace)
assertNacosAPIValues(t, request.values, map[string]string{"namespaceId": "qa-id"})
page, err := client.SearchConfigs(ctx, ConfigQuery{
NamespaceID: "dev-id",
DataID: "app",
Group: "MKEFU",
PageNo: 2,
PageSize: 15,
Search: "blur",
})
if err != nil {
t.Fatalf("SearchConfigs: %v", err)
}
if page.TotalCount != 1 || len(page.PageItems) != 1 || page.PageItems[0].DataID != "app.yaml" ||
page.PageItems[0].Group != "MKEFU" || page.PageItems[0].NamespaceID != "dev-id" {
t.Fatalf("config page = %#v", page)
}
request = mustLastNacosAPIRequest(t, recorder, http.MethodGet, routes.configList)
assertNacosAPIValues(t, request.values, map[string]string{
"dataId": "app",
test.configListGroupKey: "MKEFU",
test.configListNamespaceKey: "dev-id",
"pageNo": "2",
"pageSize": "15",
})
if test.family == nacosAPIV2 {
assertNacosAPIKeyPresent(t, request.values, "config_detail")
}
assertNacosAPIKeysAbsent(t, request.values,
otherNacosConfigGroupKey(test.configListGroupKey),
otherNacosConfigNamespaceKey(test.configListNamespaceKey),
)
detail, err := client.GetConfig(ctx, "dev-id", "MKEFU", "app.yaml")
if err != nil {
t.Fatalf("GetConfig: %v", err)
}
if detail.Content != "versioned-content" || detail.Group != "MKEFU" || detail.NamespaceID != "dev-id" {
t.Fatalf("config detail = %#v", detail)
}
request = mustLastNacosAPIRequest(t, recorder, http.MethodGet, routes.config)
assertNacosAPIValues(t, request.values, map[string]string{
"dataId": "app.yaml",
test.configGroupKey: "MKEFU",
test.configNamespaceKey: "dev-id",
})
assertNacosAPIKeysAbsent(t, request.values,
otherNacosConfigGroupKey(test.configGroupKey),
otherNacosConfigNamespaceKey(test.configNamespaceKey),
)
if err := client.PublishConfig(ctx, PublishRequest{
NamespaceID: "dev-id",
DataID: "app.yaml",
Group: "MKEFU",
Content: "new-content",
Type: "yaml",
BetaIPs: "10.0.0.10",
}); err != nil {
t.Fatalf("PublishConfig: %v", err)
}
request = mustLastNacosAPIRequest(t, recorder, http.MethodPost, routes.config)
assertNacosAPIValues(t, request.values, map[string]string{
"dataId": "app.yaml",
test.configGroupKey: "MKEFU",
test.configNamespaceKey: "dev-id",
"content": "new-content",
"type": "yaml",
})
if got := request.header.Get("betaIps"); got != "10.0.0.10" {
t.Fatalf("betaIps header = %q, want %q", got, "10.0.0.10")
}
if err := client.DeleteConfig(ctx, "dev-id", "MKEFU", "app.yaml"); err != nil {
t.Fatalf("DeleteConfig: %v", err)
}
request = mustLastNacosAPIRequest(t, recorder, http.MethodDelete, routes.config)
assertNacosAPIValues(t, request.values, map[string]string{
"dataId": "app.yaml",
test.configGroupKey: "MKEFU",
test.configNamespaceKey: "dev-id",
})
beta, err := client.GetBetaConfig(ctx, "dev-id", "MKEFU", "app.yaml")
if err != nil {
t.Fatalf("GetBetaConfig: %v", err)
}
if !beta.Exists || beta.Content != "beta-content" || beta.BetaIPs != "10.0.0.10" {
t.Fatalf("beta config = %#v", beta)
}
request = mustLastNacosAPIRequest(t, recorder, http.MethodGet, routes.beta)
if test.family == nacosAPIV3 {
assertNacosAPIValues(t, request.values, map[string]string{
"dataId": "app.yaml", "groupName": "MKEFU", "namespaceId": "dev-id",
})
} else {
assertNacosAPIValues(t, request.values, map[string]string{
"dataId": "app.yaml", "group": "MKEFU", "tenant": "dev-id", "beta": "true",
})
}
if err := client.StopBetaConfig(ctx, "dev-id", "MKEFU", "app.yaml"); err != nil {
t.Fatalf("StopBetaConfig: %v", err)
}
request = mustLastNacosAPIRequest(t, recorder, http.MethodDelete, routes.beta)
if test.family == nacosAPIV3 {
assertNacosAPIValues(t, request.values, map[string]string{
"dataId": "app.yaml", "groupName": "MKEFU", "namespaceId": "dev-id",
})
} else {
assertNacosAPIValues(t, request.values, map[string]string{
"dataId": "app.yaml", "group": "MKEFU", "tenant": "dev-id", "beta": "true",
})
}
history, err := client.ListConfigHistory(ctx, HistoryQuery{
NamespaceID: "dev-id",
DataID: "app.yaml",
Group: "MKEFU",
PageNo: 1,
PageSize: 10,
})
if err != nil {
t.Fatalf("ListConfigHistory: %v", err)
}
if history.TotalCount != 1 || len(history.PageItems) != 1 || history.PageItems[0].ID != "203" {
t.Fatalf("history page = %#v", history)
}
request = mustLastNacosAPIRequest(t, recorder, http.MethodGet, routes.historyList)
assertNacosAPIValues(t, request.values, map[string]string{
"dataId": "app.yaml",
test.configGroupKey: "MKEFU",
test.configNamespaceKey: "dev-id",
"pageNo": "1",
"pageSize": "10",
})
historyDetail, err := client.GetConfigHistory(ctx, "dev-id", "MKEFU", "app.yaml", "203")
if err != nil {
t.Fatalf("GetConfigHistory: %v", err)
}
if historyDetail.ID != "203" || historyDetail.Content != "old-content" {
t.Fatalf("history detail = %#v", historyDetail)
}
request = mustLastNacosAPIRequest(t, recorder, http.MethodGet, routes.configHistory)
assertNacosAPIValues(t, request.values, map[string]string{
"dataId": "app.yaml",
test.configGroupKey: "MKEFU",
test.configNamespaceKey: "dev-id",
"nid": "203",
})
services, err := client.ListServices(ctx, ServiceQuery{
NamespaceID: "dev-id",
GroupName: "MKEFU",
PageNo: 1,
PageSize: 20,
})
if err != nil {
t.Fatalf("ListServices: %v", err)
}
if services.Count != 1 || len(services.ServiceNames) != 1 || services.ServiceNames[0] != "MKEFU@@orders" {
t.Fatalf("service page = %#v", services)
}
serviceListPath := routes.serviceListByGroup
request = mustLastNacosAPIRequest(t, recorder, http.MethodGet, serviceListPath)
servicePageSize := "20"
if test.family == nacosAPIV3 {
servicePageSize = strconv.Itoa(maxServicePageSize)
}
assertNacosAPIValues(t, request.values, map[string]string{
"namespaceId": "dev-id",
test.serviceListGroupKey: "MKEFU",
"pageNo": "1",
"pageSize": servicePageSize,
})
service, err := client.GetService(ctx, "dev-id", "orders", "MKEFU")
if err != nil {
t.Fatalf("GetService: %v", err)
}
if service.Name != "orders" || service.GroupName != "MKEFU" || service.NamespaceID != "dev-id" || !service.Ephemeral {
t.Fatalf("service detail = %#v", service)
}
request = mustLastNacosAPIRequest(t, recorder, http.MethodGet, routes.service)
assertNacosNamingIdentity(t, request.values, test.qualifiedNaming)
serviceEphemeral := false
if err := client.CreateService(ctx, CreateServiceRequest{
NamespaceID: "dev-id", ServiceName: "orders", GroupName: "MKEFU", ProtectThreshold: 0.5,
Ephemeral: &serviceEphemeral,
}); err != nil {
t.Fatalf("CreateService: %v", err)
}
request = mustLastNacosAPIRequest(t, recorder, http.MethodPost, routes.service)
assertNacosNamingIdentity(t, request.values, test.qualifiedNaming)
if test.family == nacosAPIV1 {
if _, ok := request.values["ephemeral"]; ok {
t.Fatalf("v1 CreateService unexpectedly sent ephemeral=%q", request.values.Get("ephemeral"))
}
} else {
assertNacosAPIValues(t, request.values, map[string]string{"ephemeral": "false"})
}
if err := client.UpdateService(ctx, UpdateServiceRequest{
NamespaceID: "dev-id", ServiceName: "orders", GroupName: "MKEFU", ProtectThreshold: 0.25,
}); err != nil {
t.Fatalf("UpdateService: %v", err)
}
request = mustLastNacosAPIRequest(t, recorder, http.MethodPut, routes.service)
assertNacosNamingIdentity(t, request.values, test.qualifiedNaming)
if err := client.DeleteService(ctx, "dev-id", "orders", "MKEFU"); err != nil {
t.Fatalf("DeleteService: %v", err)
}
request = mustLastNacosAPIRequest(t, recorder, http.MethodDelete, routes.service)
assertNacosNamingIdentity(t, request.values, test.qualifiedNaming)
instances, err := client.ListInstances(ctx, InstanceQuery{
NamespaceID: "dev-id",
ServiceName: "orders",
GroupName: "MKEFU",
Clusters: "DEFAULT",
HealthyOnly: true,
})
if err != nil {
t.Fatalf("ListInstances: %v", err)
}
if len(instances.Hosts) != 1 || instances.Hosts[0].IP != "10.0.0.1" || instances.Hosts[0].Port != 8080 {
t.Fatalf("instances = %#v", instances)
}
request = mustLastNacosAPIRequest(t, recorder, http.MethodGet, routes.instanceList)
assertNacosNamingIdentity(t, request.values, test.qualifiedNaming)
zeroWeight := 0.0
instanceRequest := InstanceRequest{
NamespaceID: "dev-id",
ServiceName: "orders",
GroupName: "MKEFU",
IP: "10.0.0.1",
Port: 8080,
ClusterName: "DEFAULT",
Weight: &zeroWeight,
}
instance, err := client.GetInstance(ctx, instanceRequest)
if err != nil {
t.Fatalf("GetInstance: %v", err)
}
if instance.IP != "10.0.0.1" || instance.Port != 8080 {
t.Fatalf("instance detail = %#v", instance)
}
request = mustLastNacosAPIRequest(t, recorder, http.MethodGet, routes.instance)
assertNacosNamingIdentity(t, request.values, test.qualifiedNaming)
ephemeral := false
enabled := true
instanceRequest.Ephemeral = &ephemeral
instanceRequest.Enabled = &enabled
if err := client.RegisterInstance(ctx, instanceRequest); err != nil {
t.Fatalf("RegisterInstance: %v", err)
}
request = mustLastNacosAPIRequest(t, recorder, http.MethodPost, routes.instance)
assertNacosNamingIdentity(t, request.values, test.qualifiedNaming)
assertNacosAPIValues(t, request.values, map[string]string{
"ephemeral": "false",
"weight": "0",
})
if err := client.UpdateInstance(ctx, instanceRequest); err != nil {
t.Fatalf("UpdateInstance: %v", err)
}
request = mustLastNacosAPIRequest(t, recorder, http.MethodPut, routes.instance)
assertNacosNamingIdentity(t, request.values, test.qualifiedNaming)
assertNacosAPIValues(t, request.values, map[string]string{
"ephemeral": "false",
"weight": "0",
})
if err := client.DeregisterInstance(ctx, instanceRequest); err != nil {
t.Fatalf("DeregisterInstance: %v", err)
}
request = mustLastNacosAPIRequest(t, recorder, http.MethodDelete, routes.instance)
assertNacosNamingIdentity(t, request.values, test.qualifiedNaming)
healthy := false
instanceRequest.Healthy = &healthy
if err := client.UpdateInstanceHealth(ctx, instanceRequest); err != nil {
t.Fatalf("UpdateInstanceHealth: %v", err)
}
request = mustLastNacosAPIRequest(t, recorder, http.MethodPut, routes.health)
assertNacosNamingIdentity(t, request.values, test.qualifiedNaming)
assertNacosAPIValues(t, request.values, map[string]string{"healthy": "false"})
}
func nacosAPIMatrixHandler(family nacosAPIFamily, recorder *nacosAPIRequestRecorder) http.Handler {
routes := routesForNacosAPI(family)
return http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
recorder.record(request)
values := request.Form
switch {
case request.Method == http.MethodGet && request.URL.Path == nacosV3ReadinessPath:
if family == nacosAPIV3 {
writeNacosResult(w, nacosAPIV3, "ok")
} else {
http.NotFound(w, request)
}
case request.Method == http.MethodGet && request.URL.Path == routes.namespaceList:
writeNacosResult(w, family, []map[string]any{{
"namespace": "dev-id",
"namespaceShowName": "Development",
"namespaceDesc": "test namespace",
}})
case (request.Method == http.MethodPost || request.Method == http.MethodPut || request.Method == http.MethodDelete) &&
request.URL.Path == routes.namespace:
writeNacosMutationResult(w, family, true)
case request.Method == http.MethodGet && request.URL.Path == routes.configList && values.Get("search") != "":
writeNacosConfigList(w, family)
case request.Method == http.MethodGet && request.URL.Path == routes.beta && isNacosBetaRequest(family, values):
writeNacosBeta(w, family)
case request.Method == http.MethodDelete && request.URL.Path == routes.beta && isNacosBetaRequest(family, values):
writeNacosMutationResult(w, family, true)
case request.Method == http.MethodGet && request.URL.Path == routes.config && values.Get("dataId") != "":
writeNacosConfigDetail(w, family)
case request.Method == http.MethodPost && request.URL.Path == routes.config:
writeNacosMutationResult(w, family, true)
case request.Method == http.MethodDelete && request.URL.Path == routes.config:
writeNacosMutationResult(w, family, true)
case request.Method == http.MethodGet && request.URL.Path == routes.historyList && isNacosHistoryListRequest(family, values):
writeNacosHistory(w, family, false)
case request.Method == http.MethodGet && request.URL.Path == routes.configHistory && values.Get("nid") != "":
writeNacosHistory(w, family, true)
case request.Method == http.MethodGet && request.URL.Path == routes.serviceListByGroup && nacosServiceGroupValue(family, values) != "":
writeNacosGroupedServiceList(w, family)
case request.Method == http.MethodGet && request.URL.Path == routes.serviceList:
writeNacosServiceList(w, family)
case request.Method == http.MethodGet && request.URL.Path == routes.service:
writeNacosFamilyData(w, family, map[string]any{
"name": "orders",
"groupName": "MKEFU",
"namespaceId": "dev-id",
"ephemeral": true,
"protectThreshold": 0.5,
"metadata": map[string]string{"owner": "team-a"},
"clusters": []any{},
})
case (request.Method == http.MethodPost || request.Method == http.MethodPut || request.Method == http.MethodDelete) &&
request.URL.Path == routes.service:
writeNacosMutationResult(w, family, "ok")
case request.Method == http.MethodGet && request.URL.Path == routes.instanceList:
writeNacosInstanceList(w, family)
case request.Method == http.MethodGet && request.URL.Path == routes.instance:
writeNacosFamilyData(w, family, nacosAPIInstanceFixture())
case (request.Method == http.MethodPost || request.Method == http.MethodPut || request.Method == http.MethodDelete) &&
request.URL.Path == routes.instance:
writeNacosMutationResult(w, family, "ok")
case request.Method == http.MethodPut && request.URL.Path == routes.health:
writeNacosMutationResult(w, family, "ok")
default:
http.NotFound(w, request)
}
})
}
func isNacosHistoryListRequest(family nacosAPIFamily, values url.Values) bool {
if family == nacosAPIV1 {
return values.Get("search") == "accurate"
}
return values.Get("nid") == ""
}
func isNacosBetaRequest(family nacosAPIFamily, values url.Values) bool {
return family == nacosAPIV3 || values.Get("beta") == "true"
}
func writeNacosBeta(w http.ResponseWriter, family nacosAPIFamily) {
data := map[string]any{
"dataId": "app.yaml",
"content": "beta-content",
"type": "yaml",
"md5": ContentMD5("beta-content"),
}
if family == nacosAPIV3 {
data["groupName"] = "MKEFU"
data["namespaceId"] = "dev-id"
data["grayRule"] = `{"type":"beta","version":"1.0.0","expr":"10.0.0.10","priority":2147483647}`
writeNacosResult(w, family, data)
return
}
data["group"] = "MKEFU"
data["tenant"] = "dev-id"
data["betaIps"] = "10.0.0.10"
writeNacosJSON(w, map[string]any{
"code": 200, "message": "success", "data": data,
})
}
func writeNacosConfigList(w http.ResponseWriter, family nacosAPIFamily) {
item := map[string]any{
"id": "10",
"dataId": "app.yaml",
"content": "versioned-content",
"type": "yaml",
}
if family == nacosAPIV3 {
item["groupName"] = "MKEFU"
item["namespaceId"] = "dev-id"
item["modifyTime"] = "2026-07-28T01:00:00Z"
} else {
item["group"] = "MKEFU"
item["tenant"] = "dev-id"
item["lastModifiedTime"] = "2026-07-28T01:00:00Z"
}
page := map[string]any{
"totalCount": 1,
"pageNumber": 2,
"pagesAvailable": 2,
"pageItems": []any{item},
}
if family == nacosAPIV3 {
writeNacosResult(w, family, page)
return
}
writeNacosJSON(w, page)
}
func writeNacosConfigDetail(w http.ResponseWriter, family nacosAPIFamily) {
switch family {
case nacosAPIV2:
writeNacosResult(w, family, "versioned-content")
case nacosAPIV3:
writeNacosResult(w, family, map[string]any{
"dataId": "app.yaml",
"groupName": "MKEFU",
"namespaceId": "dev-id",
"content": "versioned-content",
"type": "yaml",
})
default:
writeNacosJSON(w, map[string]any{
"dataId": "app.yaml",
"group": "MKEFU",
"tenant": "dev-id",
"content": "versioned-content",
"type": "yaml",
})
}
}
func writeNacosHistory(w http.ResponseWriter, family nacosAPIFamily, detail bool) {
item := map[string]any{
"id": "203",
"dataId": "app.yaml",
"content": "old-content",
"md5": "old-md5",
"opType": "U",
}
if family == nacosAPIV3 {
item["groupName"] = "MKEFU"
item["namespaceId"] = "dev-id"
item["modifyTime"] = "2026-07-28T00:00:00Z"
} else {
item["group"] = "MKEFU"
item["tenant"] = "dev-id"
item["lastModifiedTime"] = "2026-07-28T00:00:00Z"
}
data := any(item)
if !detail {
data = map[string]any{
"totalCount": 1,
"pageNumber": 1,
"pagesAvailable": 1,
"pageItems": []any{item},
}
}
writeNacosFamilyData(w, family, data)
}
func writeNacosServiceList(w http.ResponseWriter, family nacosAPIFamily) {
switch family {
case nacosAPIV1:
writeNacosJSON(w, map[string]any{
"count": 1,
"serviceList": []any{
map[string]any{"name": "orders", "groupName": "MKEFU"},
},
})
case nacosAPIV2:
writeNacosResult(w, family, map[string]any{
"count": 1,
"services": []string{"MKEFU@@orders"},
})
default:
writeNacosResult(w, family, map[string]any{
"totalCount": 1,
"pageNumber": 1,
"pagesAvailable": 1,
"pageItems": []any{
map[string]any{"name": "orders", "groupName": "MKEFU"},
},
})
}
}
func nacosServiceGroupValue(family nacosAPIFamily, values url.Values) string {
if family == nacosAPIV3 {
return values.Get("groupNameParam")
}
return values.Get("groupName")
}
func writeNacosGroupedServiceList(w http.ResponseWriter, family nacosAPIFamily) {
if family == nacosAPIV1 {
writeNacosJSON(w, map[string]any{
"count": 1,
"doms": []string{"orders"},
})
return
}
writeNacosServiceList(w, family)
}
func writeNacosInstanceList(w http.ResponseWriter, family nacosAPIFamily) {
instance := nacosAPIInstanceFixture()
if family == nacosAPIV3 {
writeNacosResult(w, family, []any{instance})
return
}
writeNacosFamilyData(w, family, map[string]any{
"name": "MKEFU@@orders",
"groupName": "MKEFU",
"hosts": []any{instance},
})
}
func nacosAPIInstanceFixture() map[string]any {
return map[string]any{
"instanceId": "instance-1",
"ip": "10.0.0.1",
"port": 8080,
"weight": 1,
"healthy": true,
"enabled": true,
"ephemeral": false,
"clusterName": "DEFAULT",
"serviceName": "MKEFU@@orders",
"metadata": map[string]string{"zone": "a"},
}
}
func writeNacosFamilyData(w http.ResponseWriter, family nacosAPIFamily, data any) {
if family == nacosAPIV1 {
writeNacosJSON(w, data)
return
}
writeNacosResult(w, family, data)
}
func writeNacosMutationResult(w http.ResponseWriter, family nacosAPIFamily, data any) {
if family == nacosAPIV1 {
switch value := data.(type) {
case bool:
_, _ = io.WriteString(w, strconv.FormatBool(value))
case string:
_, _ = io.WriteString(w, value)
}
return
}
writeNacosResult(w, family, data)
}
func writeNacosResult(w http.ResponseWriter, family nacosAPIFamily, data any) {
code := 0
if family == nacosAPIV1 {
code = 200
}
writeNacosJSON(w, map[string]any{
"code": code,
"message": "success",
"data": data,
})
}
func writeNacosJSON(w http.ResponseWriter, value any) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(value)
}
func connectAPIVersionTestClient(t *testing.T, server *httptest.Server) *ClientImpl {
t.Helper()
client := &ClientImpl{}
if err := client.Connect(nacosAPITestConnectionConfig(t, server)); err != nil {
t.Fatalf("Connect: %v", err)
}
return client
}
func nacosAPITestConnectionConfig(t *testing.T, server *httptest.Server) connection.ConnectionConfig {
t.Helper()
parsed, err := url.Parse(server.URL)
if err != nil {
t.Fatal(err)
}
port, err := strconv.Atoi(parsed.Port())
if err != nil {
t.Fatal(err)
}
return connection.ConnectionConfig{
Type: "nacos",
Host: parsed.Hostname(),
Port: port,
Timeout: 5,
ConnectionParams: "contextPath=/",
}
}
func mustLastNacosAPIRequest(
t *testing.T,
recorder *nacosAPIRequestRecorder,
method, path string,
) recordedNacosAPIRequest {
t.Helper()
request, ok := recorder.last(method, path)
if !ok {
t.Fatalf("request %s %s was not recorded", method, path)
}
return request
}
func assertNacosAPIValues(t *testing.T, values url.Values, expected map[string]string) {
t.Helper()
for key, want := range expected {
if got := values.Get(key); got != want {
t.Errorf("request parameter %s = %q, want %q; values=%#v", key, got, want, values)
}
}
}
func assertNacosAPIKeyPresent(t *testing.T, values url.Values, key string) {
t.Helper()
if _, ok := values[key]; !ok {
t.Errorf("request parameter %s is absent; values=%#v", key, values)
}
}
func assertNacosAPIKeysAbsent(t *testing.T, values url.Values, keys ...string) {
t.Helper()
for _, key := range keys {
if key == "" {
continue
}
if _, ok := values[key]; ok {
t.Errorf("unexpected request parameter %s; values=%#v", key, values)
}
}
}
func assertNacosNamingIdentity(t *testing.T, values url.Values, qualified bool) {
t.Helper()
wantServiceName := "orders"
if qualified {
wantServiceName = "MKEFU@@orders"
}
assertNacosAPIValues(t, values, map[string]string{
"namespaceId": "dev-id",
"groupName": "MKEFU",
"serviceName": wantServiceName,
})
}
func otherNacosConfigGroupKey(key string) string {
if key == "group" {
return "groupName"
}
return "group"
}
func otherNacosConfigNamespaceKey(key string) string {
if key == "tenant" {
return "namespaceId"
}
return "tenant"
}