added fix for active indexed tokens + improved data structure for wallet description
This commit is contained in:
@@ -37,7 +37,9 @@ func Create(logger mlogger.Logger, db *mongo.Database) (*RefreshTokenDB, error)
|
||||
{Field: "clientId", Sort: ri.Asc},
|
||||
{Field: "deviceId", Sort: ri.Asc},
|
||||
},
|
||||
Unique: true,
|
||||
Unique: true,
|
||||
Name: "unique_active_session",
|
||||
PartialFilter: repository.Filter(IsRevokedField, false),
|
||||
}); err != nil {
|
||||
p.Logger.Error("Failed to create unique account/client/device index", zap.Error(err))
|
||||
return nil, err
|
||||
|
||||
@@ -10,23 +10,29 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tech/sendico/pkg/db/internal/mongo/refreshtokensdb"
|
||||
"github.com/tech/sendico/pkg/db/repository"
|
||||
"github.com/tech/sendico/pkg/db/repository/builder"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
factory "github.com/tech/sendico/pkg/mlogger/factory"
|
||||
"github.com/tech/sendico/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/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
func setupTestDB(t *testing.T) (*refreshtokensdb.RefreshTokenDB, func()) {
|
||||
db, _, cleanup := setupTestDBWithMongo(t)
|
||||
return db, cleanup
|
||||
}
|
||||
|
||||
func setupTestDBWithMongo(t *testing.T) (*refreshtokensdb.RefreshTokenDB, *mongo.Database, func()) {
|
||||
// mark as helper for better test failure reporting
|
||||
t.Helper()
|
||||
|
||||
@@ -62,7 +68,7 @@ func setupTestDB(t *testing.T) (*refreshtokensdb.RefreshTokenDB, func()) {
|
||||
_ = mongoContainer.Terminate(termCtx)
|
||||
}
|
||||
|
||||
return db, cleanup
|
||||
return db, database, cleanup
|
||||
}
|
||||
|
||||
func createTestRefreshToken(accountRef primitive.ObjectID, clientID, deviceID, token string) *model.RefreshToken {
|
||||
@@ -332,6 +338,63 @@ func TestRefreshTokenDB_SessionReplacement(t *testing.T) {
|
||||
_, err = db.GetByCRT(ctx, secondCRT)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("Create_After_GlobalRevocation_AllowsNewActive", func(t *testing.T) {
|
||||
userID := primitive.NewObjectID()
|
||||
clientID := "web-app"
|
||||
deviceID := "user-laptop"
|
||||
|
||||
firstToken := createTestRefreshToken(userID, clientID, deviceID, "revoked_token_123")
|
||||
err := db.Create(ctx, firstToken)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, firstToken.GetID())
|
||||
|
||||
// Global revoke (deviceID empty) — all tokens should be revoked
|
||||
err = db.RevokeAll(ctx, userID, "")
|
||||
require.NoError(t, err)
|
||||
|
||||
var revoked model.RefreshToken
|
||||
err = db.Get(ctx, *firstToken.GetID(), &revoked)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, revoked.IsRevoked)
|
||||
|
||||
// Creating a new token for the same account/client/device must succeed and produce an active token
|
||||
reissueToken := createTestRefreshToken(userID, clientID, deviceID, "new_token_after_revocation")
|
||||
err = db.Create(ctx, reissueToken)
|
||||
require.NoError(t, err)
|
||||
|
||||
newCRT := &model.ClientRefreshToken{
|
||||
SessionIdentifier: model.SessionIdentifier{
|
||||
ClientID: clientID,
|
||||
DeviceID: deviceID,
|
||||
},
|
||||
RefreshToken: "new_token_after_revocation",
|
||||
}
|
||||
_, err = db.GetByCRT(ctx, newCRT)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Old token must remain unusable
|
||||
oldCRT := &model.ClientRefreshToken{
|
||||
SessionIdentifier: model.SessionIdentifier{
|
||||
ClientID: clientID,
|
||||
DeviceID: deviceID,
|
||||
},
|
||||
RefreshToken: "revoked_token_123",
|
||||
}
|
||||
_, err = db.GetByCRT(ctx, oldCRT)
|
||||
assert.Error(t, err)
|
||||
|
||||
// Both records exist: revoked + new active
|
||||
query := repository.Query().
|
||||
Filter(repository.AccountField(), userID).
|
||||
And(
|
||||
repository.Query().Comparison(repository.Field("clientId"), builder.Eq, clientID),
|
||||
repository.Query().Comparison(repository.Field("deviceId"), builder.Eq, deviceID),
|
||||
)
|
||||
ids, err := db.Repository.ListIDs(ctx, query)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, ids, 2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRefreshTokenDB_ClientManagement(t *testing.T) {
|
||||
@@ -637,3 +700,29 @@ func TestRefreshTokenDB_DatabaseIndexes(t *testing.T) {
|
||||
assert.Len(t, ids, 5) // Should find 5 non-revoked tokens
|
||||
})
|
||||
}
|
||||
|
||||
func TestRefreshTokenDB_IndexPartialUniqueActiveSession(t *testing.T) {
|
||||
db, database, cleanup := setupTestDBWithMongo(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
cursor, err := database.Collection(db.Repository.Collection()).Indexes().List(ctx)
|
||||
require.NoError(t, err)
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
found := false
|
||||
for cursor.Next(ctx) {
|
||||
var idx bson.M
|
||||
require.NoError(t, cursor.Decode(&idx))
|
||||
if idx["name"] == "unique_active_session" {
|
||||
found = true
|
||||
assert.Equal(t, true, idx["unique"])
|
||||
|
||||
partial, ok := idx["partialFilterExpression"].(bson.M)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, bson.M{"isRevoked": false}, partial)
|
||||
}
|
||||
}
|
||||
assert.True(t, found, "unique_active_session index not found")
|
||||
}
|
||||
|
||||
@@ -41,6 +41,9 @@ func (r *MongoRepository) CreateIndex(def *ri.Definition) error {
|
||||
if def.Name != "" {
|
||||
opts.SetName(def.Name)
|
||||
}
|
||||
if def.PartialFilter != nil {
|
||||
opts.SetPartialFilterExpression(def.PartialFilter.BuildQuery())
|
||||
}
|
||||
|
||||
_, err := r.collection.Indexes().CreateOne(
|
||||
context.Background(),
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
//go:build integration
|
||||
// +build integration
|
||||
|
||||
package repositoryimp_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tech/sendico/pkg/db/repository"
|
||||
ri "github.com/tech/sendico/pkg/db/repository/index"
|
||||
"github.com/testcontainers/testcontainers-go"
|
||||
"github.com/testcontainers/testcontainers-go/modules/mongodb"
|
||||
"github.com/testcontainers/testcontainers-go/wait"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
func TestCreateIndex_WithPartialFilter(t *testing.T) {
|
||||
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)
|
||||
|
||||
mongoURI, err := mongoContainer.ConnectionString(startCtx)
|
||||
require.NoError(t, err)
|
||||
|
||||
client, err := mongo.Connect(startCtx, options.Client().ApplyURI(mongoURI))
|
||||
require.NoError(t, err)
|
||||
defer client.Disconnect(context.Background())
|
||||
|
||||
database := client.Database("test_partial_index_" + t.Name())
|
||||
defer database.Drop(context.Background())
|
||||
|
||||
repo := repository.CreateMongoRepository(database, "partial_index_items")
|
||||
|
||||
def := &ri.Definition{
|
||||
Keys: []ri.Key{
|
||||
{Field: "field", Sort: ri.Asc},
|
||||
},
|
||||
Unique: true,
|
||||
Name: "partial_unique_field_true",
|
||||
PartialFilter: repository.Filter("flag", treu),
|
||||
}
|
||||
|
||||
require.NoError(t, repo.CreateIndex(def))
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cursor, err := database.Collection(repo.Collection()).Indexes().List(ctx)
|
||||
require.NoError(t, err)
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
found := false
|
||||
for cursor.Next(ctx) {
|
||||
var idx bson.M
|
||||
require.NoError(t, cursor.Decode(&idx))
|
||||
if idx["name"] == def.Name {
|
||||
found = true
|
||||
assert.Equal(t, true, idx["unique"])
|
||||
assert.Equal(t, bson.M{"field": int32(1)}, idx["key"])
|
||||
partial, ok := idx["partialFilterExpression"].(bson.M)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, bson.M{"flag": true}, partial)
|
||||
}
|
||||
}
|
||||
assert.True(t, found, "partial unique index was not created")
|
||||
|
||||
termCtx, termCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer termCancel()
|
||||
_ = mongoContainer.Terminate(termCtx)
|
||||
}
|
||||
Reference in New Issue
Block a user