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") }