//go:build integration // +build integration package refreshtokens_test import ( "context" "errors" "testing" "time" "git.sendico.io/tech/sxchnge/bff/pkg/merrors" "git.sendico.io/tech/sxchnge/bff/pkg/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/v2/bson" ) func TestRefreshTokenDB_ClientManagement(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() t.Run("Client_CRUD_Operations", func(t *testing.T) { // Note: Client management is handled by a separate client database. // This test verifies that refresh tokens work with different client IDs. userID := bson.NewObjectID() webToken := createTestRefreshToken(userID, "web-app", "device1", "token1") err := db.Create(ctx, webToken) require.NoError(t, err) mobileToken := createTestRefreshToken(userID, "mobile-app", "device2", "token2") err = db.Create(ctx, mobileToken) require.NoError(t, err) webCRT := &model.ClientRefreshToken{ SessionIdentifier: model.SessionIdentifier{ ClientID: "web-app", DeviceID: "device1", }, RefreshToken: "token1", } retrievedToken, err := db.GetByCRT(ctx, webCRT) require.NoError(t, err) assert.Equal(t, "web-app", retrievedToken.ClientID) assert.Equal(t, "device1", retrievedToken.DeviceID) mobileCRT := &model.ClientRefreshToken{ SessionIdentifier: model.SessionIdentifier{ ClientID: "mobile-app", DeviceID: "device2", }, RefreshToken: "token2", } retrievedToken, err = db.GetByCRT(ctx, mobileCRT) require.NoError(t, err) assert.Equal(t, "mobile-app", retrievedToken.ClientID) assert.Equal(t, "device2", retrievedToken.DeviceID) }) } func TestRefreshTokenDB_SecurityScenarios(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() t.Run("Token_Hijacking_Prevention", func(t *testing.T) { userID := bson.NewObjectID() clientID := "web-app" deviceID := "user-browser" token := "hijacked_token_123" refreshToken := createTestRefreshToken(userID, clientID, deviceID, token) err := db.Create(ctx, refreshToken) require.NoError(t, err) session := &model.SessionIdentifier{ ClientID: clientID, DeviceID: deviceID, } err = db.Revoke(ctx, userID, session) require.NoError(t, err) crt := &model.ClientRefreshToken{ SessionIdentifier: model.SessionIdentifier{ ClientID: clientID, DeviceID: deviceID, }, RefreshToken: token, } _, err = db.GetByCRT(ctx, crt) assert.Error(t, err) assert.True(t, errors.Is(err, merrors.ErrNoData)) }) t.Run("Invalid_Token_Attempts", func(t *testing.T) { crt := &model.ClientRefreshToken{ SessionIdentifier: model.SessionIdentifier{ ClientID: "invalid-client", DeviceID: "invalid-device", }, RefreshToken: "invalid_token_123", } _, err := db.GetByCRT(ctx, crt) assert.Error(t, err) assert.True(t, errors.Is(err, merrors.ErrNoData)) }) } func TestRefreshTokenDB_ExpiredTokenHandling(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() t.Run("Expired_Token_Cleanup", func(t *testing.T) { userID := bson.NewObjectID() clientID := "web-app" deviceID := "user-device" token := "expired_token_123" refreshToken := createTestRefreshToken(userID, clientID, deviceID, token) refreshToken.ExpiresAt = time.Now().Add(-1 * time.Hour) err := db.Create(ctx, refreshToken) require.NoError(t, err) var storedToken model.RefreshToken err = db.Get(ctx, *refreshToken.GetID(), &storedToken) require.NoError(t, err) assert.True(t, storedToken.ExpiresAt.Before(time.Now())) crt := &model.ClientRefreshToken{ SessionIdentifier: model.SessionIdentifier{ ClientID: clientID, DeviceID: deviceID, }, RefreshToken: token, } _, err = db.GetByCRT(ctx, crt) assert.Error(t, err) assert.True(t, errors.Is(err, merrors.ErrAccessDenied)) }) } func TestRefreshTokenDB_ConcurrentAccess(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() t.Run("Concurrent_Token_Usage", func(t *testing.T) { userID := bson.NewObjectID() clientID := "web-app" deviceID := "user-device" token := "concurrent_token_123" refreshToken := createTestRefreshToken(userID, clientID, deviceID, token) err := db.Create(ctx, refreshToken) require.NoError(t, err) crt := &model.ClientRefreshToken{ SessionIdentifier: model.SessionIdentifier{ ClientID: clientID, DeviceID: deviceID, }, RefreshToken: token, } done := make(chan error, 2) go func() { _, err := db.GetByCRT(ctx, crt) done <- err }() go func() { _, err := db.GetByCRT(ctx, crt) done <- err }() for i := 0; i < 2; i++ { err := <-done require.NoError(t, err) } }) } func TestRefreshTokenDB_EdgeCases(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() t.Run("Delete_Token_By_ID", func(t *testing.T) { userID := bson.NewObjectID() refreshToken := createTestRefreshToken(userID, "web-app", "device-1", "token_123") err := db.Create(ctx, refreshToken) require.NoError(t, err) tokenID := *refreshToken.GetID() err = db.Delete(ctx, tokenID) require.NoError(t, err) var result model.RefreshToken err = db.Get(ctx, tokenID, &result) assert.Error(t, err) assert.True(t, errors.Is(err, merrors.ErrNoData)) }) t.Run("Revoke_Non_Existent_Token", func(t *testing.T) { userID := bson.NewObjectID() session := &model.SessionIdentifier{ ClientID: "non-existent-client", DeviceID: "non-existent-device", } err := db.Revoke(ctx, userID, session) assert.NoError(t, err) }) t.Run("RevokeAll_No_Other_Devices", func(t *testing.T) { userID := bson.NewObjectID() clientID := "web-app" deviceID := "only-device" refreshToken := createTestRefreshToken(userID, clientID, deviceID, "token_123") err := db.Create(ctx, refreshToken) require.NoError(t, err) err = db.RevokeAll(ctx, userID, deviceID) require.NoError(t, err) crt := &model.ClientRefreshToken{ SessionIdentifier: model.SessionIdentifier{ ClientID: clientID, DeviceID: deviceID, }, RefreshToken: "token_123", } _, err = db.GetByCRT(ctx, crt) require.NoError(t, err) }) }