Files
dingtalk-workspace-cli/internal/event/runtimecred/broker_test.go

204 lines
5.9 KiB
Go

// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package runtimecred
import (
"context"
"errors"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
)
func TestCrossPlatformCoverageBrokerLocalFallbackAndRuntimePrecedence(t *testing.T) {
var localCalls atomic.Int32
b := New(Config{LocalResolve: func(context.Context) (string, error) {
localCalls.Add(1)
return "local", nil
}})
if got, err := b.Resolve(context.Background()); err != nil || got != "local" {
t.Fatalf("local Resolve = %q, %v", got, err)
}
gen, err := b.Update(0, " runtime ")
if err != nil || gen != 1 {
t.Fatalf("Update = %d, %v", gen, err)
}
if got, err := b.Resolve(context.Background()); err != nil || got != "runtime" {
t.Fatalf("runtime Resolve = %q, %v", got, err)
}
if localCalls.Load() != 1 {
t.Fatalf("local resolver calls = %d", localCalls.Load())
}
}
func TestCrossPlatformCoverageBrokerRequireSeedWaitsAndCancels(t *testing.T) {
b := New(Config{RequireSeed: true})
ctx, cancel := context.WithCancel(context.Background())
done := make(chan error, 1)
go func() { _, err := b.Resolve(ctx); done <- err }()
cancel()
if err := <-done; !errors.Is(err, context.Canceled) {
t.Fatalf("Resolve error = %v", err)
}
resolved := make(chan string, 1)
go func() {
token, _ := b.Resolve(context.Background())
resolved <- token
}()
select {
case <-resolved:
t.Fatal("Resolve returned before seed")
case <-time.After(20 * time.Millisecond):
}
if _, err := b.Update(0, "seed"); err != nil {
t.Fatal(err)
}
if got := <-resolved; got != "seed" {
t.Fatalf("Resolve = %q", got)
}
}
func TestCrossPlatformCoverageBrokerUpdateCASIdempotenceAndConcurrentConflict(t *testing.T) {
b := New(Config{})
if gen, err := b.Update(0, "same"); err != nil || gen != 1 {
t.Fatalf("first update = %d, %v", gen, err)
}
if gen, err := b.Update(0, "same"); err != nil || gen != 1 {
t.Fatalf("idempotent stale update = %d, %v", gen, err)
}
if _, err := b.Update(0, "different"); err == nil {
t.Fatal("expected generation conflict")
} else {
var conflict *GenerationConflictError
if !errors.As(err, &conflict) || conflict.Actual != 1 {
t.Fatalf("conflict = %#v, %v", conflict, err)
}
}
b = New(Config{})
start := make(chan struct{})
var successes atomic.Int32
var conflicts atomic.Int32
var wg sync.WaitGroup
for _, token := range []string{"a", "b"} {
wg.Add(1)
go func(token string) {
defer wg.Done()
<-start
_, err := b.Update(0, token)
if err == nil {
successes.Add(1)
return
}
var conflict *GenerationConflictError
if errors.As(err, &conflict) {
conflicts.Add(1)
}
}(token)
}
close(start)
wg.Wait()
if successes.Load() != 1 || conflicts.Load() != 1 {
t.Fatalf("successes=%d conflicts=%d", successes.Load(), conflicts.Load())
}
}
func TestCrossPlatformCoverageBrokerRefreshRejectedRuntimeNeverFallsBack(t *testing.T) {
var refreshCalls atomic.Int32
b := New(Config{LocalRefresh: func(context.Context, string) (string, error) {
refreshCalls.Add(1)
return "local-new", nil
}})
if _, err := b.Update(0, "runtime-a"); err != nil {
t.Fatal(err)
}
if _, err := b.RefreshRejected(context.Background(), "runtime-a"); !errors.Is(err, ErrRuntimeTokenRejected) {
t.Fatalf("same token refresh error = %v", err)
}
if refreshCalls.Load() != 0 {
t.Fatalf("local refresh called %d times", refreshCalls.Load())
}
if _, err := b.Update(1, "runtime-b"); err != nil {
t.Fatal(err)
}
if got, err := b.RefreshRejected(context.Background(), "runtime-a"); err != nil || got != "runtime-b" {
t.Fatalf("rotated refresh = %q, %v", got, err)
}
if superseded, err := b.ClassifyRejectedAfterRetry("runtime-b"); superseded || !errors.Is(err, ErrRuntimeTokenRejected) {
t.Fatalf("current retry rejection = superseded %v, error %v", superseded, err)
}
if _, err := b.Update(2, "runtime-c"); err != nil {
t.Fatal(err)
}
if superseded, err := b.ClassifyRejectedAfterRetry("runtime-b"); !superseded || err != nil {
t.Fatalf("newer generation classification = superseded %v, error %v", superseded, err)
}
localOnly := New(Config{})
if superseded, err := localOnly.ClassifyRejectedAfterRetry("local"); superseded || err != nil {
t.Fatalf("local-only classification = superseded %v, error %v", superseded, err)
}
}
func TestCrossPlatformCoverageBrokerDefersSeedUntilActivation(t *testing.T) {
b := New(Config{RequireSeed: true, RequireActivation: true})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
resolved := make(chan string, 1)
errs := make(chan error, 1)
go func() {
token, err := b.Resolve(ctx)
if err != nil {
errs <- err
return
}
resolved <- token
}()
generation, err := b.Update(0, "runtime-a")
if err != nil || generation != 1 {
t.Fatalf("Update() = generation %d, err %v", generation, err)
}
select {
case token := <-resolved:
t.Fatalf("Resolve() returned before activation: %q", token)
case err := <-errs:
t.Fatalf("Resolve() failed before activation: %v", err)
case <-time.After(20 * time.Millisecond):
}
if _, err := b.Activate(0); err == nil {
t.Fatal("Activate() with stale generation unexpectedly succeeded")
}
if activeGeneration, err := b.Activate(generation); err != nil || activeGeneration != generation {
t.Fatalf("Activate() = generation %d, err %v", activeGeneration, err)
}
select {
case token := <-resolved:
if token != "runtime-a" {
t.Fatalf("Resolve() token = %q", token)
}
case err := <-errs:
t.Fatalf("Resolve() failed after activation: %v", err)
case <-time.After(time.Second):
t.Fatal("Resolve() remained blocked after activation")
}
}
func TestCrossPlatformCoverageBrokerRejectsInvalidTokensWithoutEcho(t *testing.T) {
b := New(Config{MaxTokenBytes: 4})
for _, token := range []string{" ", "secret-token"} {
_, err := b.Update(0, token)
if err == nil {
t.Fatalf("Update(%q) succeeded", token)
}
if strings.Contains(err.Error(), token) {
t.Fatal("validation error contained rejected token")
}
}
}