//go:build integration // +build integration package refreshtokens_test import ( "context" "crypto/sha1" "errors" "fmt" "testing" "time" "git.sendico.io/tech/sxchnge/bff/edge/shared/storage/refreshtokens" "git.sendico.io/tech/sxchnge/bff/pkg/merrors" factory "git.sendico.io/tech/sxchnge/bff/pkg/mlogger/factory" "git.sendico.io/tech/sxchnge/bff/pkg/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/mongodb" "github.com/testcontainers/testcontainers-go/wait" "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/mongo" "go.mongodb.org/mongo-driver/v2/mongo/options" ) func setupTestDB(t *testing.T) (*refreshtokens.RefreshTokenDB, func()) { db, _, cleanup := setupTestDBWithMongo(t) return db, cleanup } func setupTestDBWithMongo(t *testing.T) (*refreshtokens.RefreshTokenDB, *mongo.Database, func()) { // mark as helper for better test failure reporting t.Helper() startCtx, startCancel := context.WithTimeout(context.Background(), 2*time.Minute) defer startCancel() mongoContainer, err := mongodb.Run(startCtx, "mongo:latest", mongodb.WithUsername("root"), mongodb.WithPassword("password"), testcontainers.WithWaitStrategy(wait.ForListeningPort("27017/tcp").WithStartupTimeout(2*time.Minute)), ) require.NoError(t, err, "failed to start MongoDB container") mongoURI, err := mongoContainer.ConnectionString(startCtx) require.NoError(t, err, "failed to get MongoDB connection string") clientOptions := options.Client().ApplyURI(mongoURI) client, err := mongo.Connect(clientOptions) require.NoError(t, err, "failed to connect to MongoDB") database := client.Database(fmt.Sprintf("test_refresh_tokens_%x", sha1.Sum([]byte(t.Name())))) logger := factory.NewLogger(true) db, err := refreshtokens.Create(logger, database) require.NoError(t, err, "failed to create refresh tokens db") cleanup := func() { _ = database.Drop(context.Background()) _ = client.Disconnect(context.Background()) termCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() _ = mongoContainer.Terminate(termCtx) } return db, database, cleanup } func createTestRefreshToken(accountRef bson.ObjectID, clientID, deviceID, token string) *model.RefreshToken { return &model.RefreshToken{ ClientRefreshToken: model.ClientRefreshToken{ SessionIdentifier: model.SessionIdentifier{ ClientID: clientID, DeviceID: deviceID, }, RefreshToken: token, }, AccountRef: &accountRef, ExpiresAt: time.Now().Add(24 * time.Hour), IsRevoked: false, UserAgent: "TestUserAgent/1.0", IPAddress: "192.168.1.1", LastUsedAt: time.Now(), } } func TestRefreshTokenDB_AuthenticationFlow(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() t.Run("Complete_User_Authentication_Flow", func(t *testing.T) { // Setup: Create user and client userID := bson.NewObjectID() clientID := "web-app" deviceID := "user-desktop-chrome" token := "refresh_token_12345" // Step 1: User logs in - create initial refresh token refreshToken := createTestRefreshToken(userID, clientID, deviceID, token) err := db.Create(ctx, refreshToken) require.NoError(t, err) // Step 2: User uses refresh token to get new access token crt := &model.ClientRefreshToken{ SessionIdentifier: model.SessionIdentifier{ ClientID: clientID, DeviceID: deviceID, }, RefreshToken: token, } retrievedToken, err := db.GetByCRT(ctx, crt) require.NoError(t, err) require.NotNil(t, retrievedToken.AccountRef) assert.Equal(t, userID, *retrievedToken.AccountRef) assert.Equal(t, token, retrievedToken.RefreshToken) assert.False(t, retrievedToken.IsRevoked) // Step 3: User logs out - revoke the token session := &model.SessionIdentifier{ ClientID: clientID, DeviceID: deviceID, } err = db.Revoke(ctx, userID, session) require.NoError(t, err) // Step 4: Try to use revoked token - should fail _, err = db.GetByCRT(ctx, crt) assert.Error(t, err) assert.True(t, errors.Is(err, merrors.ErrNoData)) }) t.Run("Manual_Token_Revocation_Workaround", func(t *testing.T) { // Test manual revocation by directly updating the token userID := bson.NewObjectID() clientID := "web-app" deviceID := "user-desktop-chrome" token := "manual_revoke_token_123" // Step 1: Create token refreshToken := createTestRefreshToken(userID, clientID, deviceID, token) err := db.Create(ctx, refreshToken) require.NoError(t, err) // Step 2: Manually revoke token by updating it directly refreshToken.IsRevoked = true err = db.Update(ctx, refreshToken) require.NoError(t, err) // Step 3: Try to use revoked token - should fail 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)) }) } func TestRefreshTokenDB_MultiDeviceManagement(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() t.Run("User_With_Multiple_Devices", func(t *testing.T) { userID := bson.NewObjectID() clientID := "mobile-app" // User logs in from phone phoneToken := createTestRefreshToken(userID, clientID, "phone-ios", "phone_token_123") err := db.Create(ctx, phoneToken) require.NoError(t, err) // User logs in from tablet tabletToken := createTestRefreshToken(userID, clientID, "tablet-android", "tablet_token_456") err = db.Create(ctx, tabletToken) require.NoError(t, err) // User logs in from desktop desktopToken := createTestRefreshToken(userID, clientID, "desktop-windows", "desktop_token_789") err = db.Create(ctx, desktopToken) require.NoError(t, err) // User wants to logout from all devices except current (phone) err = db.RevokeAll(ctx, userID, "phone-ios") require.NoError(t, err) // Phone should still work phoneCRT := &model.ClientRefreshToken{ SessionIdentifier: model.SessionIdentifier{ ClientID: clientID, DeviceID: "phone-ios", }, RefreshToken: "phone_token_123", } _, err = db.GetByCRT(ctx, phoneCRT) require.NoError(t, err) // Tablet and desktop should be revoked tabletCRT := &model.ClientRefreshToken{ SessionIdentifier: model.SessionIdentifier{ ClientID: clientID, DeviceID: "tablet-android", }, RefreshToken: "tablet_token_456", } _, err = db.GetByCRT(ctx, tabletCRT) assert.Error(t, err) desktopCRT := &model.ClientRefreshToken{ SessionIdentifier: model.SessionIdentifier{ ClientID: clientID, DeviceID: "desktop-windows", }, RefreshToken: "desktop_token_789", } _, err = db.GetByCRT(ctx, desktopCRT) assert.Error(t, err) }) }