From 6bfdf35f217b06fa7a5f6b2b9800d4b37797997c Mon Sep 17 00:00:00 2001 From: Gerrit Date: Tue, 2 Sep 2025 12:31:06 +0200 Subject: [PATCH] Add filters for project and tenant labels. --- api/v1/mocks/UnsafeVersionServiceServer.go | 29 ++ api/v1/mocks/VersionServiceClient.go | 69 +++++ api/v1/mocks/VersionServiceServer.go | 59 ++++ api/v1/project.pb.go | 13 +- api/v1/tenant.pb.go | 13 +- pkg/datastore/memory.go | 4 +- pkg/datastore/mocks/Storage.go | 25 +- pkg/datastore/postgres.go | 9 +- pkg/datastore/postgres_benchmark_test.go | 2 +- pkg/datastore/postgres_test.go | 16 +- pkg/service/project.go | 40 ++- pkg/service/project_test.go | 321 +++++++++++++++++---- pkg/service/projectmember.go | 2 +- pkg/service/projectmember_test.go | 4 +- pkg/service/service.go | 4 +- pkg/service/tenant.go | 35 ++- pkg/service/tenant_test.go | 278 +++++++++++++++--- pkg/service/tenantmember.go | 2 +- pkg/service/tenantmember_test.go | 4 +- proto/v1/project.proto | 1 + proto/v1/tenant.proto | 1 + 21 files changed, 777 insertions(+), 154 deletions(-) create mode 100644 api/v1/mocks/UnsafeVersionServiceServer.go create mode 100644 api/v1/mocks/VersionServiceClient.go create mode 100644 api/v1/mocks/VersionServiceServer.go diff --git a/api/v1/mocks/UnsafeVersionServiceServer.go b/api/v1/mocks/UnsafeVersionServiceServer.go new file mode 100644 index 0000000..e6b6f08 --- /dev/null +++ b/api/v1/mocks/UnsafeVersionServiceServer.go @@ -0,0 +1,29 @@ +// Code generated by mockery v2.53.3. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// UnsafeVersionServiceServer is an autogenerated mock type for the UnsafeVersionServiceServer type +type UnsafeVersionServiceServer struct { + mock.Mock +} + +// mustEmbedUnimplementedVersionServiceServer provides a mock function with no fields +func (_m *UnsafeVersionServiceServer) mustEmbedUnimplementedVersionServiceServer() { + _m.Called() +} + +// NewUnsafeVersionServiceServer creates a new instance of UnsafeVersionServiceServer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewUnsafeVersionServiceServer(t interface { + mock.TestingT + Cleanup(func()) +}) *UnsafeVersionServiceServer { + mock := &UnsafeVersionServiceServer{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/api/v1/mocks/VersionServiceClient.go b/api/v1/mocks/VersionServiceClient.go new file mode 100644 index 0000000..905bbe3 --- /dev/null +++ b/api/v1/mocks/VersionServiceClient.go @@ -0,0 +1,69 @@ +// Code generated by mockery v2.53.3. DO NOT EDIT. + +package mocks + +import ( + context "context" + + grpc "google.golang.org/grpc" + + mock "github.com/stretchr/testify/mock" + + v1 "github.com/metal-stack/masterdata-api/api/v1" +) + +// VersionServiceClient is an autogenerated mock type for the VersionServiceClient type +type VersionServiceClient struct { + mock.Mock +} + +// Get provides a mock function with given fields: ctx, in, opts +func (_m *VersionServiceClient) Get(ctx context.Context, in *v1.GetVersionRequest, opts ...grpc.CallOption) (*v1.GetVersionResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Get") + } + + var r0 *v1.GetVersionResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *v1.GetVersionRequest, ...grpc.CallOption) (*v1.GetVersionResponse, error)); ok { + return rf(ctx, in, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, *v1.GetVersionRequest, ...grpc.CallOption) *v1.GetVersionResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.GetVersionResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *v1.GetVersionRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewVersionServiceClient creates a new instance of VersionServiceClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewVersionServiceClient(t interface { + mock.TestingT + Cleanup(func()) +}) *VersionServiceClient { + mock := &VersionServiceClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/api/v1/mocks/VersionServiceServer.go b/api/v1/mocks/VersionServiceServer.go new file mode 100644 index 0000000..a354392 --- /dev/null +++ b/api/v1/mocks/VersionServiceServer.go @@ -0,0 +1,59 @@ +// Code generated by mockery v2.53.3. DO NOT EDIT. + +package mocks + +import ( + context "context" + + v1 "github.com/metal-stack/masterdata-api/api/v1" + mock "github.com/stretchr/testify/mock" +) + +// VersionServiceServer is an autogenerated mock type for the VersionServiceServer type +type VersionServiceServer struct { + mock.Mock +} + +// Get provides a mock function with given fields: _a0, _a1 +func (_m *VersionServiceServer) Get(_a0 context.Context, _a1 *v1.GetVersionRequest) (*v1.GetVersionResponse, error) { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for Get") + } + + var r0 *v1.GetVersionResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *v1.GetVersionRequest) (*v1.GetVersionResponse, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(context.Context, *v1.GetVersionRequest) *v1.GetVersionResponse); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.GetVersionResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *v1.GetVersionRequest) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewVersionServiceServer creates a new instance of VersionServiceServer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewVersionServiceServer(t interface { + mock.TestingT + Cleanup(func()) +}) *VersionServiceServer { + mock := &VersionServiceServer{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/api/v1/project.pb.go b/api/v1/project.pb.go index ac6618b..f30f210 100644 --- a/api/v1/project.pb.go +++ b/api/v1/project.pb.go @@ -346,6 +346,7 @@ type ProjectFindRequest struct { Name *string `protobuf:"bytes,8,opt,name=name,proto3,oneof" json:"name,omitempty"` Description *string `protobuf:"bytes,9,opt,name=description,proto3,oneof" json:"description,omitempty"` TenantId *string `protobuf:"bytes,10,opt,name=tenant_id,json=tenantId,proto3,oneof" json:"tenant_id,omitempty"` + Labels []string `protobuf:"bytes,11,rep,name=labels,proto3" json:"labels,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -454,6 +455,13 @@ func (x *ProjectFindRequest) GetTenantId() string { return "" } +func (x *ProjectFindRequest) GetLabels() []string { + if x != nil { + return x.Labels + } + return nil +} + type ProjectResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Project *Project `protobuf:"bytes,1,opt,name=project,proto3" json:"project,omitempty"` @@ -572,7 +580,7 @@ const file_v1_project_proto_rawDesc = "" + "\x02id\x18\x01 \x01(\tR\x02id\"V\n" + "\x18ProjectGetHistoryRequest\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12*\n" + - "\x02at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\x02at\"\xa7\x05\n" + + "\x02at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\x02at\"\xbf\x05\n" + "\x12ProjectFindRequest\x12E\n" + "\rdeprecated_id\x18\x01 \x01(\v2\x1c.google.protobuf.StringValueB\x02\x18\x01R\fdeprecatedId\x12I\n" + "\x0fdeprecated_name\x18\x02 \x01(\v2\x1c.google.protobuf.StringValueB\x02\x18\x01R\x0edeprecatedName\x12W\n" + @@ -585,7 +593,8 @@ const file_v1_project_proto_rawDesc = "" + "\x04name\x18\b \x01(\tH\x01R\x04name\x88\x01\x01\x12%\n" + "\vdescription\x18\t \x01(\tH\x02R\vdescription\x88\x01\x01\x12 \n" + "\ttenant_id\x18\n" + - " \x01(\tH\x03R\btenantId\x88\x01\x01\x1a>\n" + + " \x01(\tH\x03R\btenantId\x88\x01\x01\x12\x16\n" + + "\x06labels\x18\v \x03(\tR\x06labels\x1a>\n" + "\x10AnnotationsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x05\n" + diff --git a/api/v1/tenant.pb.go b/api/v1/tenant.pb.go index be70c3d..f69035f 100644 --- a/api/v1/tenant.pb.go +++ b/api/v1/tenant.pb.go @@ -764,6 +764,7 @@ type TenantFindRequest struct { Annotations map[string]string `protobuf:"bytes,4,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` Id *string `protobuf:"bytes,5,opt,name=id,proto3,oneof" json:"id,omitempty"` Name *string `protobuf:"bytes,6,opt,name=name,proto3,oneof" json:"name,omitempty"` + Labels []string `protobuf:"bytes,7,rep,name=labels,proto3" json:"labels,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -842,6 +843,13 @@ func (x *TenantFindRequest) GetName() string { return "" } +func (x *TenantFindRequest) GetLabels() []string { + if x != nil { + return x.Labels + } + return nil +} + type TenantResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Tenant *Tenant `protobuf:"bytes,1,opt,name=tenant,proto3" json:"tenant,omitempty"` @@ -1005,7 +1013,7 @@ const file_v1_tenant_proto_rawDesc = "" + "\x02id\x18\x01 \x01(\tR\x02id\"U\n" + "\x17TenantGetHistoryRequest\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12*\n" + - "\x02at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\x02at\"\x91\x03\n" + + "\x02at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\x02at\"\xa9\x03\n" + "\x11TenantFindRequest\x12E\n" + "\rdeprecated_id\x18\x01 \x01(\v2\x1c.google.protobuf.StringValueB\x02\x18\x01R\fdeprecatedId\x12I\n" + "\x0fdeprecated_name\x18\x02 \x01(\v2\x1c.google.protobuf.StringValueB\x02\x18\x01R\x0edeprecatedName\x12\"\n" + @@ -1013,7 +1021,8 @@ const file_v1_tenant_proto_rawDesc = "" + ".v1.PagingR\x06paging\x12H\n" + "\vannotations\x18\x04 \x03(\v2&.v1.TenantFindRequest.AnnotationsEntryR\vannotations\x12\x13\n" + "\x02id\x18\x05 \x01(\tH\x00R\x02id\x88\x01\x01\x12\x17\n" + - "\x04name\x18\x06 \x01(\tH\x01R\x04name\x88\x01\x01\x1a>\n" + + "\x04name\x18\x06 \x01(\tH\x01R\x04name\x88\x01\x01\x12\x16\n" + + "\x06labels\x18\a \x03(\tR\x06labels\x1a>\n" + "\x10AnnotationsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x05\n" + diff --git a/pkg/datastore/memory.go b/pkg/datastore/memory.go index 51e41af..80f2f9f 100644 --- a/pkg/datastore/memory.go +++ b/pkg/datastore/memory.go @@ -70,8 +70,8 @@ func (m *memoryDatastore[E]) DeleteAll(ctx context.Context, ids ...string) error } // Find implements Storage. -func (m *memoryDatastore[E]) Find(ctx context.Context, filter map[string]any, paging *v1.Paging) ([]E, *uint64, error) { - m.log.Debug("find", "entity", m.entity, "filter", filter) +func (m *memoryDatastore[E]) Find(ctx context.Context, paging *v1.Paging, filters ...any) ([]E, *uint64, error) { + m.log.Debug("find", "entity", m.entity, "filter", filters) m.lock.Lock() defer m.lock.Unlock() diff --git a/pkg/datastore/mocks/Storage.go b/pkg/datastore/mocks/Storage.go index 1559acf..c5073bd 100644 --- a/pkg/datastore/mocks/Storage.go +++ b/pkg/datastore/mocks/Storage.go @@ -79,9 +79,12 @@ func (_m *Storage[E]) DeleteAll(ctx context.Context, ids ...string) error { return r0 } -// Find provides a mock function with given fields: ctx, filter, paging -func (_m *Storage[E]) Find(ctx context.Context, filter map[string]interface{}, paging *v1.Paging) ([]E, *uint64, error) { - ret := _m.Called(ctx, filter, paging) +// Find provides a mock function with given fields: ctx, paging, filters +func (_m *Storage[E]) Find(ctx context.Context, paging *v1.Paging, filters ...interface{}) ([]E, *uint64, error) { + var _ca []interface{} + _ca = append(_ca, ctx, paging) + _ca = append(_ca, filters...) + ret := _m.Called(_ca...) if len(ret) == 0 { panic("no return value specified for Find") @@ -90,27 +93,27 @@ func (_m *Storage[E]) Find(ctx context.Context, filter map[string]interface{}, p var r0 []E var r1 *uint64 var r2 error - if rf, ok := ret.Get(0).(func(context.Context, map[string]interface{}, *v1.Paging) ([]E, *uint64, error)); ok { - return rf(ctx, filter, paging) + if rf, ok := ret.Get(0).(func(context.Context, *v1.Paging, ...interface{}) ([]E, *uint64, error)); ok { + return rf(ctx, paging, filters...) } - if rf, ok := ret.Get(0).(func(context.Context, map[string]interface{}, *v1.Paging) []E); ok { - r0 = rf(ctx, filter, paging) + if rf, ok := ret.Get(0).(func(context.Context, *v1.Paging, ...interface{}) []E); ok { + r0 = rf(ctx, paging, filters...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]E) } } - if rf, ok := ret.Get(1).(func(context.Context, map[string]interface{}, *v1.Paging) *uint64); ok { - r1 = rf(ctx, filter, paging) + if rf, ok := ret.Get(1).(func(context.Context, *v1.Paging, ...interface{}) *uint64); ok { + r1 = rf(ctx, paging, filters...) } else { if ret.Get(1) != nil { r1 = ret.Get(1).(*uint64) } } - if rf, ok := ret.Get(2).(func(context.Context, map[string]interface{}, *v1.Paging) error); ok { - r2 = rf(ctx, filter, paging) + if rf, ok := ret.Get(2).(func(context.Context, *v1.Paging, ...interface{}) error); ok { + r2 = rf(ctx, paging, filters...) } else { r2 = ret.Error(2) } diff --git a/pkg/datastore/postgres.go b/pkg/datastore/postgres.go index 82175f0..591770f 100644 --- a/pkg/datastore/postgres.go +++ b/pkg/datastore/postgres.go @@ -32,7 +32,7 @@ type Storage[E Entity] interface { Get(ctx context.Context, id string) (E, error) GetHistory(ctx context.Context, id string, at time.Time, ve E) error GetHistoryCreated(ctx context.Context, id string, ve E) error - Find(ctx context.Context, filter map[string]any, paging *v1.Paging) ([]E, *uint64, error) + Find(ctx context.Context, paging *v1.Paging, filters ...any) ([]E, *uint64, error) } // Entity defines a database entity which is stored in jsonb format and with version information @@ -370,14 +370,15 @@ func (ds *datastore[E]) DeleteAll(ctx context.Context, ids ...string) error { } // Find returns matching elements from the database -func (ds *datastore[E]) Find(ctx context.Context, filter map[string]any, paging *v1.Paging) ([]E, *uint64, error) { - ds.log.Debug("find", "entity", ds.jsonField, "filter", filter) +func (ds *datastore[E]) Find(ctx context.Context, paging *v1.Paging, filters ...any) ([]E, *uint64, error) { + ds.log.Debug("find", "entity", ds.jsonField, "filters", filters) q := ds.sb.Select(ds.jsonField). From(ds.tableName) - if len(filter) > 0 { + for _, filter := range filters { q = q.Where(filter) } + q = q.OrderBy("id") // Add paging query if paging is defined diff --git a/pkg/datastore/postgres_benchmark_test.go b/pkg/datastore/postgres_benchmark_test.go index 2fe0177..130058c 100644 --- a/pkg/datastore/postgres_benchmark_test.go +++ b/pkg/datastore/postgres_benchmark_test.go @@ -90,7 +90,7 @@ func BenchmarkFindTenant(b *testing.B) { f := make(map[string]any) f["tenant ->> 'name'"] = "tenant-1" - t, _, err := ds.Find(context.Background(), f, nil) + t, _, err := ds.Find(context.Background(), nil, f) require.NoError(b, err) assert.NotNil(b, t) assert.Len(b, t, 1) diff --git a/pkg/datastore/postgres_test.go b/pkg/datastore/postgres_test.go index f4a21d2..1240b44 100644 --- a/pkg/datastore/postgres_test.go +++ b/pkg/datastore/postgres_test.go @@ -119,7 +119,7 @@ func TestCRUD(t *testing.T) { filter := make(map[string]any) // filter["tenant->>name"] = "Important Tenant" filter["id"] = "tenant-1" - tenants, _, err := tenantDS.Find(ctx, filter, nil) + tenants, _, err := tenantDS.Find(ctx, nil, filter) require.NoError(t, err) assert.NotNil(t, tenants) assert.Len(t, tenants, 1) @@ -503,7 +503,7 @@ func TestFind(t *testing.T) { // now search it filter := make(map[string]any) filter["id"] = t6 - tfr, _, err := tenantDS.Find(ctx, filter, nil) + tfr, _, err := tenantDS.Find(ctx, nil, filter) require.NoError(t, err) assert.NotNil(t, tfr) assert.Len(t, tfr, 1) @@ -520,13 +520,13 @@ func TestFind(t *testing.T) { } // find all filter = make(map[string]any) - tfr, _, err = tenantDS.Find(ctx, filter, nil) + tfr, _, err = tenantDS.Find(ctx, nil, filter) require.NoError(t, err) assert.NotNil(t, tfr) // find one filter["id"] = "ftenant-9" - t9, _, err := tenantDS.Find(ctx, filter, nil) + t9, _, err := tenantDS.Find(ctx, nil, filter) require.NoError(t, err) assert.NotNil(t, t9) assert.Len(t, t9, 1) @@ -534,7 +534,7 @@ func TestFind(t *testing.T) { // find one by name filter = make(map[string]any) filter["tenant ->> 'name'"] = "tenant-8" - t8, _, err := tenantDS.Find(ctx, filter, nil) + t8, _, err := tenantDS.Find(ctx, nil, filter) require.NoError(t, err) assert.NotNil(t, t8) assert.Len(t, t8, 1) @@ -542,7 +542,7 @@ func TestFind(t *testing.T) { // find one by description filter = make(map[string]any) filter["tenant ->> 'description'"] = "Tenant 4" - t4, _, err := tenantDS.Find(ctx, filter, nil) + t4, _, err := tenantDS.Find(ctx, nil, filter) require.NoError(t, err) assert.NotNil(t, t4) assert.Len(t, t4, 1) @@ -575,14 +575,14 @@ func TestFindWithPaging(t *testing.T) { assert.Len(t, ts, 100) // Then find the first 60 results - ts, nextpage, err = tenantDS.Find(ctx, nil, &v1.Paging{Count: pointer.Pointer(uint64(60))}) + ts, nextpage, err = tenantDS.Find(ctx, &v1.Paging{Count: pointer.Pointer(uint64(60))}) require.NoError(t, err) assert.NotNil(t, nextpage) assert.Equal(t, uint64(1), *nextpage) assert.Len(t, ts, 60) // At least the next 60, but only 40 left and no more pages - ts, nextpage, err = tenantDS.Find(ctx, nil, &v1.Paging{Page: nextpage, Count: pointer.Pointer(uint64(60))}) + ts, nextpage, err = tenantDS.Find(ctx, &v1.Paging{Page: nextpage, Count: pointer.Pointer(uint64(60))}) require.NoError(t, err) assert.Nil(t, nextpage) assert.Len(t, ts, 40) diff --git a/pkg/service/project.go b/pkg/service/project.go index e6c5488..6d49d25 100644 --- a/pkg/service/project.go +++ b/pkg/service/project.go @@ -4,6 +4,8 @@ import ( "context" "fmt" "log/slog" + "strconv" + "strings" v1 "github.com/metal-stack/masterdata-api/api/v1" "github.com/metal-stack/masterdata-api/pkg/datastore" @@ -46,7 +48,7 @@ func (s *projectService) Create(ctx context.Context, req *v1.ProjectCreateReques maxProjects := tenant.GetQuotas().GetProject().Max filter := make(map[string]any) filter["project ->> 'tenant_id'"] = project.GetTenantId() - projects, _, err := s.projectStore.Find(ctx, filter, nil) + projects, _, err := s.projectStore.Find(ctx, nil, filter) if err != nil { return nil, err } @@ -84,7 +86,7 @@ func (s *projectService) Delete(ctx context.Context, req *v1.ProjectDeleteReques filter := map[string]any{ "projectmember ->> 'project_id'": project.Meta.Id, } - memberships, _, err := s.projectMemberStore.Find(ctx, filter, nil) + memberships, _, err := s.projectMemberStore.Find(ctx, nil, filter) if err != nil { return nil, err } @@ -137,25 +139,45 @@ func (s *projectService) Find(ctx context.Context, req *v1.ProjectFindRequest) ( req.TenantId = &req.DeprecatedTenantId.Value // nolint:staticcheck } - filter := make(map[string]any) + var filters []any + + mapFilter := make(map[string]any) if req.Id != nil { - filter["id"] = req.Id + mapFilter["id"] = req.Id } if req.Name != nil { - filter["project ->> 'name'"] = req.Name + mapFilter["project ->> 'name'"] = req.Name } if req.Description != nil { - filter["project ->> 'description'"] = req.Description + mapFilter["project ->> 'description'"] = req.Description } if req.TenantId != nil { - filter["project ->> 'tenant_id'"] = req.TenantId + mapFilter["project ->> 'tenant_id'"] = req.TenantId } for key, value := range req.Annotations { // select * from project where project -> 'meta' -> 'annotations' ->> 'metal-stack.io/admitted' = 'true'; f := fmt.Sprintf("project -> 'meta' -> 'annotations' ->> '%s'", key) - filter[f] = value + mapFilter[f] = value + } + + if len(mapFilter) > 0 { + filters = append(filters, mapFilter) + } + + if len(req.Labels) > 0 { + var contains []string + + for _, label := range req.Labels { + contains = append(contains, strconv.Quote(label)) + } + + // select * from projects where project -> 'meta' -> 'labels' @> '["a=b","c=d"]'; + labelFilter := fmt.Sprintf(`project -> 'meta' -> 'labels' @> '[%s]'`, strings.Join(contains, ",")) + + filters = append(filters, labelFilter) } - res, nextPage, err := s.projectStore.Find(ctx, filter, req.Paging) + + res, nextPage, err := s.projectStore.Find(ctx, req.Paging, filters...) if err != nil { return nil, err } diff --git a/pkg/service/project_test.go b/pkg/service/project_test.go index dc5afe1..9852b45 100644 --- a/pkg/service/project_test.go +++ b/pkg/service/project_test.go @@ -3,15 +3,21 @@ package service import ( "context" "log/slog" + "slices" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" v1 "github.com/metal-stack/masterdata-api/api/v1" "github.com/metal-stack/metal-lib/pkg/pointer" + "github.com/metal-stack/metal-lib/pkg/testcommon" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/runtime/protoimpl" "testing" + "github.com/metal-stack/masterdata-api/pkg/datastore" "github.com/metal-stack/masterdata-api/pkg/datastore/mocks" ) @@ -81,7 +87,7 @@ func TestCreateProjectWithQuotaCheck(t *testing.T) { var projects []*v1.Project // see: https://github.com/stretchr/testify/blob/master/mock/mock.go#L149-L162 tenantStorageMock.On("Get", ctx, p1.GetTenantId()).Return(t1, nil) - storageMock.On("Find", ctx, filter, mock.AnythingOfType("*v1.Paging")).Return(projects, nil, nil) + storageMock.On("Find", ctx, mock.AnythingOfType("*v1.Paging"), filter).Return(projects, nil, nil) storageMock.On("Create", ctx, p1).Return(nil) resp, err := ts.Create(ctx, tcr) require.NoError(t, err) @@ -145,7 +151,7 @@ func TestDeleteProject(t *testing.T) { } var paging *v1.Paging - projectMemberStorageMock.On("Find", ctx, filter, paging).Return([]*v1.ProjectMember{}, nil, nil) + projectMemberStorageMock.On("Find", ctx, paging, filter).Return([]*v1.ProjectMember{}, nil, nil) storageMock.On("DeleteAll", ctx, p3.Meta.Id).Return(nil) storageMock.On("Delete", ctx, p3.Meta.Id).Return(nil) resp, err := ps.Delete(ctx, pdr) @@ -179,73 +185,264 @@ func TestGetProject(t *testing.T) { assert.Equal(t, tgr.Id, resp.GetProject().GetMeta().GetId()) } -func TestFindProjectByID(t *testing.T) { - storageMock := &mocks.Storage[*v1.Project]{} - tenantStorageMock := &mocks.Storage[*v1.Tenant]{} - ts := &projectService{ - projectStore: storageMock, - tenantStore: tenantStorageMock, - log: slog.Default(), - } - ctx := context.Background() - var t5s []*v1.Project - // filter by id - f1 := make(map[string]any) - tfr := &v1.ProjectFindRequest{ - Id: pointer.Pointer("p5"), +func TestFindProject(t *testing.T) { + ctx := t.Context() + ves := []datastore.Entity{ + &v1.Project{}, + &v1.ProjectMember{}, + &v1.Tenant{}, + &v1.TenantMember{}, } - f1["id"] = pointer.Pointer("p5") - storageMock.On("Find", ctx, f1, mock.AnythingOfType("*v1.Paging")).Return(t5s, nil, nil) - resp, err := ts.Find(ctx, tfr) + container, db, err := StartPostgres(ctx, ves...) require.NoError(t, err) - assert.NotNil(t, resp) -} - -func TestFindProjectByName(t *testing.T) { - storageMock := &mocks.Storage[*v1.Project]{} - tenantStorageMock := &mocks.Storage[*v1.Tenant]{} - ts := &projectService{ - projectStore: storageMock, - tenantStore: tenantStorageMock, - log: slog.Default(), - } - ctx := context.Background() + defer func() { + require.NoError(t, db.Close()) + require.NoError(t, container.Terminate(ctx)) + }() - // filter by name - var t6s []*v1.Project - tfr := &v1.ProjectFindRequest{ - Name: pointer.Pointer("Sixth"), - } + var ( + projectStore = datastore.New(log, db, &v1.Project{}) + testProject1 = &v1.Project{ + Meta: &v1.Meta{ + Id: "1", + Kind: "Project", + Apiversion: "v1", + Version: 1, + Annotations: map[string]string{ + "a": "b", + "c": "d", + }, + Labels: []string{"e", "f"}, + }, + Name: "project-1", + Description: "project 1", + TenantId: "tenant-1", + } + testProject2 = &v1.Project{ + Meta: &v1.Meta{ + Id: "2", + Kind: "Project", + Apiversion: "v1", + Version: 1, + Annotations: map[string]string{ + "c": "d", + "e": "f", + }, + Labels: []string{"f", "g", "h"}, + }, + Name: "project-2", + Description: "project 2", + TenantId: "tenant-2", + } - f2 := make(map[string]any) - f2["project ->> 'name'"] = pointer.Pointer("Sixth") - storageMock.On("Find", ctx, f2, mock.AnythingOfType("*v1.Paging")).Return(t6s, nil, nil) - resp, err := ts.Find(ctx, tfr) - require.NoError(t, err) - assert.NotNil(t, resp) -} + service = &projectService{ + projectStore: projectStore, + log: log, + } + ) -func TestFindProjectByTenant(t *testing.T) { - storageMock := &mocks.Storage[*v1.Project]{} - tenantStorageMock := &mocks.Storage[*v1.Tenant]{} - ts := &projectService{ - projectStore: storageMock, - tenantStore: tenantStorageMock, - log: slog.Default(), + tests := []struct { + name string + prepare func() + req *v1.ProjectFindRequest + want *v1.ProjectListResponse + wantErr error + }{ + { + name: "find by id", + req: &v1.ProjectFindRequest{ + Id: pointer.Pointer("1"), + }, + prepare: func() { + require.NoError(t, projectStore.Create(ctx, testProject1)) + require.NoError(t, projectStore.Create(ctx, testProject2)) + }, + want: &v1.ProjectListResponse{ + Projects: []*v1.Project{ + testProject1, + }, + }, + wantErr: nil, + }, + { + name: "find by id (no results)", + req: &v1.ProjectFindRequest{ + Id: pointer.Pointer("no-result"), + }, + prepare: func() { + require.NoError(t, projectStore.Create(ctx, testProject1)) + require.NoError(t, projectStore.Create(ctx, testProject2)) + }, + want: &v1.ProjectListResponse{ + Projects: nil, + }, + wantErr: nil, + }, + { + name: "find by name", + req: &v1.ProjectFindRequest{ + Name: pointer.Pointer("project-2"), + }, + prepare: func() { + require.NoError(t, projectStore.Create(ctx, testProject1)) + require.NoError(t, projectStore.Create(ctx, testProject2)) + }, + want: &v1.ProjectListResponse{ + Projects: []*v1.Project{ + testProject2, + }, + }, + wantErr: nil, + }, + { + name: "find by tenant", + req: &v1.ProjectFindRequest{ + TenantId: pointer.Pointer("tenant-2"), + }, + prepare: func() { + require.NoError(t, projectStore.Create(ctx, testProject1)) + require.NoError(t, projectStore.Create(ctx, testProject2)) + }, + want: &v1.ProjectListResponse{ + Projects: []*v1.Project{ + testProject2, + }, + }, + wantErr: nil, + }, + { + name: "find by annotation", + req: &v1.ProjectFindRequest{ + Annotations: map[string]string{ + "a": "b", + }, + }, + prepare: func() { + require.NoError(t, projectStore.Create(ctx, testProject1)) + require.NoError(t, projectStore.Create(ctx, testProject2)) + }, + want: &v1.ProjectListResponse{ + Projects: []*v1.Project{ + testProject1, + }, + }, + wantErr: nil, + }, + { + name: "find by annotation #2", + req: &v1.ProjectFindRequest{ + Annotations: map[string]string{ + "a": "b", + "c": "d", + }, + }, + prepare: func() { + require.NoError(t, projectStore.Create(ctx, testProject1)) + require.NoError(t, projectStore.Create(ctx, testProject2)) + }, + want: &v1.ProjectListResponse{ + Projects: []*v1.Project{ + testProject1, + }, + }, + wantErr: nil, + }, + { + name: "find by annotation #3", + req: &v1.ProjectFindRequest{ + Annotations: map[string]string{ + "c": "d", + }, + }, + prepare: func() { + require.NoError(t, projectStore.Create(ctx, testProject1)) + require.NoError(t, projectStore.Create(ctx, testProject2)) + }, + want: &v1.ProjectListResponse{ + Projects: []*v1.Project{ + testProject1, + testProject2, + }, + }, + wantErr: nil, + }, + { + name: "find by label", + req: &v1.ProjectFindRequest{ + Labels: []string{"e"}, + }, + prepare: func() { + require.NoError(t, projectStore.Create(ctx, testProject1)) + require.NoError(t, projectStore.Create(ctx, testProject2)) + }, + want: &v1.ProjectListResponse{ + Projects: []*v1.Project{ + testProject1, + }, + }, + wantErr: nil, + }, + { + name: "find by label #2", + req: &v1.ProjectFindRequest{ + Labels: []string{"e", "f"}, + }, + prepare: func() { + require.NoError(t, projectStore.Create(ctx, testProject1)) + require.NoError(t, projectStore.Create(ctx, testProject2)) + }, + want: &v1.ProjectListResponse{ + Projects: []*v1.Project{ + testProject1, + }, + }, + wantErr: nil, + }, + { + name: "find by label #3", + req: &v1.ProjectFindRequest{ + Labels: []string{"f"}, + }, + prepare: func() { + require.NoError(t, projectStore.Create(ctx, testProject1)) + require.NoError(t, projectStore.Create(ctx, testProject2)) + }, + want: &v1.ProjectListResponse{ + Projects: []*v1.Project{ + testProject1, + testProject2, + }, + }, + wantErr: nil, + }, } - ctx := context.Background() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for _, ve := range ves { + _, err := db.ExecContext(ctx, "TRUNCATE TABLE "+ve.TableName()) + require.NoError(t, err) + } - // filter by name - var t6s []*v1.Project - tfr := &v1.ProjectFindRequest{ - TenantId: pointer.Pointer("p1"), - } + if tt.prepare != nil { + tt.prepare() + } - f2 := make(map[string]any) - f2["project ->> 'tenant_id'"] = pointer.Pointer("p1") - storageMock.On("Find", ctx, f2, mock.AnythingOfType("*v1.Paging")).Return(t6s, nil, nil) - resp, err := ts.Find(ctx, tfr) - require.NoError(t, err) - assert.NotNil(t, resp) + got, err := service.Find(ctx, tt.req) + if diff := cmp.Diff(err, tt.wantErr); diff != "" { + t.Errorf("(-want +got):\n%s", diff) + return + } + slices.SortFunc(got.Projects, func(i, j *v1.Project) int { + if i.Meta.Id < j.Meta.Id { + return -1 + } else { + return 1 + } + }) + if diff := cmp.Diff(tt.want, got, cmpopts.IgnoreTypes(protoimpl.MessageState{}), cmpopts.IgnoreFields(v1.Meta{}, "CreatedTime"), testcommon.IgnoreUnexported()); diff != "" { + t.Errorf("(-want +got):\n%s", diff) + } + }) + } } diff --git a/pkg/service/projectmember.go b/pkg/service/projectmember.go index 2307dc7..4ba70fa 100644 --- a/pkg/service/projectmember.go +++ b/pkg/service/projectmember.go @@ -83,7 +83,7 @@ func (s *projectMemberService) Find(ctx context.Context, req *v1.ProjectMemberFi f := fmt.Sprintf("projectmember -> 'meta' -> 'annotations' ->> '%s'", key) filter[f] = value } - res, _, err := s.projectMemberStore.Find(ctx, filter, nil) + res, _, err := s.projectMemberStore.Find(ctx, nil, filter) if err != nil { return nil, err } diff --git a/pkg/service/projectmember_test.go b/pkg/service/projectmember_test.go index 05fb2dd..097cba9 100644 --- a/pkg/service/projectmember_test.go +++ b/pkg/service/projectmember_test.go @@ -153,7 +153,7 @@ func TestFindProjectMemberByProject(t *testing.T) { f2 := make(map[string]any) f2["projectmember ->> 'project_id'"] = pointer.Pointer("p1") - storageMock.On("Find", ctx, f2, mock.AnythingOfType("*v1.Paging")).Return(t6s, nil, nil) + storageMock.On("Find", ctx, mock.AnythingOfType("*v1.Paging"), f2).Return(t6s, nil, nil) resp, err := ts.Find(ctx, tfr) require.NoError(t, err) assert.NotNil(t, resp) @@ -179,7 +179,7 @@ func TestFindProjectMemberByTenant(t *testing.T) { f2 := make(map[string]any) f2["projectmember ->> 'tenant_id'"] = pointer.Pointer("t1") - storageMock.On("Find", ctx, f2, mock.AnythingOfType("*v1.Paging")).Return(t6s, nil, nil) + storageMock.On("Find", ctx, mock.AnythingOfType("*v1.Paging"), f2).Return(t6s, nil, nil) resp, err := ts.Find(ctx, tfr) require.NoError(t, err) assert.NotNil(t, resp) diff --git a/pkg/service/service.go b/pkg/service/service.go index d033a09..7c5d14f 100644 --- a/pkg/service/service.go +++ b/pkg/service/service.go @@ -58,8 +58,8 @@ func (s StorageStatusWrapper[E]) GetHistoryCreated(ctx context.Context, id strin return wrapGetStatusError(s.storage.GetHistoryCreated(ctx, id, ve)) } -func (s StorageStatusWrapper[E]) Find(ctx context.Context, filter map[string]any, paging *v1.Paging) ([]E, *uint64, error) { - return s.storage.Find(ctx, filter, paging) +func (s StorageStatusWrapper[E]) Find(ctx context.Context, paging *v1.Paging, filters ...any) ([]E, *uint64, error) { + return s.storage.Find(ctx, paging, filters...) } // wrapCreateStatusError wraps some errors in a grpc status error diff --git a/pkg/service/tenant.go b/pkg/service/tenant.go index 44630b8..95ecff0 100644 --- a/pkg/service/tenant.go +++ b/pkg/service/tenant.go @@ -5,6 +5,8 @@ import ( "encoding/json" "fmt" "log/slog" + "strconv" + "strings" sq "github.com/Masterminds/squirrel" "github.com/jmoiron/sqlx" @@ -58,11 +60,11 @@ func (s *tenantService) Delete(ctx context.Context, req *v1.TenantDeleteRequest) tenantIsMemberFilter := map[string]any{ "tenantmember ->> 'member_id'": tenant.Meta.Id, } - tenantIsHostMemberships, _, err := s.tenantMemberStore.Find(ctx, tenantIsHostFilter, nil) + tenantIsHostMemberships, _, err := s.tenantMemberStore.Find(ctx, nil, tenantIsHostFilter) if err != nil { return nil, err } - tenantIsMemberMemberships, _, err := s.tenantMemberStore.Find(ctx, tenantIsMemberFilter, nil) + tenantIsMemberMemberships, _, err := s.tenantMemberStore.Find(ctx, nil, tenantIsMemberFilter) if err != nil { return nil, err } @@ -125,19 +127,38 @@ func (s *tenantService) Find(ctx context.Context, req *v1.TenantFindRequest) (*v req.Name = &req.DeprecatedName.Value // nolint:staticcheck } - filter := make(map[string]any) + var filters []any + + mapFilter := make(map[string]any) if req.Id != nil { - filter["id"] = req.GetId() + mapFilter["id"] = req.GetId() } if req.Name != nil { - filter["tenant ->> 'name'"] = req.GetName() + mapFilter["tenant ->> 'name'"] = req.GetName() } for key, value := range req.Annotations { // select * from tenants where tenant -> 'meta' -> 'annotations' ->> 'metal-stack.io/admitted' = 'true'; f := fmt.Sprintf("tenant -> 'meta' -> 'annotations' ->> '%s'", key) - filter[f] = value + mapFilter[f] = value + } + + if len(mapFilter) > 0 { + filters = append(filters, mapFilter) + } + + if len(req.Labels) > 0 { + var contains []string + + for _, label := range req.Labels { + contains = append(contains, strconv.Quote(label)) + } + + // select * from tenants where tenant -> 'meta' -> 'labels' @> '["a=b","c=d"]'; + labelFilter := fmt.Sprintf(`tenant -> 'meta' -> 'labels' @> '[%s]'`, strings.Join(contains, ",")) + + filters = append(filters, labelFilter) } - res, nextPage, err := s.tenantStore.Find(ctx, filter, req.Paging) + res, nextPage, err := s.tenantStore.Find(ctx, req.Paging, filters...) if err != nil { return nil, err } diff --git a/pkg/service/tenant_test.go b/pkg/service/tenant_test.go index 423bec9..000f999 100644 --- a/pkg/service/tenant_test.go +++ b/pkg/service/tenant_test.go @@ -13,7 +13,6 @@ import ( "github.com/metal-stack/metal-lib/pkg/pointer" "github.com/metal-stack/metal-lib/pkg/testcommon" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "google.golang.org/protobuf/runtime/protoimpl" @@ -28,7 +27,7 @@ func TestMain(m *testing.M) { defer func() { os.Exit(code) }() - log = slog.Default() + log = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) code = m.Run() } @@ -112,7 +111,7 @@ func TestDeleteTenant(t *testing.T) { var paging *v1.Paging storageMock.On("Delete", ctx, t3.Meta.Id).Return(nil) - memberStorageMock.On("Find", ctx, tfilter, paging).Return([]*v1.TenantMember{ + memberStorageMock.On("Find", ctx, paging, tfilter).Return([]*v1.TenantMember{ { Meta: &v1.Meta{ Id: "t3", @@ -121,7 +120,7 @@ func TestDeleteTenant(t *testing.T) { MemberId: t3.Meta.Id, }, }, nil, nil) - memberStorageMock.On("Find", ctx, mfilter, paging).Return([]*v1.TenantMember{ + memberStorageMock.On("Find", ctx, paging, mfilter).Return([]*v1.TenantMember{ { Meta: &v1.Meta{ Id: "t3", @@ -160,47 +159,250 @@ func TestGetTenant(t *testing.T) { assert.Equal(t, tgr.Id, resp.GetTenant().GetMeta().GetId()) } -func TestFindTenantByID(t *testing.T) { - storageMock := &mocks.Storage[*v1.Tenant]{} - ts := &tenantService{ - tenantStore: storageMock, - log: slog.Default(), - } - ctx := context.Background() - var t5s []*v1.Tenant - // filter by id - f1 := make(map[string]any) - tfr := &v1.TenantFindRequest{ - Id: pointer.Pointer("t5"), +func TestFindTenant(t *testing.T) { + ctx := t.Context() + ves := []datastore.Entity{ + &v1.Project{}, + &v1.ProjectMember{}, + &v1.Tenant{}, + &v1.TenantMember{}, } - f1["id"] = "t5" - storageMock.On("Find", ctx, f1, mock.AnythingOfType("*v1.Paging")).Return(t5s, nil, nil) - resp, err := ts.Find(ctx, tfr) + container, db, err := StartPostgres(ctx, ves...) require.NoError(t, err) - assert.NotNil(t, resp) -} + defer func() { + require.NoError(t, db.Close()) + require.NoError(t, container.Terminate(ctx)) + }() -func TestFindTenantByName(t *testing.T) { - storageMock := &mocks.Storage[*v1.Tenant]{} - ts := &tenantService{ - tenantStore: storageMock, - log: slog.Default(), - } - ctx := context.Background() + var ( + tenantStore = datastore.New(log, db, &v1.Tenant{}) + testTenant1 = &v1.Tenant{ + Meta: &v1.Meta{ + Id: "1", + Kind: "Tenant", - // filter by name - var t6s []*v1.Tenant - tfr := &v1.TenantFindRequest{ - Name: pointer.Pointer("Fifth"), + Apiversion: "v1", + Version: 1, + Annotations: map[string]string{ + "a": "b", + "c": "d", + }, + Labels: []string{"e", "f"}, + }, + Name: "tenant-1", + Description: "tenant 1", + } + testTenant2 = &v1.Tenant{ + Meta: &v1.Meta{ + Id: "2", + Kind: "Tenant", + Apiversion: "v1", + Version: 1, + Annotations: map[string]string{ + "c": "d", + "e": "f", + }, + Labels: []string{"f", "g", "h"}, + }, + Name: "tenant-2", + Description: "tenant 2", + } + + service = &tenantService{ + db: db, + tenantStore: tenantStore, + log: log, + } + ) + + tests := []struct { + name string + prepare func() + req *v1.TenantFindRequest + want *v1.TenantListResponse + wantErr error + }{ + { + name: "find by id", + req: &v1.TenantFindRequest{ + Id: pointer.Pointer("1"), + }, + prepare: func() { + require.NoError(t, tenantStore.Create(ctx, testTenant1)) + require.NoError(t, tenantStore.Create(ctx, testTenant2)) + }, + want: &v1.TenantListResponse{ + Tenants: []*v1.Tenant{ + testTenant1, + }, + }, + wantErr: nil, + }, + { + name: "find by id (no results)", + req: &v1.TenantFindRequest{ + Id: pointer.Pointer("no-result"), + }, + prepare: func() { + require.NoError(t, tenantStore.Create(ctx, testTenant1)) + require.NoError(t, tenantStore.Create(ctx, testTenant2)) + }, + want: &v1.TenantListResponse{ + Tenants: nil, + }, + wantErr: nil, + }, + { + name: "find by name", + req: &v1.TenantFindRequest{ + Name: pointer.Pointer("tenant-2"), + }, + prepare: func() { + require.NoError(t, tenantStore.Create(ctx, testTenant1)) + require.NoError(t, tenantStore.Create(ctx, testTenant2)) + }, + want: &v1.TenantListResponse{ + Tenants: []*v1.Tenant{ + testTenant2, + }, + }, + wantErr: nil, + }, + { + name: "find by annotation", + req: &v1.TenantFindRequest{ + Annotations: map[string]string{ + "a": "b", + }, + }, + prepare: func() { + require.NoError(t, tenantStore.Create(ctx, testTenant1)) + require.NoError(t, tenantStore.Create(ctx, testTenant2)) + }, + want: &v1.TenantListResponse{ + Tenants: []*v1.Tenant{ + testTenant1, + }, + }, + wantErr: nil, + }, + { + name: "find by annotation #2", + req: &v1.TenantFindRequest{ + Annotations: map[string]string{ + "a": "b", + "c": "d", + }, + }, + prepare: func() { + require.NoError(t, tenantStore.Create(ctx, testTenant1)) + require.NoError(t, tenantStore.Create(ctx, testTenant2)) + }, + want: &v1.TenantListResponse{ + Tenants: []*v1.Tenant{ + testTenant1, + }, + }, + wantErr: nil, + }, + { + name: "find by annotation #3", + req: &v1.TenantFindRequest{ + Annotations: map[string]string{ + "c": "d", + }, + }, + prepare: func() { + require.NoError(t, tenantStore.Create(ctx, testTenant1)) + require.NoError(t, tenantStore.Create(ctx, testTenant2)) + }, + want: &v1.TenantListResponse{ + Tenants: []*v1.Tenant{ + testTenant1, + testTenant2, + }, + }, + wantErr: nil, + }, + { + name: "find by label", + req: &v1.TenantFindRequest{ + Labels: []string{"e"}, + }, + prepare: func() { + require.NoError(t, tenantStore.Create(ctx, testTenant1)) + require.NoError(t, tenantStore.Create(ctx, testTenant2)) + }, + want: &v1.TenantListResponse{ + Tenants: []*v1.Tenant{ + testTenant1, + }, + }, + wantErr: nil, + }, + { + name: "find by label #2", + req: &v1.TenantFindRequest{ + Labels: []string{"e", "f"}, + }, + prepare: func() { + require.NoError(t, tenantStore.Create(ctx, testTenant1)) + require.NoError(t, tenantStore.Create(ctx, testTenant2)) + }, + want: &v1.TenantListResponse{ + Tenants: []*v1.Tenant{ + testTenant1, + }, + }, + wantErr: nil, + }, + { + name: "find by label #3", + req: &v1.TenantFindRequest{ + Labels: []string{"f"}, + }, + prepare: func() { + require.NoError(t, tenantStore.Create(ctx, testTenant1)) + require.NoError(t, tenantStore.Create(ctx, testTenant2)) + }, + want: &v1.TenantListResponse{ + Tenants: []*v1.Tenant{ + testTenant1, + testTenant2, + }, + }, + wantErr: nil, + }, } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for _, ve := range ves { + _, err := db.ExecContext(ctx, "TRUNCATE TABLE "+ve.TableName()) + require.NoError(t, err) + } - f2 := make(map[string]any) - f2["tenant ->> 'name'"] = "Fifth" - storageMock.On("Find", ctx, f2, mock.AnythingOfType("*v1.Paging")).Return(t6s, nil, nil) - resp, err := ts.Find(ctx, tfr) - require.NoError(t, err) - assert.NotNil(t, resp) + if tt.prepare != nil { + tt.prepare() + } + + got, err := service.Find(ctx, tt.req) + if diff := cmp.Diff(err, tt.wantErr); diff != "" { + t.Errorf("(-want +got):\n%s", diff) + return + } + slices.SortFunc(got.Tenants, func(i, j *v1.Tenant) int { + if i.Meta.Id < j.Meta.Id { + return -1 + } else { + return 1 + } + }) + if diff := cmp.Diff(tt.want, got, cmpopts.IgnoreTypes(protoimpl.MessageState{}), cmpopts.IgnoreFields(v1.Meta{}, "CreatedTime"), testcommon.IgnoreUnexported()); diff != "" { + t.Errorf("(-want +got):\n%s", diff) + } + }) + } } func Test_tenantService_FindParticipatingProjects(t *testing.T) { diff --git a/pkg/service/tenantmember.go b/pkg/service/tenantmember.go index c61e864..6eae834 100644 --- a/pkg/service/tenantmember.go +++ b/pkg/service/tenantmember.go @@ -81,7 +81,7 @@ func (s *tenantMemberService) Find(ctx context.Context, req *v1.TenantMemberFind f := fmt.Sprintf("tenantmember -> 'meta' -> 'annotations' ->> '%s'", key) filter[f] = value } - res, _, err := s.tenantMemberStore.Find(ctx, filter, nil) + res, _, err := s.tenantMemberStore.Find(ctx, nil, filter) if err != nil { return nil, err } diff --git a/pkg/service/tenantmember_test.go b/pkg/service/tenantmember_test.go index cbe5a8a..488d261 100644 --- a/pkg/service/tenantmember_test.go +++ b/pkg/service/tenantmember_test.go @@ -143,7 +143,7 @@ func TestFindTenantMemberByTenant(t *testing.T) { f2 := make(map[string]any) f2["tenantmember ->> 'tenant_id'"] = pointer.Pointer("p1") - storageMock.On("Find", ctx, f2, mock.AnythingOfType("*v1.Paging")).Return(t6s, nil, nil) + storageMock.On("Find", ctx, mock.AnythingOfType("*v1.Paging"), f2).Return(t6s, nil, nil) resp, err := ts.Find(ctx, tfr) require.NoError(t, err) assert.NotNil(t, resp) @@ -167,7 +167,7 @@ func TestFindTenantMemberByMember(t *testing.T) { f2 := make(map[string]any) f2["tenantmember ->> 'member_id'"] = pointer.Pointer("t1") - storageMock.On("Find", ctx, f2, mock.AnythingOfType("*v1.Paging")).Return(t6s, nil, nil) + storageMock.On("Find", ctx, mock.AnythingOfType("*v1.Paging"), f2).Return(t6s, nil, nil) resp, err := ts.Find(ctx, tfr) require.NoError(t, err) assert.NotNil(t, resp) diff --git a/proto/v1/project.proto b/proto/v1/project.proto index 8370cba..ccbd73d 100644 --- a/proto/v1/project.proto +++ b/proto/v1/project.proto @@ -61,6 +61,7 @@ message ProjectFindRequest { optional string name = 8; optional string description = 9; optional string tenant_id = 10; + repeated string labels = 11; } message ProjectResponse { diff --git a/proto/v1/tenant.proto b/proto/v1/tenant.proto index 167b9c6..b1f808c 100644 --- a/proto/v1/tenant.proto +++ b/proto/v1/tenant.proto @@ -103,6 +103,7 @@ message TenantFindRequest { optional string id = 5; optional string name = 6; + repeated string labels = 7; } message TenantResponse {