Files
configcenter/internal/api/grpcapi/server_test.go

279 lines
9.7 KiB
Go

package grpcapi
import (
"context"
"encoding/json"
"log/slog"
"net"
"strings"
"testing"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"google.golang.org/grpc/test/bufconn"
authpkg "github.com/longpeng/configcenter/internal/auth"
"github.com/longpeng/configcenter/internal/domain"
"github.com/longpeng/configcenter/internal/metrics"
memoryruntime "github.com/longpeng/configcenter/internal/runtime/memory"
memory_store "github.com/longpeng/configcenter/internal/store/memory"
"github.com/longpeng/configcenter/internal/watch"
configcenterv1 "github.com/longpeng/configcenter/pkg/proto/v1"
)
func TestConfigServiceGetAndWatchFromRevision(t *testing.T) {
testServer := newTestServer(t, false, 8*time.Hour)
putRuntime(t, testServer.runtime, 1, map[string]string{"feature": "one"})
putRuntime(t, testServer.runtime, 2, map[string]string{"feature": "two"})
putRuntime(t, testServer.runtime, 3, map[string]string{"feature": "three"})
response, err := testServer.config.GetConfig(context.Background(), &configcenterv1.GetConfigRequest{
Env: "DEV", App: "demo-service", Namespace: "application",
})
if err != nil {
t.Fatal(err)
}
if response.GetRevision() != 3 || response.GetReleaseVersion() != 3 || itemValue(response.GetItems(), "feature") != "three" {
t.Fatalf("unexpected get response: %#v", response)
}
watchContext, cancel := context.WithCancel(context.Background())
defer cancel()
stream, err := testServer.config.WatchConfig(watchContext, &configcenterv1.WatchConfigRequest{
Env: "DEV", App: "demo-service", Namespace: "application", StartRevision: 2,
})
if err != nil {
t.Fatal(err)
}
first, err := stream.Recv()
if err != nil {
t.Fatal(err)
}
second, err := stream.Recv()
if err != nil {
t.Fatal(err)
}
if first.GetRevision() != 2 || first.GetType() != configcenterv1.ConfigEvent_UPDATED || itemValue(first.GetItems(), "feature") != "two" {
t.Fatalf("unexpected first replay event: %#v", first)
}
if second.GetRevision() != 3 || second.GetType() != configcenterv1.ConfigEvent_UPDATED || itemValue(second.GetItems(), "feature") != "three" {
t.Fatalf("unexpected second replay event: %#v", second)
}
}
func TestWatchFallsBackToFullSyncAfterCompaction(t *testing.T) {
testServer := newTestServer(t, false, 8*time.Hour)
for version := 1; version <= 257; version++ {
putRuntime(t, testServer.runtime, version, map[string]string{"version": string(rune('A' + (version % 26)))})
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
stream, err := testServer.config.WatchConfig(ctx, &configcenterv1.WatchConfigRequest{
Env: "DEV", App: "demo-service", Namespace: "application", StartRevision: 1,
})
if err != nil {
t.Fatal(err)
}
event, err := stream.Recv()
if err != nil {
t.Fatal(err)
}
if event.GetType() != configcenterv1.ConfigEvent_FULL_SYNC || event.GetRevision() != 257 {
t.Fatalf("unexpected compact recovery event: %#v", event)
}
}
func TestWatchReconnectsFromLastSeenRevisionPlusOne(t *testing.T) {
testServer := newTestServer(t, false, 8*time.Hour)
putRuntime(t, testServer.runtime, 1, map[string]string{"feature": "one"})
firstContext, cancelFirst := context.WithCancel(context.Background())
firstStream, err := testServer.config.WatchConfig(firstContext, &configcenterv1.WatchConfigRequest{
Env: "DEV", App: "demo-service", Namespace: "application",
})
if err != nil {
t.Fatal(err)
}
initial, err := firstStream.Recv()
if err != nil {
t.Fatal(err)
}
if initial.GetType() != configcenterv1.ConfigEvent_FULL_SYNC || initial.GetRevision() != 1 {
t.Fatalf("unexpected initial watch event: %#v", initial)
}
cancelFirst()
putRuntime(t, testServer.runtime, 2, map[string]string{"feature": "two"})
putRuntime(t, testServer.runtime, 3, map[string]string{"feature": "three"})
secondContext, cancelSecond := context.WithCancel(context.Background())
defer cancelSecond()
secondStream, err := testServer.config.WatchConfig(secondContext, &configcenterv1.WatchConfigRequest{
Env: "DEV", App: "demo-service", Namespace: "application", StartRevision: initial.GetRevision() + 1,
})
if err != nil {
t.Fatal(err)
}
second, err := secondStream.Recv()
if err != nil {
t.Fatal(err)
}
third, err := secondStream.Recv()
if err != nil {
t.Fatal(err)
}
if second.GetType() != configcenterv1.ConfigEvent_UPDATED || second.GetRevision() != 2 || itemValue(second.GetItems(), "feature") != "two" {
t.Fatalf("unexpected event after reconnect: %#v", second)
}
if third.GetType() != configcenterv1.ConfigEvent_UPDATED || third.GetRevision() != 3 || itemValue(third.GetItems(), "feature") != "three" {
t.Fatalf("unexpected second event after reconnect: %#v", third)
}
}
func TestAdminServicePublishAndRollbackUseAuthenticatedActor(t *testing.T) {
testServer := newTestServer(t, false, 8*time.Hour)
if _, err := testServer.repository.UpdateConfigItem(context.Background(), 30, domain.ConfigItem{Value: "9090", UpdatedBy: "editor"}); err != nil {
t.Fatal(err)
}
ctx := metadata.NewOutgoingContext(context.Background(), metadata.Pairs("x-user", "grpc-admin"))
published, err := testServer.admin.PublishConfig(ctx, &configcenterv1.PublishRequest{
EnvId: 1, AppId: 10, NamespaceId: 20, Comment: "grpc publish", Operator: "spoofed",
})
if err != nil {
t.Fatal(err)
}
if published.GetReleaseVersion() != 2 || published.GetStatus() != "pending" {
t.Fatalf("unexpected publish response: %#v", published)
}
release, err := testServer.repository.GetRelease(context.Background(), published.GetReleaseId())
if err != nil {
t.Fatal(err)
}
if release.Operator != "grpc-admin" {
t.Fatalf("client supplied operator was trusted: %#v", release)
}
rolledBack, err := testServer.admin.RollbackConfig(ctx, &configcenterv1.RollbackRequest{
EnvId: 1, AppId: 10, NamespaceId: 20, TargetVersion: 1, Operator: "spoofed",
})
if err != nil {
t.Fatal(err)
}
if rolledBack.GetNewReleaseVersion() != 3 || rolledBack.GetStatus() != "pending" {
t.Fatalf("unexpected rollback response: %#v", rolledBack)
}
}
func TestAuthenticationRejectsMissingRoleAndExpiredToken(t *testing.T) {
const testPassword = "correct-horse-battery-staple"
testServer := newTestServer(t, true, 8*time.Hour)
user, err := testServer.authorizer.CreateUser(context.Background(), domain.User{Username: "reader", DisplayName: "Reader"}, testPassword)
if err != nil {
t.Fatal(err)
}
if user.ID == 0 {
t.Fatal("user was not created")
}
token, _, err := testServer.authorizer.Login(context.Background(), "reader", testPassword)
if err != nil {
t.Fatal(err)
}
_, err = testServer.config.GetConfig(bearerContext(token), &configcenterv1.GetConfigRequest{Env: "DEV", App: "demo-service", Namespace: "application"})
if status.Code(err) != codes.PermissionDenied {
t.Fatalf("expected permission denied, got %v", err)
}
expiring := newTestServer(t, true, time.Millisecond)
adminToken, _, err := expiring.authorizer.Login(context.Background(), "admin", testPassword)
if err != nil {
t.Fatal(err)
}
time.Sleep(1100 * time.Millisecond)
_, err = expiring.config.GetConfig(bearerContext(adminToken), &configcenterv1.GetConfigRequest{Env: "DEV", App: "demo-service", Namespace: "application"})
if status.Code(err) != codes.Unauthenticated {
t.Fatalf("expected unauthenticated for expired token, got %v", err)
}
}
type grpcTestServer struct {
config configcenterv1.ConfigServiceClient
admin configcenterv1.AdminServiceClient
runtime *memoryruntime.Store
repository *memory_store.Store
authorizer *authpkg.Manager
}
func newTestServer(t *testing.T, authEnabled bool, ttl time.Duration) grpcTestServer {
t.Helper()
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
repository := memory_store.New(true)
runtimeStore := memoryruntime.New()
authorizer, err := authpkg.New(repository, authEnabled, strings.Repeat("s", 32), ttl)
if err != nil {
t.Fatal(err)
}
if authEnabled {
if err := authorizer.Bootstrap(ctx, "admin", "correct-horse-battery-staple", "Administrator"); err != nil {
t.Fatal(err)
}
}
collector := metrics.New()
hub := watch.New(ctx, runtimeStore)
api := New(repository, runtimeStore, hub, authorizer, collector, slog.Default())
server := grpc.NewServer(grpc.UnaryInterceptor(api.UnaryInterceptor), grpc.StreamInterceptor(api.StreamInterceptor))
api.Register(server)
listener := bufconn.Listen(1 << 20)
go func() { _ = server.Serve(listener) }()
t.Cleanup(server.Stop)
t.Cleanup(func() { _ = listener.Close() })
connection, err := grpc.NewClient(
"passthrough:///bufnet",
grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { return listener.Dial() }),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = connection.Close() })
return grpcTestServer{
config: configcenterv1.NewConfigServiceClient(connection),
admin: configcenterv1.NewAdminServiceClient(connection),
runtime: runtimeStore,
repository: repository,
authorizer: authorizer,
}
}
func putRuntime(t *testing.T, runtimeStore *memoryruntime.Store, version int, items map[string]string) {
t.Helper()
payload, err := json.Marshal(items)
if err != nil {
t.Fatal(err)
}
if _, err := runtimeStore.Put(context.Background(), "/config/dev/demo-service/application", payload, domain.Release{ID: int64(version), Version: version, Operator: "test", Time: time.Now()}); err != nil {
t.Fatal(err)
}
}
func itemValue(items []*configcenterv1.ConfigItem, key string) string {
for _, item := range items {
if item.GetKey() == key {
return item.GetValue()
}
}
return ""
}
func bearerContext(token string) context.Context {
return metadata.NewOutgoingContext(context.Background(), metadata.Pairs("authorization", "Bearer "+token))
}