package refreshtokens import ( "context" "errors" "testing" "time" "git.sendico.io/tech/sxchnge/bff/pkg/db/repository" "git.sendico.io/tech/sxchnge/bff/pkg/db/repository/builder" "git.sendico.io/tech/sxchnge/bff/pkg/db/storable" "git.sendico.io/tech/sxchnge/bff/pkg/db/template" "git.sendico.io/tech/sxchnge/bff/pkg/merrors" "git.sendico.io/tech/sxchnge/bff/pkg/model" "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/v2/bson" "go.uber.org/zap" ) type createRepository struct { repository.Repository filters []bson.D patches []bson.D patchMatches []bool patchErr error insertCalls int insertErr error storedID bson.ObjectID } func (r *createRepository) PatchOneByFilter( _ context.Context, filter builder.Query, patch builder.Patch, result storable.Storable, ) (bool, error) { r.filters = append(r.filters, filter.BuildQuery()) r.patches = append(r.patches, patch.Build()) if r.patchErr != nil { return false, r.patchErr } call := len(r.filters) - 1 matched := call < len(r.patchMatches) && r.patchMatches[call] if matched { result.SetID(r.storedID) } return matched, nil } func (r *createRepository) Insert(_ context.Context, object storable.Storable, _ builder.Query) error { r.insertCalls++ if object.GetID() == nil || object.GetID().IsZero() { object.SetID(bson.NewObjectID()) } return r.insertErr } func newCreateDB(repo repository.Repository) *RefreshTokenDB { return &RefreshTokenDB{ DBImp: template.DBImp[*model.RefreshToken]{ Logger: zap.NewNop(), Repository: repo, }, } } func versionedToken(accountRef bson.ObjectID, version, value string) *model.RefreshToken { return &model.RefreshToken{ ClientRefreshToken: model.ClientRefreshToken{ SessionIdentifier: model.SessionIdentifier{ ClientID: "web-app", DeviceID: "browser", SessionVersion: version, }, RefreshToken: value, }, AccountRef: &accountRef, ExpiresAt: time.Now().Add(24 * time.Hour), UserAgent: "test-agent", IPAddress: "192.0.2.1", } } func extendedJSON(t *testing.T, document bson.D) string { t.Helper() raw, err := bson.MarshalExtJSON(document, true, false) require.NoError(t, err) return string(raw) } func TestCreateReplacesSlotWithoutMatchingNewSessionVersion(t *testing.T) { storedID := bson.NewObjectID() repo := &createRepository{ patchMatches: []bool{true}, storedID: storedID, } db := newCreateDB(repo) token := versionedToken(bson.NewObjectID(), "version-2", "replacement-token") err := db.Create(context.Background(), token) require.NoError(t, err) require.Equal(t, storedID, *token.GetID()) require.Zero(t, repo.insertCalls) require.Len(t, repo.filters, 1) filterJSON := extendedJSON(t, repo.filters[0]) require.Contains(t, filterJSON, "accountRef") require.Contains(t, filterJSON, "clientId") require.Contains(t, filterJSON, "deviceId") require.Contains(t, filterJSON, "isRevoked") require.NotContains(t, filterJSON, "sessionVersion") patchJSON := extendedJSON(t, repo.patches[0]) require.Contains(t, patchJSON, "sessionVersion") require.Contains(t, patchJSON, "version-2") } func TestCreateReconcilesConcurrentSlotInsert(t *testing.T) { storedID := bson.NewObjectID() repo := &createRepository{ patchMatches: []bool{false, true}, insertErr: merrors.DataConflict("duplicate_key"), storedID: storedID, } db := newCreateDB(repo) token := versionedToken(bson.NewObjectID(), "version-2", "replacement-token") err := db.Create(context.Background(), token) require.NoError(t, err) require.Equal(t, storedID, *token.GetID()) require.Equal(t, 1, repo.insertCalls) require.Len(t, repo.filters, 2) } func TestCreatePreservesUnrelatedInsertConflict(t *testing.T) { repo := &createRepository{ patchMatches: []bool{false, false}, insertErr: merrors.DataConflict("duplicate_key"), storedID: bson.NewObjectID(), } db := newCreateDB(repo) token := versionedToken(bson.NewObjectID(), "version-2", "replacement-token") err := db.Create(context.Background(), token) require.Error(t, err) require.True(t, errors.Is(err, merrors.ErrDataConflict)) require.Equal(t, 1, repo.insertCalls) require.Len(t, repo.filters, 2) } func TestCredentialFilterKeepsSessionVersion(t *testing.T) { query := filterByAccount(bson.NewObjectID(), &model.SessionIdentifier{ ClientID: "web-app", DeviceID: "browser", SessionVersion: "version-2", }) filterJSON := extendedJSON(t, query.BuildQuery()) require.Contains(t, filterJSON, "sessionVersion") require.Contains(t, filterJSON, "version-2") }