Skip to content

Commit bcbdcfa

Browse files
committed
feat(api): add namespace conflicts detection system
- Add NamespaceConflicts model and detection method - Replace NamespaceResolve usage with NamespaceConflicts in CreateNamespace - Implement NamespaceConflicts store method with MongoDB aggregation - Add comprehensive test coverage for conflict detection - Simplify namespace creation validation logic
1 parent a624421 commit bcbdcfa

File tree

7 files changed

+144
-48
lines changed

7 files changed

+144
-48
lines changed

api/services/namespace.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ func (s *service) CreateNamespace(ctx context.Context, req *requests.NamespaceCr
4444
}
4545
}
4646

47-
if dup, err := s.store.NamespaceResolve(ctx, store.NamespaceNameResolver, strings.ToLower(req.Name)); dup != nil || (err != nil && err != store.ErrNoDocuments) {
47+
conflictsTarget := &models.NamespaceConflicts{Name: strings.ToLower(req.Name)}
48+
if _, has, err := s.store.NamespaceConflicts(ctx, conflictsTarget); has || err != nil {
4849
return nil, NewErrNamespaceDuplicated(err)
4950
}
5051

api/services/namespace_test.go

Lines changed: 12 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -444,50 +444,15 @@ func TestCreateNamespace(t *testing.T) {
444444
}, nil).
445445
Once()
446446
storeMock.
447-
On("NamespaceResolve", ctx, store.NamespaceNameResolver, "namespace").
448-
Return(&models.Namespace{Name: "namespace"}, nil).
447+
On("NamespaceConflicts", ctx, &models.NamespaceConflicts{Name: "namespace"}).
448+
Return(nil, true, nil).
449449
Once()
450450
},
451451
expected: Expected{
452452
ns: nil,
453453
err: NewErrNamespaceDuplicated(nil),
454454
},
455455
},
456-
{
457-
description: "fails retrieve namespace fails without ErrNoDocuments",
458-
req: &requests.NamespaceCreate{
459-
UserID: "000000000000000000000000",
460-
Name: "namespace",
461-
TenantID: "00000000-0000-4000-0000-000000000000",
462-
},
463-
requiredMocks: func() {
464-
storeMock.
465-
On("UserGetInfo", ctx, "000000000000000000000000").
466-
Return(
467-
&models.UserInfo{
468-
OwnedNamespaces: []models.Namespace{{}},
469-
AssociatedNamespaces: []models.Namespace{},
470-
},
471-
nil,
472-
).
473-
Once()
474-
storeMock.
475-
On("UserResolve", ctx, store.UserIDResolver, "000000000000000000000000").
476-
Return(&models.User{
477-
ID: "000000000000000000000000",
478-
MaxNamespaces: 3,
479-
}, nil).
480-
Once()
481-
storeMock.
482-
On("NamespaceResolve", ctx, store.NamespaceNameResolver, "namespace").
483-
Return(nil, errors.New("error")).
484-
Once()
485-
},
486-
expected: Expected{
487-
ns: nil,
488-
err: NewErrNamespaceDuplicated(errors.New("error")),
489-
},
490-
},
491456
{
492457
description: "fails when store namespace create fails",
493458
req: &requests.NamespaceCreate{
@@ -514,8 +479,8 @@ func TestCreateNamespace(t *testing.T) {
514479
}, nil).
515480
Once()
516481
storeMock.
517-
On("NamespaceResolve", ctx, store.NamespaceNameResolver, "namespace").
518-
Return(nil, store.ErrNoDocuments).
482+
On("NamespaceConflicts", ctx, &models.NamespaceConflicts{Name: "namespace"}).
483+
Return(nil, false, nil).
519484
Once()
520485
// envs.IsCommunity = true
521486
envMock.
@@ -591,8 +556,8 @@ func TestCreateNamespace(t *testing.T) {
591556
}, nil).
592557
Once()
593558
storeMock.
594-
On("NamespaceResolve", ctx, store.NamespaceNameResolver, "namespace").
595-
Return(nil, store.ErrNoDocuments).
559+
On("NamespaceConflicts", ctx, &models.NamespaceConflicts{Name: "namespace"}).
560+
Return(nil, false, nil).
596561
Once()
597562
// envs.IsCommunity = true
598563
envMock.
@@ -707,8 +672,8 @@ func TestCreateNamespace(t *testing.T) {
707672
}, nil).
708673
Once()
709674
storeMock.
710-
On("NamespaceResolve", ctx, store.NamespaceNameResolver, "namespace").
711-
Return(nil, store.ErrNoDocuments).
675+
On("NamespaceConflicts", ctx, &models.NamespaceConflicts{Name: "namespace"}).
676+
Return(nil, false, nil).
712677
Once()
713678
// envs.IsCommunity = true
714679
envMock.
@@ -829,8 +794,8 @@ func TestCreateNamespace(t *testing.T) {
829794
}, nil).
830795
Once()
831796
storeMock.
832-
On("NamespaceResolve", ctx, store.NamespaceNameResolver, "namespace").
833-
Return(nil, store.ErrNoDocuments).
797+
On("NamespaceConflicts", ctx, &models.NamespaceConflicts{Name: "namespace"}).
798+
Return(nil, false, nil).
834799
Once()
835800
envMock.
836801
On("Get", "SHELLHUB_CLOUD").
@@ -946,8 +911,8 @@ func TestCreateNamespace(t *testing.T) {
946911
}, nil).
947912
Once()
948913
storeMock.
949-
On("NamespaceResolve", ctx, store.NamespaceNameResolver, "namespace").
950-
Return(nil, store.ErrNoDocuments).
914+
On("NamespaceConflicts", ctx, &models.NamespaceConflicts{Name: "namespace"}).
915+
Return(nil, false, nil).
951916
Once()
952917
envMock.
953918
On("Get", "SHELLHUB_CLOUD").

api/store/mocks/store.go

Lines changed: 37 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/store/mongo/namespace.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,38 @@ func (s *Store) NamespaceCreate(ctx context.Context, namespace *models.Namespace
289289
return namespace, err
290290
}
291291

292+
func (s *Store) NamespaceConflicts(ctx context.Context, target *models.NamespaceConflicts) ([]string, bool, error) {
293+
pipeline := []bson.M{
294+
{
295+
"$match": bson.M{
296+
"$or": []bson.M{
297+
{"name": target.Name},
298+
},
299+
},
300+
},
301+
}
302+
303+
cursor, err := s.db.Collection("namespaces").Aggregate(ctx, pipeline)
304+
if err != nil {
305+
return nil, false, FromMongoError(err)
306+
}
307+
defer cursor.Close(ctx)
308+
309+
namespace := new(models.NamespaceConflicts)
310+
conflicts := make([]string, 0)
311+
for cursor.Next(ctx) {
312+
if err := cursor.Decode(&namespace); err != nil {
313+
return nil, false, FromMongoError(err)
314+
}
315+
316+
if namespace.Name == target.Name {
317+
conflicts = append(conflicts, "name")
318+
}
319+
}
320+
321+
return conflicts, len(conflicts) > 0, nil
322+
}
323+
292324
func (s *Store) NamespaceDelete(ctx context.Context, tenantID string) error {
293325
session, err := s.db.Client().StartSession()
294326
if err != nil {

api/store/mongo/namespace_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,52 @@ func TestNamespaceCreate(t *testing.T) {
428428
}
429429
}
430430

431+
func TestNamespaceConflicts(t *testing.T) {
432+
type Expected struct {
433+
conflicts []string
434+
ok bool
435+
err error
436+
}
437+
438+
cases := []struct {
439+
description string
440+
target *models.NamespaceConflicts
441+
fixtures []string
442+
expected Expected
443+
}{
444+
{
445+
description: "no conflicts when target is empty",
446+
target: &models.NamespaceConflicts{},
447+
fixtures: []string{fixtureNamespaces},
448+
expected: Expected{[]string{}, false, nil},
449+
},
450+
{
451+
description: "no conflicts with non existing name",
452+
target: &models.NamespaceConflicts{Name: "nonexistent-namespace"},
453+
fixtures: []string{fixtureNamespaces},
454+
expected: Expected{[]string{}, false, nil},
455+
},
456+
{
457+
description: "conflict detected with existing name",
458+
target: &models.NamespaceConflicts{Name: "namespace-1"},
459+
fixtures: []string{fixtureNamespaces},
460+
expected: Expected{[]string{"name"}, true, nil},
461+
},
462+
}
463+
464+
for _, tc := range cases {
465+
t.Run(tc.description, func(t *testing.T) {
466+
ctx := context.Background()
467+
468+
require.NoError(t, srv.Apply(tc.fixtures...))
469+
t.Cleanup(func() { require.NoError(t, srv.Reset()) })
470+
471+
conflicts, ok, err := s.NamespaceConflicts(ctx, tc.target)
472+
require.Equal(t, tc.expected, Expected{conflicts, ok, err})
473+
})
474+
}
475+
}
476+
431477
func TestNamespaceEdit(t *testing.T) {
432478
cases := []struct {
433479
description string

api/store/namespace.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ type NamespaceStore interface {
3838

3939
NamespaceCreate(ctx context.Context, namespace *models.Namespace) (*models.Namespace, error)
4040

41+
NamespaceConflicts(ctx context.Context, target *models.NamespaceConflicts) (conflicts []string, has bool, err error)
42+
4143
// NamespaceEdit updates a namespace with the specified tenant.
4244
// It returns an error, if any, or store.ErrNoDocuments if the namespace does not exist.
4345
NamespaceEdit(ctx context.Context, tenant string, changes *models.NamespaceChanges) error

pkg/models/namespace.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,16 @@ const DefaultAnnouncementMessage = `
8686
* *
8787
******************************************************************
8888
`
89+
90+
// NamespaceConflicts holds namespace attributes that must be unique for each document and can be utilized in queries
91+
// to identify conflicts.
92+
type NamespaceConflicts struct {
93+
Name string
94+
}
95+
96+
// Distinct removes the c attributes whether it's equal to the namespace attribute.
97+
func (c *NamespaceConflicts) Distinct(namespace *Namespace) {
98+
if c.Name == namespace.Name {
99+
c.Name = ""
100+
}
101+
}

0 commit comments

Comments
 (0)