internal/users/storetest/storetest.go
Rama main · réplica de lectura del repositorio original.
// Package storetest contains the acceptance contract shared by user stores.
package storetest
import (
"context"
"errors"
"fmt"
"sync"
"testing"
"time"
"github.com/readeem/hostebin/internal/users"
)
// Run exercises the observable users.Store contract. Backend packages should
// call it from a TestStoreConformance test with a fresh store factory.
func Run(t *testing.T, newStore func(*testing.T) users.Store) {
t.Helper()
ctx := context.Background()
t.Run("sentinel errors and composite operations", func(t *testing.T) {
store := newStore(t)
defer store.Close()
if _, err := store.GetUser(ctx, "missing"); !errors.Is(err, users.ErrUserNotFound) {
t.Fatalf("GetUser missing = %v", err)
}
if _, err := store.GetTokenForUser(ctx, "missing"); !errors.Is(err, users.ErrUserNotFound) {
t.Fatalf("GetTokenForUser missing = %v", err)
}
now := time.Now().UTC()
user := users.User{ID: "u_one", Name: "one", Role: users.RoleAdmin, CreatedAt: now}
token := users.Token{ID: "t_one", UserID: user.ID, Label: "initial", Digest: users.HashToken("secret"), CreatedAt: now}
if err := store.CreateUserWithToken(ctx, user, token); err != nil {
t.Fatal(err)
}
clash := users.User{ID: "u_two", Name: "ONE", Role: users.RoleUser, CreatedAt: now}
clashToken := users.Token{ID: "t_clash", UserID: clash.ID, Label: "clash", Digest: users.HashToken("clash"), CreatedAt: now}
if err := store.CreateUserWithToken(ctx, clash, clashToken); !errors.Is(err, users.ErrDuplicateName) {
t.Fatalf("duplicate name = %v", err)
}
second := users.User{ID: "u_two", Name: "two", Role: users.RoleUser, CreatedAt: now}
duplicate := users.Token{ID: "t_two", UserID: second.ID, Label: "duplicate", Digest: token.Digest, CreatedAt: now}
if err := store.CreateUserWithToken(ctx, second, duplicate); !errors.Is(err, users.ErrDuplicateToken) {
t.Fatalf("composite injected failure = %v", err)
}
if _, err := store.GetUser(ctx, second.ID); !errors.Is(err, users.ErrUserNotFound) {
t.Fatalf("partially-created user after composite failure = %v", err)
}
if _, _, err := store.LookupToken(ctx, token.Digest); err != nil {
t.Fatal(err)
}
another := users.Token{ID: "t_another", UserID: user.ID, Label: "another", Digest: users.HashToken("another"), CreatedAt: now}
if err := store.SetToken(ctx, another); err != nil {
t.Fatalf("set replacement token = %v", err)
}
if _, _, err := store.LookupToken(ctx, token.Digest); !errors.Is(err, users.ErrTokenNotFound) {
t.Fatalf("replaced token still resolves = %v", err)
}
token = another
if err := store.DeleteTokenForUser(ctx, user.ID); err != nil {
t.Fatal(err)
}
if _, _, err := store.LookupToken(ctx, token.Digest); !errors.Is(err, users.ErrTokenNotFound) {
t.Fatalf("lookup immediately after revoke = %v", err)
}
if err := store.DeleteUserWithToken(ctx, user.ID); err != nil {
t.Fatal(err)
}
if _, err := store.GetUser(ctx, user.ID); !errors.Is(err, users.ErrUserNotFound) {
t.Fatalf("user after composite delete = %v", err)
}
})
t.Run("concurrent access", func(t *testing.T) {
store := newStore(t)
defer store.Close()
now := time.Now().UTC()
user := users.User{ID: "u_concurrent", Name: "concurrent", Role: users.RoleUser, CreatedAt: now}
seed := users.Token{ID: "t_seed", UserID: user.ID, Label: "seed", Digest: users.HashToken("seed"), CreatedAt: now}
if err := store.CreateUserWithToken(ctx, user, seed); err != nil {
t.Fatal(err)
}
var wg sync.WaitGroup
for i := range 16 {
wg.Go(func() {
id := fmt.Sprintf("t_%d", i)
digest := users.HashToken(id)
_ = store.SetToken(ctx, users.Token{ID: id, UserID: user.ID, Label: id, Digest: digest, CreatedAt: now})
_, _, _ = store.LookupToken(ctx, digest)
_, _ = store.GetTokenForUser(ctx, user.ID)
})
}
wg.Wait()
token, err := store.GetTokenForUser(ctx, user.ID)
if err != nil || token.UserID != user.ID {
t.Fatalf("GetTokenForUser = %#v, %v", token, err)
}
})
}