Organize backend into product executable and shared Go modules
This commit is contained in:
1 parent
981e84a4b4
commit
f0720ec66b
382 files changed
+1944
-6989
No files matched your search
@@ -0,0 +1,231 @@
|
||||
//go:build integration
|
||||
// +build integration
|
||||
|
||||
package refreshtokens_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha1"
|
||||
"errors"
|
||||
"fmt"
|
||||
sessionmodel "git.sendico.io/tech/sxchnge/product/sessions/model"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/product/sessions/store"
|
||||
"git.sendico.io/tech/sxchnge/shared/errors"
|
||||
factory "git.sendico.io/tech/sxchnge/shared/logging/factory"
|
||||
"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) *sessionmodel.RefreshToken {
|
||||
return &sessionmodel.RefreshToken{
|
||||
ClientRefreshToken: sessionmodel.ClientRefreshToken{
|
||||
SessionIdentifier: sessionmodel.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 := &sessionmodel.ClientRefreshToken{
|
||||
SessionIdentifier: sessionmodel.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 := &sessionmodel.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 := &sessionmodel.ClientRefreshToken{
|
||||
SessionIdentifier: sessionmodel.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 := &sessionmodel.ClientRefreshToken{
|
||||
SessionIdentifier: sessionmodel.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 := &sessionmodel.ClientRefreshToken{
|
||||
SessionIdentifier: sessionmodel.SessionIdentifier{
|
||||
ClientID: clientID,
|
||||
DeviceID: "tablet-android",
|
||||
},
|
||||
RefreshToken: "tablet_token_456",
|
||||
}
|
||||
_, err = db.GetByCRT(ctx, tabletCRT)
|
||||
assert.Error(t, err)
|
||||
|
||||
desktopCRT := &sessionmodel.ClientRefreshToken{
|
||||
SessionIdentifier: sessionmodel.SessionIdentifier{
|
||||
ClientID: clientID,
|
||||
DeviceID: "desktop-windows",
|
||||
},
|
||||
RefreshToken: "desktop_token_789",
|
||||
}
|
||||
_, err = db.GetByCRT(ctx, desktopCRT)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user