Files
gochat/backend/internal/auth/refresh_store_test.go
T
Rogeeandrogee 6c78820a1f H-338: close H-335 release blockers (#59)
* H-16: align takeover with channel AI workflow (#2)

* feat(conversations): complete manual AI takeover

* fix(conversations): align AI takeover flow with channel AI

* fix(conversations): close takeover review gaps

---------

Co-authored-by: Rogee <rogee@ipao.vip>

* feat(shangwutong): sync customer names back to channel (#3)

Co-authored-by: Rogee <rogee@ipao.vip>

* fix(shangwutong): close contact sync review gaps (#4)

Co-authored-by: Rogee <rogee@ipao.vip>

* H-28: harden Shangwutong CID sync (#5)

* fix(shangwutong): close contact sync review gaps

* fix(shangwutong): harden CID sync boundaries

---------

Co-authored-by: Rogee <rogee@ipao.vip>

* fix(conversations): sync AI takeover exit in realtime (#6)

Co-authored-by: Rogee <rogee@ipao.vip>

* test(shangwutong): cover CID rename reliability (#7)

Co-authored-by: Rogee <rogee@ipao.vip>

* H-43: fix WEB Captain takeover E2E flow (#8)

* test(shangwutong): cover CID rename reliability

* H-43: fix WEB Captain takeover flow

* H-48: preserve compatible provider model

* H-49: make Captain takeover atomic

* H-50: prevent duplicate widget initialization

---------

Co-authored-by: Rogee <rogee@ipao.vip>

* H-55: make Captain bindings atomic (#9)

Co-authored-by: Rogee <rogee@ipao.vip>

* H-60: harden Captain migration rollback and concurrency

* chore(agent): baseline — uncommitted work from the local directory

* H-335: add safe Captain skills and user deactivation

* H-338: close auth and Captain review blockers

* H-338: close assignment and session races

* H-338: close assignment and websocket invalidation gaps

* H-338: enforce assignment write invariants

---------

Co-authored-by: Rogee <rogee@ipao.vip>
2026-08-20 10:21:19 +08:00

98 lines
3.4 KiB
Go

package auth
import (
"context"
"fmt"
"sync"
"testing"
"github.com/alicebob/miniredis/v2"
"github.com/gochat/gochat/internal/config"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/require"
)
func TestRefreshTokenStoreScopesTokensByClient(t *testing.T) {
store := NewRefreshTokenStore(nil, &config.JWTConfig{RefreshExpiryHours: 24})
ctx := context.Background()
require.NoError(t, store.StoreForClient(ctx, 7, "chrome", "chrome-token"))
require.NoError(t, store.StoreForClient(ctx, 7, "firefox", "firefox-token"))
valid, err := store.ValidateForClient(ctx, 7, "chrome", "chrome-token")
require.NoError(t, err)
require.True(t, valid)
require.NoError(t, store.RevokeClient(ctx, 7, "chrome"))
valid, err = store.ValidateForClient(ctx, 7, "chrome", "chrome-token")
require.NoError(t, err)
require.False(t, valid)
valid, err = store.ValidateForClient(ctx, 7, "firefox", "firefox-token")
require.NoError(t, err)
require.True(t, valid)
}
func TestRefreshTokenStoreRevokeUserRedisDoesNotMatchSimilarUserIDs(t *testing.T) {
mr := miniredis.RunT(t)
store := NewRefreshTokenStore(redis.NewClient(&redis.Options{Addr: mr.Addr()}), &config.JWTConfig{RefreshExpiryHours: 24})
ctx := context.Background()
require.NoError(t, store.StoreForClient(ctx, 7, "browser", "user-7"))
require.NoError(t, store.StoreForClient(ctx, 70, "browser", "user-70"))
require.NoError(t, store.RevokeUser(ctx, 7))
valid, err := store.ValidateForClient(ctx, 70, "browser", "user-70")
require.NoError(t, err)
require.True(t, valid)
}
func TestRefreshTokenStoreRevokeUserRemovesAllClients(t *testing.T) {
store := NewRefreshTokenStore(nil, &config.JWTConfig{RefreshExpiryHours: 24})
ctx := context.Background()
require.NoError(t, store.Store(ctx, 7, "legacy"))
require.NoError(t, store.StoreForClient(ctx, 7, "chrome", "chrome-token"))
require.NoError(t, store.StoreForClient(ctx, 7, "mobile", "mobile-token"))
require.NoError(t, store.StoreForClient(ctx, 8, "chrome", "other-user"))
require.NoError(t, store.RevokeUser(ctx, 7))
for clientID, token := range map[string]string{"": "legacy", "chrome": "chrome-token", "mobile": "mobile-token"} {
valid, err := store.ValidateForClient(ctx, 7, clientID, token)
require.NoError(t, err)
require.False(t, valid)
}
valid, err := store.ValidateForClient(ctx, 8, "chrome", "other-user")
require.NoError(t, err)
require.True(t, valid)
}
func TestRefreshTokenStoreConcurrentDoubleRefreshAllowsOneWinner(t *testing.T) {
for _, backend := range []string{"memory", "redis"} {
t.Run(backend, func(t *testing.T) {
var rdb *redis.Client
if backend == "redis" {
mr := miniredis.RunT(t)
rdb = redis.NewClient(&redis.Options{Addr: mr.Addr()})
t.Cleanup(func() { require.NoError(t, rdb.Close()) })
}
store := NewRefreshTokenStore(rdb, &config.JWTConfig{RefreshExpiryHours: 24})
ctx := context.Background()
require.NoError(t, store.StoreForClient(ctx, 7, "browser", "old-token"))
start := make(chan struct{})
results := make([]bool, 2)
errs := make([]error, 2)
var wg sync.WaitGroup
for i := range results {
wg.Add(1)
go func(i int) {
defer wg.Done()
<-start
results[i], errs[i] = store.CompareAndSwapForClient(ctx, 7, "browser", "old-token", fmt.Sprintf("new-token-%d", i))
}(i)
}
close(start)
wg.Wait()
require.NoError(t, errs[0])
require.NoError(t, errs[1])
require.NotEqual(t, results[0], results[1])
})
}
}