279 lines
9.7 KiB
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))
|
|
}
|