Files

229 lines
7.1 KiB
Go

// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package msgcrypto
import (
"context"
"errors"
"sync"
"testing"
"time"
)
// countingProvider hands out a fresh code per call and records how often it was
// asked, so cache behaviour can be asserted.
type countingProvider struct {
mu sync.Mutex
calls int
code string
err error
}
func (p *countingProvider) AuthCode(context.Context) (string, error) {
p.mu.Lock()
defer p.mu.Unlock()
p.calls++
if p.err != nil {
return "", p.err
}
if p.code != "" {
return p.code, nil
}
return "code-" + string(rune('a'+p.calls-1)), nil
}
// callCount reports the number of upstream fetches.
func (p *countingProvider) callCount() int {
p.mu.Lock()
defer p.mu.Unlock()
return p.calls
}
func TestCrossPlatformCoverageAuthCodeFuncAdaptsFunction(t *testing.T) {
provider := AuthCodeFunc(func(context.Context) (string, error) { return "abc", nil })
code, err := provider.AuthCode(context.Background())
if err != nil || code != "abc" {
t.Fatalf("AuthCode() = %q, %v; want abc, nil", code, err)
}
}
func TestCrossPlatformCoverageStaticAuthCodeReturnsCode(t *testing.T) {
code, err := StaticAuthCode("fixed").AuthCode(context.Background())
if err != nil || code != "fixed" {
t.Fatalf("AuthCode() = %q, %v; want fixed, nil", code, err)
}
}
func TestCrossPlatformCoverageStaticAuthCodeRejectsEmptyCode(t *testing.T) {
_, err := StaticAuthCode("").AuthCode(context.Background())
if !errors.Is(err, ErrNoAuthCode) {
t.Fatalf("AuthCode() = %v, want ErrNoAuthCode", err)
}
}
func TestCrossPlatformCoverageCachedAuthCodeReusesCodeWithinTTL(t *testing.T) {
provider := &countingProvider{code: "same"}
cache := NewCachedAuthCode(provider, time.Minute)
for i := 0; i < 5; i++ {
code, err := cache.AuthCode(context.Background())
if err != nil {
t.Fatalf("AuthCode() #%d = %v", i+1, err)
}
if code != "same" {
t.Fatalf("AuthCode() #%d = %q, want same", i+1, code)
}
}
if got := provider.callCount(); got != 1 {
t.Fatalf("upstream called %d times, want 1 (the code must be cached)", got)
}
}
func TestCrossPlatformCoverageCachedAuthCodeRefetchesAfterTTL(t *testing.T) {
provider := &countingProvider{}
cache := NewCachedAuthCode(provider, time.Minute)
now := time.Now()
cache.now = func() time.Time { return now }
first, err := cache.AuthCode(context.Background())
if err != nil {
t.Fatalf("first AuthCode() = %v", err)
}
// Move past the TTL. The DingTalk code expires server-side, so a stale
// one must not be reused.
now = now.Add(time.Minute + time.Second)
second, err := cache.AuthCode(context.Background())
if err != nil {
t.Fatalf("second AuthCode() = %v", err)
}
if first == second {
t.Fatalf("AuthCode() returned the same code %q after the TTL expired", first)
}
if got := provider.callCount(); got != 2 {
t.Fatalf("upstream called %d times, want 2", got)
}
}
func TestCrossPlatformCoverageCachedAuthCodeDefaultTTLIsUnderServerWindow(t *testing.T) {
// Portal vendorAuthCode expiresIn is 120s and the code is one-shot.
// The unconsumed-cache window must stay under that server lifetime.
if DefaultAuthCodeTTL >= 120*time.Second {
t.Fatalf("DefaultAuthCodeTTL = %v, want less than the 120s portal expiresIn", DefaultAuthCodeTTL)
}
cache := NewCachedAuthCode(&countingProvider{}, 0)
if cache.ttl != DefaultAuthCodeTTL {
t.Fatalf("ttl = %v, want DefaultAuthCodeTTL %v", cache.ttl, DefaultAuthCodeTTL)
}
}
func TestCrossPlatformCoverageCachedAuthCodeNegativeTTLFallsBackToDefault(t *testing.T) {
cache := NewCachedAuthCode(&countingProvider{}, -time.Second)
if cache.ttl != DefaultAuthCodeTTL {
t.Fatalf("ttl = %v, want DefaultAuthCodeTTL %v", cache.ttl, DefaultAuthCodeTTL)
}
}
func TestCrossPlatformCoverageCachedAuthCodePropagatesUpstreamError(t *testing.T) {
wantErr := errors.New("token service down")
cache := NewCachedAuthCode(&countingProvider{err: wantErr}, time.Minute)
_, err := cache.AuthCode(context.Background())
if !errors.Is(err, wantErr) {
t.Fatalf("AuthCode() = %v, want it to wrap %v", err, wantErr)
}
}
func TestCrossPlatformCoverageCachedAuthCodeDoesNotCacheFailures(t *testing.T) {
provider := &countingProvider{err: errors.New("transient")}
cache := NewCachedAuthCode(provider, time.Minute)
if _, err := cache.AuthCode(context.Background()); err == nil {
t.Fatal("AuthCode() = nil error, want failure")
}
provider.mu.Lock()
provider.err = nil
provider.code = "recovered"
provider.mu.Unlock()
code, err := cache.AuthCode(context.Background())
if err != nil {
t.Fatalf("AuthCode() after recovery = %v", err)
}
if code != "recovered" {
t.Fatalf("AuthCode() = %q, want recovered (a failure must not be cached)", code)
}
}
func TestCrossPlatformCoverageCachedAuthCodeRejectsEmptyUpstreamCode(t *testing.T) {
// A provider that reports success with no code is a bug upstream; the
// cache must surface it instead of caching an unusable value.
cache := NewCachedAuthCode(AuthCodeFunc(func(context.Context) (string, error) {
return "", nil
}), time.Minute)
if _, err := cache.AuthCode(context.Background()); !errors.Is(err, ErrNoAuthCode) {
t.Fatalf("AuthCode() = %v, want ErrNoAuthCode", err)
}
}
func TestCrossPlatformCoverageCachedAuthCodeInvalidateForcesRefetch(t *testing.T) {
provider := &countingProvider{}
cache := NewCachedAuthCode(provider, time.Hour)
if _, err := cache.AuthCode(context.Background()); err != nil {
t.Fatalf("first AuthCode() = %v", err)
}
cache.Invalidate()
if _, err := cache.AuthCode(context.Background()); err != nil {
t.Fatalf("second AuthCode() = %v", err)
}
if got := provider.callCount(); got != 2 {
t.Fatalf("upstream called %d times, want 2 after Invalidate", got)
}
}
func TestCrossPlatformCoverageCachedAuthCodeWithoutProviderReportsMissingProvider(t *testing.T) {
cache := NewCachedAuthCode(nil, time.Minute)
if _, err := cache.AuthCode(context.Background()); !errors.Is(err, ErrNoAuthCodeProvider) {
t.Fatalf("AuthCode() = %v, want ErrNoAuthCodeProvider", err)
}
}
func TestCrossPlatformCoverageCachedAuthCodeIsSafeForConcurrentUse(t *testing.T) {
// The backend may ask for a code from a CGO callback while another
// operation is in flight, so concurrent access must not race.
provider := &countingProvider{code: "shared"}
cache := NewCachedAuthCode(provider, time.Hour)
var wg sync.WaitGroup
for i := 0; i < 32; i++ {
wg.Add(1)
go func() {
defer wg.Done()
if code, err := cache.AuthCode(context.Background()); err != nil || code != "shared" {
t.Errorf("AuthCode() = %q, %v; want shared, nil", code, err)
}
}()
}
wg.Wait()
if got := provider.callCount(); got != 1 {
t.Fatalf("upstream called %d times, want 1", got)
}
}