Files
sxchnge/backend/edge/shared/storage/refreshtokens/refreshtokens_test.go
T

232 lines
6.8 KiB
Go

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