Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions internal/repoerrors/foreignkeyviolation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package repoerrors

import "github.com/device-management-toolkit/console/pkg/consoleerrors"

// ForeignKeyViolationError reports a delete or insert that would break a
// relationship between records. Postgres and SQLite raise it from the foreign
// key constraint itself; MongoDB has no constraints, so its repositories check
// the referencing collection first and raise the same error — the way RPS did
// it (src/data/postgres/tables/wirelessProfiles.ts queries
// profiles_wirelessconfigs before deleting). Controllers map it to 400.
type ForeignKeyViolationError struct {
Console consoleerrors.InternalError
}

func (e ForeignKeyViolationError) Error() string {
return e.Console.Error()
}

func (e ForeignKeyViolationError) Wrap(details string) error {
e.Console.Message = "foreign key violation: " + details

return e
}
4 changes: 4 additions & 0 deletions internal/usecase/nosqldb/mongo/ciraconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ func (r *CIRARepo) Update(ctx context.Context, c *entity.CIRAConfig) (bool, erro
}},
)
if err != nil {
if isDuplicateKey(err) {
return false, errCIRANotUnique.Wrap(err.Error())
}

return false, errCIRADatabase.Wrap("Update", "UpdateOne", err)
}

Expand Down
23 changes: 23 additions & 0 deletions internal/usecase/nosqldb/mongo/ciraconfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,29 @@ func TestCIRARepo_Insert_DuplicateReturnsNotUniqueError(t *testing.T) {
require.True(t, errors.As(err, &nu), "expected NotUniqueError, got %T: %v", err, err)
}

// A unique-index collision on update has to reach the handler as a
// NotUniqueError so it answers 409, the way the SQL backends do.
func TestCIRARepo_Update_DuplicateReturnsNotUniqueError(t *testing.T) {
t.Parallel()

db, md := newMockedDB(t)

md.AddResponses(duplicateKeyResponse())

repo := mongo.NewCIRARepo(db)

ok, err := repo.Update(context.Background(), &entity.CIRAConfig{
ConfigName: "cira1",
TenantID: "t1",
})
require.False(t, ok)
require.Error(t, err)

var notUnique repoerrors.NotUniqueError

require.ErrorAs(t, err, &notUnique)
}

func TestCIRARepo_Update_Matched(t *testing.T) {
t.Parallel()

Expand Down
4 changes: 4 additions & 0 deletions internal/usecase/nosqldb/mongo/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,10 @@ func (r *DeviceRepo) Update(ctx context.Context, d *entity.Device) (bool, error)
}},
)
if err != nil {
if isDuplicateKey(err) {
return false, errDeviceNotUnique.Wrap(err.Error())
}

return false, errDeviceDatabase.Wrap("Update", "UpdateOne", err)
}

Expand Down
23 changes: 23 additions & 0 deletions internal/usecase/nosqldb/mongo/device_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,29 @@ func TestDeviceRepo_Insert_DuplicateReturnsNotUniqueError(t *testing.T) {
require.True(t, errors.As(err, &nu))
}

// A unique-index collision on update has to reach the handler as a
// NotUniqueError so it answers 409, the way the SQL backends do.
func TestDeviceRepo_Update_DuplicateReturnsNotUniqueError(t *testing.T) {
t.Parallel()

db, md := newMockedDB(t)

md.AddResponses(duplicateKeyResponse())

repo := mongo.NewDeviceRepo(db)

ok, err := repo.Update(context.Background(), &entity.Device{
GUID: "g1",
TenantID: "t1",
})
require.False(t, ok)
require.Error(t, err)

var notUnique repoerrors.NotUniqueError

require.ErrorAs(t, err, &notUnique)
}

func TestDeviceRepo_Update_Matched(t *testing.T) {
t.Parallel()

Expand Down
4 changes: 4 additions & 0 deletions internal/usecase/nosqldb/mongo/domain.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ func (r *DomainRepo) Update(ctx context.Context, d *entity.Domain) (bool, error)
}},
)
if err != nil {
if isDuplicateKey(err) {
return false, errDomainNotUnique.Wrap(err.Error())
}

return false, errDomainDatabase.Wrap("Update", "UpdateOne", err)
}

Expand Down
24 changes: 24 additions & 0 deletions internal/usecase/nosqldb/mongo/domain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,30 @@ func TestDomainRepo_Update_Matched(t *testing.T) {
require.True(t, ok)
}

// A suffix collision on update has to reach the handler as a NotUniqueError so
// it answers 409, the way the SQL backends do from their unique index.
func TestDomainRepo_Update_DuplicateReturnsNotUniqueError(t *testing.T) {
t.Parallel()

db, md := newMockedDB(t)

md.AddResponses(duplicateKeyResponse())

repo := mongo.NewDomainRepo(db)

ok, err := repo.Update(context.Background(), &entity.Domain{
ProfileName: "Acme",
DomainSuffix: "taken.com",
TenantID: "t1",
})
require.False(t, ok)
require.Error(t, err)

var notUnique repoerrors.NotUniqueError

require.ErrorAs(t, err, &notUnique)
}

func TestDomainRepo_Update_NoMatch(t *testing.T) {
t.Parallel()

Expand Down
4 changes: 4 additions & 0 deletions internal/usecase/nosqldb/mongo/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ var (
errWiFiNotUnique = repoerrors.NotUniqueError{Console: consoleerrors.CreateConsoleError("MongoWirelessRepo")}
errProfileWiFiConfigsDatabase = repoerrors.DatabaseError{Console: consoleerrors.CreateConsoleError("MongoProfileWiFiConfigsRepo")}
errProfileWiFiConfigsNotUnique = repoerrors.NotUniqueError{Console: consoleerrors.CreateConsoleError("MongoProfileWiFiConfigsRepo")}

// Mongo has no foreign keys, so the repositories check the referencing
// collection themselves and raise the error SQL gets from its constraint.
errWiFiForeignKeyViolation = repoerrors.ForeignKeyViolationError{Console: consoleerrors.CreateConsoleError("MongoWirelessRepo")}
)

// isDuplicateKey matches Mongo E11000 errors (mapped to NotUniqueError, mirroring SQL).
Expand Down
4 changes: 4 additions & 0 deletions internal/usecase/nosqldb/mongo/ieee8021xconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ func (r *IEEE8021xRepo) Update(ctx context.Context, c *entity.IEEE8021xConfig) (
}},
)
if err != nil {
if isDuplicateKey(err) {
return false, errIEEENotUnique.Wrap(err.Error())
}

return false, errIEEEDatabase.Wrap("Update", "UpdateOne", err)
}

Expand Down
23 changes: 23 additions & 0 deletions internal/usecase/nosqldb/mongo/ieee8021xconfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,29 @@ func TestIEEE8021xRepo_Insert_DuplicateReturnsNotUniqueError(t *testing.T) {
require.True(t, errors.As(err, &nu))
}

// A unique-index collision on update has to reach the handler as a
// NotUniqueError so it answers 409, the way the SQL backends do.
func TestIEEE8021xRepo_Update_DuplicateReturnsNotUniqueError(t *testing.T) {
t.Parallel()

db, md := newMockedDB(t)

md.AddResponses(duplicateKeyResponse())

repo := mongo.NewIEEE8021xRepo(db)

ok, err := repo.Update(context.Background(), &entity.IEEE8021xConfig{
ProfileName: "ieee1",
TenantID: "t1",
})
require.False(t, ok)
require.Error(t, err)

var notUnique repoerrors.NotUniqueError

require.ErrorAs(t, err, &notUnique)
}

func TestIEEE8021xRepo_Update(t *testing.T) {
t.Parallel()

Expand Down
4 changes: 4 additions & 0 deletions internal/usecase/nosqldb/mongo/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,10 @@ func (r *ProfileRepo) Update(ctx context.Context, p *entity.Profile) (bool, erro
bson.M{opSet: set},
)
if err != nil {
if isDuplicateKey(err) {
return false, errProfileNotUnique.Wrap(err.Error())
}

return false, errProfileDatabase.Wrap("Update", "UpdateOne", err)
}

Expand Down
23 changes: 23 additions & 0 deletions internal/usecase/nosqldb/mongo/profile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,29 @@ func TestProfileRepo_Insert_DuplicateReturnsNotUniqueError(t *testing.T) {
require.True(t, errors.As(err, &nu))
}

// A unique-index collision on update has to reach the handler as a
// NotUniqueError so it answers 409, the way the SQL backends do.
func TestProfileRepo_Update_DuplicateReturnsNotUniqueError(t *testing.T) {
t.Parallel()

db, md := newMockedDB(t)

md.AddResponses(duplicateKeyResponse())

repo := mongo.NewProfileRepo(db, logger.New("error"))

ok, err := repo.Update(context.Background(), &entity.Profile{
ProfileName: "p1",
TenantID: "t1",
})
require.False(t, ok)
require.Error(t, err)

var notUnique repoerrors.NotUniqueError

require.ErrorAs(t, err, &notUnique)
}

func TestProfileRepo_Update(t *testing.T) {
t.Parallel()

Expand Down
29 changes: 23 additions & 6 deletions internal/usecase/nosqldb/mongo/wificonfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,20 @@
)

type WirelessRepo struct {
col *mongo.Collection
ieee8021xCol *mongo.Collection
log logger.Interface
col *mongo.Collection
ieee8021xCol *mongo.Collection
profileWiFiCol *mongo.Collection
log logger.Interface
}

var _ wificonfigs.Repository = (*WirelessRepo)(nil)

func NewWirelessRepo(db *mongo.Database, log logger.Interface) *WirelessRepo {
return &WirelessRepo{
col: db.Collection(CollectionWirelessConfigs),
ieee8021xCol: db.Collection(CollectionIEEE8021xConfigs),
log: log,
col: db.Collection(CollectionWirelessConfigs),
ieee8021xCol: db.Collection(CollectionIEEE8021xConfigs),
profileWiFiCol: db.Collection(CollectionProfileWiFiConfigs),
log: log,
}
}

Expand Down Expand Up @@ -162,6 +164,17 @@
return false, nil
}

// SQL leaves this to the profiles_wirelessconfigs foreign key. Mongo has no
// constraints, so look for a referencing row the way RPS did before deleting.
err := r.profileWiFiCol.FindOne(ctx, bson.M{fieldWirelessProfileName: profileName, fieldTenantID: tenantID}).Err()

switch {
case err == nil:
return false, errWiFiForeignKeyViolation.Wrap("wireless profile " + profileName + " is associated with an AMT profile")
case !errors.Is(err, mongo.ErrNoDocuments):
return false, errWiFiDatabase.Wrap("Delete", "FindOne", err)
}

res, err := r.col.DeleteOne(ctx, bson.M{fieldProfileName: profileName, fieldTenantID: tenantID})
if err != nil {
return false, errWiFiDatabase.Wrap("Delete", "DeleteOne", err)
Expand Down Expand Up @@ -192,6 +205,10 @@
}},
)
if err != nil {
if isDuplicateKey(err) {
return false, errWiFiNotUnique.Wrap(err.Error())
}

return false, errWiFiDatabase.Wrap("Update", "UpdateOne", err)
}

Expand Down
77 changes: 76 additions & 1 deletion internal/usecase/nosqldb/mongo/wificonfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,29 @@ func TestWirelessRepo_Insert_DuplicateReturnsNotUniqueError(t *testing.T) {
require.True(t, errors.As(err, &nu))
}

// A unique-index collision on update has to reach the handler as a
// NotUniqueError so it answers 409, the way the SQL backends do.
func TestWirelessRepo_Update_DuplicateReturnsNotUniqueError(t *testing.T) {
t.Parallel()

db, md := newMockedDB(t)

md.AddResponses(duplicateKeyResponse())

repo := mongo.NewWirelessRepo(db, logger.New("error"))

ok, err := repo.Update(context.Background(), &entity.WirelessConfig{
ProfileName: "wifi1",
TenantID: "t1",
})
require.False(t, ok)
require.Error(t, err)

var notUnique repoerrors.NotUniqueError

require.ErrorAs(t, err, &notUnique)
}

func TestWirelessRepo_Update(t *testing.T) {
t.Parallel()

Expand All @@ -186,11 +209,63 @@ func TestWirelessRepo_Delete(t *testing.T) {

db, md := newMockedDB(t)

md.AddResponses(deleteResponse(1))
// No referencing profiles_wirelessconfigs row, then the delete itself.
md.AddResponses(findResponse("consoledb.profiles_wirelessconfigs"), deleteResponse(1))

Comment on lines +212 to 214
repo := mongo.NewWirelessRepo(db, logger.New("error"))

ok, err := repo.Delete(context.Background(), "wifi1", "t1")
require.NoError(t, err)
require.True(t, ok)
}

// A failed reference lookup must not fall through to the delete: the repo cannot
// tell whether the wireless profile is still in use, so it reports the error.
func TestWirelessRepo_Delete_ReferenceLookupFailurePreventsDelete(t *testing.T) {
t.Parallel()

db, md := newMockedDB(t)

// Only one queued response: a delete would need a second, and reaching it
// would hang rather than silently pass.
md.AddResponses(bson.D{
{Key: "ok", Value: 0},
{Key: "code", Value: int32(13)},
{Key: "errmsg", Value: "not authorized"},
})

repo := mongo.NewWirelessRepo(db, logger.New("error"))

ok, err := repo.Delete(context.Background(), "wifi1", "t1")
require.False(t, ok)
require.Error(t, err)

var dbErr repoerrors.DatabaseError

require.ErrorAs(t, err, &dbErr)
}

// SQL gets this from the profiles_wirelessconfigs foreign key; Mongo has to look
// for the referencing row itself, and must raise the same error so the handler
// still answers 400.
func TestWirelessRepo_Delete_ReferencedByProfileIsRejected(t *testing.T) {
t.Parallel()

db, md := newMockedDB(t)

md.AddResponses(findResponse("consoledb.profiles_wirelessconfigs",
bson.D{{Key: "profilename", Value: "amt-profile"}, {Key: "wirelessprofilename", Value: "wifi1"}},
))

repo := mongo.NewWirelessRepo(db, logger.New("error"))

ok, err := repo.Delete(context.Background(), "wifi1", "t1")
require.False(t, ok)
require.Error(t, err)

var fkErr repoerrors.ForeignKeyViolationError

require.ErrorAs(t, err, &fkErr)
// FriendlyMessage is what the handler puts in the 400 body.
require.Contains(t, fkErr.Console.FriendlyMessage(), "foreign key violation")
}
Loading
Loading