Files
gochat/internal/repository/access_token_repo_test.go
T
2026-06-04 15:44:48 +08:00

237 lines
9.1 KiB
Go

package repository
import (
"context"
"crypto/sha256"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/gochat/gochat/internal/model"
)
// hashToken returns the SHA-256 hex digest of a plaintext token string.
func hashToken(plain string) string {
h := sha256.Sum256([]byte(plain))
return fmt.Sprintf("%x", h)
}
// tokenPrefix extracts the first 8 characters of the plaintext token for prefix lookup.
func tokenPrefix(plain string) string {
if len(plain) < 8 {
return plain
}
return plain[:8]
}
// helper: build a minimal valid AccessToken for testing
func newTestAccessToken(ownerType string, ownerID uint, plainToken string, name string) *model.AccessToken {
return &model.AccessToken{
OwnerType: ownerType,
OwnerID: ownerID,
Token: hashToken(plainToken),
TokenPrefix: tokenPrefix(plainToken),
Name: name,
}
}
// ========== Create + GetByID roundtrip ==========
func TestAccessTokenRepo_CreateAndGetByID(t *testing.T) {
db := setupTestDB(t, &model.AccessToken{}, &model.PlatformApp{}, &model.User{})
repo := NewAccessTokenRepo(db)
// Create prerequisite PlatformApp for FK
account := &model.Account{Name: "TestAccount"}
require.NoError(t, db.Create(account).Error)
platformApp := &model.PlatformApp{Name: "TestApp", Type: "api", Status: "active"}
require.NoError(t, db.Create(platformApp).Error)
token := newTestAccessToken(model.AccessTokenOwnerTypePlatformApp, platformApp.ID, "secret_plain_token_1", "TestToken")
err := repo.Create(context.Background(), token)
require.NoError(t, err)
assert.NotZero(t, token.ID, "ID should be set after Create")
// GetByID roundtrip
found, err := repo.GetByID(context.Background(), token.ID)
require.NoError(t, err)
assert.Equal(t, token.ID, found.ID)
assert.Equal(t, hashToken("secret_plain_token_1"), found.Token)
assert.Equal(t, tokenPrefix("secret_plain_token_1"), found.TokenPrefix)
assert.Equal(t, model.AccessTokenOwnerTypePlatformApp, found.OwnerType)
assert.Equal(t, platformApp.ID, found.OwnerID)
}
// ========== FindByTokenPrefix lookup ==========
func TestAccessTokenRepo_FindByTokenPrefix(t *testing.T) {
db := setupTestDB(t, &model.AccessToken{}, &model.PlatformApp{}, &model.User{})
repo := NewAccessTokenRepo(db)
account := &model.Account{Name: "TestAccount"}
require.NoError(t, db.Create(account).Error)
platformApp := &model.PlatformApp{Name: "TestApp", Type: "api", Status: "active"}
require.NoError(t, db.Create(platformApp).Error)
plain := "abcdef1234567890xyz"
token := newTestAccessToken(model.AccessTokenOwnerTypePlatformApp, platformApp.ID, plain, "PrefixLookup")
err := repo.Create(context.Background(), token)
require.NoError(t, err)
// Find by prefix + owner type
found, err := repo.FindByTokenPrefix(context.Background(), tokenPrefix(plain), model.AccessTokenOwnerTypePlatformApp)
require.NoError(t, err)
assert.Equal(t, token.ID, found.ID)
assert.Equal(t, hashToken(plain), found.Token)
}
// ========== FindByOwner for PlatformApp owner ==========
func TestAccessTokenRepo_FindByOwner_PlatformApp(t *testing.T) {
db := setupTestDB(t, &model.AccessToken{}, &model.PlatformApp{}, &model.User{})
repo := NewAccessTokenRepo(db)
account := &model.Account{Name: "TestAccount"}
require.NoError(t, db.Create(account).Error)
platformApp := &model.PlatformApp{Name: "TestApp", Type: "api", Status: "active"}
require.NoError(t, db.Create(platformApp).Error)
t1 := newTestAccessToken(model.AccessTokenOwnerTypePlatformApp, platformApp.ID, "token_p1", "Token1")
t2 := newTestAccessToken(model.AccessTokenOwnerTypePlatformApp, platformApp.ID, "token_p2", "Token2")
require.NoError(t, repo.Create(context.Background(), t1))
require.NoError(t, repo.Create(context.Background(), t2))
tokens, err := repo.FindByOwner(context.Background(), model.AccessTokenOwnerTypePlatformApp, platformApp.ID)
require.NoError(t, err)
assert.Len(t, tokens, 2)
}
// ========== FindByOwner for User owner ==========
func TestAccessTokenRepo_FindByOwner_User(t *testing.T) {
db := setupTestDB(t, &model.AccessToken{}, &model.PlatformApp{}, &model.User{})
repo := NewAccessTokenRepo(db)
account := &model.Account{Name: "TestAccount"}
require.NoError(t, db.Create(account).Error)
user := &model.User{Name: "TestUser", Email: "user@test.com"}
require.NoError(t, db.Create(user).Error)
t1 := newTestAccessToken(model.AccessTokenOwnerTypeUser, user.ID, "token_u1", "UserToken1")
t2 := newTestAccessToken(model.AccessTokenOwnerTypeUser, user.ID, "token_u2", "UserToken2")
require.NoError(t, repo.Create(context.Background(), t1))
require.NoError(t, repo.Create(context.Background(), t2))
tokens, err := repo.FindByOwner(context.Background(), model.AccessTokenOwnerTypeUser, user.ID)
require.NoError(t, err)
assert.Len(t, tokens, 2)
// Different owner should not see these tokens
tokens2, err := repo.FindByOwner(context.Background(), model.AccessTokenOwnerTypePlatformApp, user.ID)
require.NoError(t, err)
assert.Len(t, tokens2, 0)
}
// ========== UpdateLastUsedAt ==========
func TestAccessTokenRepo_UpdateLastUsedAt(t *testing.T) {
db := setupTestDB(t, &model.AccessToken{}, &model.PlatformApp{}, &model.User{})
repo := NewAccessTokenRepo(db)
account := &model.Account{Name: "TestAccount"}
require.NoError(t, db.Create(account).Error)
platformApp := &model.PlatformApp{Name: "TestApp", Type: "api", Status: "active"}
require.NoError(t, db.Create(platformApp).Error)
token := newTestAccessToken(model.AccessTokenOwnerTypePlatformApp, platformApp.ID, "token_lu", "LastUsed")
require.NoError(t, repo.Create(context.Background(), token))
// Initially LastUsedAt should be nil
found, err := repo.GetByID(context.Background(), token.ID)
require.NoError(t, err)
assert.Nil(t, found.LastUsedAt)
// Update LastUsedAt
err = repo.UpdateLastUsedAt(context.Background(), token.ID)
require.NoError(t, err)
// Verify it was set
found2, err := repo.GetByID(context.Background(), token.ID)
require.NoError(t, err)
assert.NotNil(t, found2.LastUsedAt)
assert.True(t, found2.LastUsedAt.After(time.Now().Add(-5*time.Second)))
}
// ========== Soft delete + FindByTokenPrefix should not find deleted tokens ==========
func TestAccessTokenRepo_SoftDelete_NotFoundByPrefix(t *testing.T) {
db := setupTestDB(t, &model.AccessToken{}, &model.PlatformApp{}, &model.User{})
repo := NewAccessTokenRepo(db)
account := &model.Account{Name: "TestAccount"}
require.NoError(t, db.Create(account).Error)
platformApp := &model.PlatformApp{Name: "TestApp", Type: "api", Status: "active"}
require.NoError(t, db.Create(platformApp).Error)
plain := "deletable_token_abc"
token := newTestAccessToken(model.AccessTokenOwnerTypePlatformApp, platformApp.ID, plain, "DeleteMe")
require.NoError(t, repo.Create(context.Background(), token))
// Verify prefix lookup works before deletion
found, err := repo.FindByTokenPrefix(context.Background(), tokenPrefix(plain), model.AccessTokenOwnerTypePlatformApp)
require.NoError(t, err)
assert.Equal(t, token.ID, found.ID)
// Soft delete
err = repo.Delete(context.Background(), token.ID)
require.NoError(t, err)
// FindByTokenPrefix should NOT find the deleted token
_, err = repo.FindByTokenPrefix(context.Background(), tokenPrefix(plain), model.AccessTokenOwnerTypePlatformApp)
assert.Error(t, err, "FindByTokenPrefix should not find soft-deleted tokens")
}
// ========== FindActiveByOwner ==========
func TestAccessTokenRepo_FindActiveByOwner(t *testing.T) {
db := setupTestDB(t, &model.AccessToken{}, &model.PlatformApp{}, &model.User{})
repo := NewAccessTokenRepo(db)
account := &model.Account{Name: "TestAccount"}
require.NoError(t, db.Create(account).Error)
platformApp := &model.PlatformApp{Name: "TestApp", Type: "api", Status: "active"}
require.NoError(t, db.Create(platformApp).Error)
// Token with no expiry (never expires)
t1 := newTestAccessToken(model.AccessTokenOwnerTypePlatformApp, platformApp.ID, "active_token_no_expiry", "NoExpiry")
require.NoError(t, repo.Create(context.Background(), t1))
// Token with future expiry (still active)
futureExpiry := time.Now().Add(24 * time.Hour)
t2 := newTestAccessToken(model.AccessTokenOwnerTypePlatformApp, platformApp.ID, "active_token_future_expiry", "FutureExpiry")
t2.ExpiresAt = &futureExpiry
require.NoError(t, repo.Create(context.Background(), t2))
// Token with past expiry (expired)
pastExpiry := time.Now().Add(-24 * time.Hour)
t3 := newTestAccessToken(model.AccessTokenOwnerTypePlatformApp, platformApp.ID, "expired_token", "Expired")
t3.ExpiresAt = &pastExpiry
require.NoError(t, repo.Create(context.Background(), t3))
// FindActiveByOwner should return only non-expired tokens
tokens, err := repo.FindActiveByOwner(context.Background(), model.AccessTokenOwnerTypePlatformApp, platformApp.ID)
require.NoError(t, err)
assert.Len(t, tokens, 2, "should only return non-expired tokens")
// Verify both active tokens are present (check IDs)
ids := make(map[uint]bool)
for _, tk := range tokens {
ids[tk.ID] = true
}
assert.True(t, ids[t1.ID], "no-expiry token should be active")
assert.True(t, ids[t2.ID], "future-expiry token should be active")
assert.False(t, ids[t3.ID], "past-expiry token should NOT be active")
}