From 5c167982fd7d3be1826cf92dab519c5bed4470bb Mon Sep 17 00:00:00 2001 From: Imre Halasz Date: Thu, 21 Nov 2024 14:56:47 +0100 Subject: [PATCH] ns: Implement MAC settings profile List endpoint --- .../grpc_mac_settings_profile.go | 16 +- .../grpc_mac_settings_profile_test.go | 228 ++++++++++++++++++ .../networkserver_util_internal_test.go | 16 ++ .../redis/mac_settings_profile.go | 78 ++++-- .../redis/mac_settings_profile_test.go | 21 ++ pkg/networkserver/registry.go | 1 + 6 files changed, 340 insertions(+), 20 deletions(-) diff --git a/pkg/networkserver/grpc_mac_settings_profile.go b/pkg/networkserver/grpc_mac_settings_profile.go index cc45e1286c1..9945bb17411 100644 --- a/pkg/networkserver/grpc_mac_settings_profile.go +++ b/pkg/networkserver/grpc_mac_settings_profile.go @@ -147,13 +147,25 @@ func (m *NsMACSettingsProfileRegistry) Delete(ctx context.Context, req *ttnpb.De } // List lists the MAC settings profiles. -func (*NsMACSettingsProfileRegistry) List(ctx context.Context, req *ttnpb.ListMACSettingsProfilesRequest, +func (m *NsMACSettingsProfileRegistry) List(ctx context.Context, req *ttnpb.ListMACSettingsProfilesRequest, ) (*ttnpb.ListMACSettingsProfilesResponse, error) { if err := rights.RequireApplication( ctx, req.ApplicationIds, ttnpb.Right_RIGHT_APPLICATION_DEVICES_READ, ); err != nil { return nil, err } + paths := []string{"ids", "mac_settings"} + if req.FieldMask != nil { + paths = req.FieldMask.GetPaths() + } + profiles, err := m.registry.List(ctx, req.ApplicationIds, paths) + if err != nil { + logRegistryRPCError(ctx, err, "Failed to list MAC settings profiles") + return nil, err + } - return &ttnpb.ListMACSettingsProfilesResponse{}, nil + return &ttnpb.ListMACSettingsProfilesResponse{ + MacSettingsProfiles: profiles, + TotalCount: uint32(len(profiles)), // nolint: gosec + }, nil } diff --git a/pkg/networkserver/grpc_mac_settings_profile_test.go b/pkg/networkserver/grpc_mac_settings_profile_test.go index fa1a934b5a9..5771d056c49 100644 --- a/pkg/networkserver/grpc_mac_settings_profile_test.go +++ b/pkg/networkserver/grpc_mac_settings_profile_test.go @@ -944,3 +944,231 @@ func TestMACSettingsProfileRegistryDelete(t *testing.T) { }) } } + +func TestMACSettingsProfileRegistryList(t *testing.T) { + t.Parallel() + nilProfileAssertion := func(t *testing.T, profile *ttnpb.ListMACSettingsProfilesResponse) bool { + t.Helper() + return assertions.New(t).So(profile, should.BeNil) + } + nilErrorAssertion := func(t *testing.T, err error) bool { + t.Helper() + return assertions.New(t).So(err, should.BeNil) + } + permissionDeniedErrorAssertion := func(t *testing.T, err error) bool { + t.Helper() + return assertions.New(t).So(errors.IsPermissionDenied(err), should.BeTrue) + } + notFoundErrorAssertion := func(t *testing.T, err error) bool { + t.Helper() + return assertions.New(t).So(errors.IsNotFound(err), should.BeTrue) + } + + registeredProfileIDs := &ttnpb.MACSettingsProfileIdentifiers{ + ApplicationIds: &ttnpb.ApplicationIdentifiers{ApplicationId: "test-app-id"}, + ProfileId: "test-profile-id", + } + + for _, tc := range []struct { + Name string + ContextFunc func(context.Context) context.Context + ListFunc func(context.Context, *ttnpb.ApplicationIdentifiers, []string) ([]*ttnpb.MACSettingsProfile, error) // nolint: lll + ProfileRequest *ttnpb.ListMACSettingsProfilesRequest + ProfileAssertion func(*testing.T, *ttnpb.ListMACSettingsProfilesResponse) bool + ErrorAssertion func(*testing.T, error) bool + ListCalls uint64 + }{ + { + Name: "Permission denied", + ContextFunc: func(ctx context.Context) context.Context { + return rights.NewContext(ctx, &rights.Rights{ + ApplicationRights: *rights.NewMap(map[string]*ttnpb.Rights{ + unique.ID(test.Context(), &ttnpb.ApplicationIdentifiers{ApplicationId: "test-app-id"}): nil, + }), + }) + }, + ListFunc: func( + ctx context.Context, + _ *ttnpb.ApplicationIdentifiers, + _ []string, + ) ([]*ttnpb.MACSettingsProfile, error) { + err := errors.New("ListFunc must not be called") + test.MustTFromContext(ctx).Error(err) + return nil, err + }, + ProfileRequest: &ttnpb.ListMACSettingsProfilesRequest{ + ApplicationIds: registeredProfileIDs.ApplicationIds, + FieldMask: ttnpb.FieldMask("mac_settings"), + }, + ProfileAssertion: nilProfileAssertion, + ErrorAssertion: permissionDeniedErrorAssertion, + ListCalls: 0, + }, + { + Name: "Invalid application ID", + ContextFunc: func(ctx context.Context) context.Context { + return rights.NewContext(ctx, &rights.Rights{ + ApplicationRights: *rights.NewMap(map[string]*ttnpb.Rights{ + unique.ID(test.Context(), &ttnpb.ApplicationIdentifiers{ + ApplicationId: "invalid-application", + }): ttnpb.RightsFrom( + ttnpb.Right_RIGHT_APPLICATION_DEVICES_READ, + ), + }), + }) + }, + ListFunc: func( + ctx context.Context, + _ *ttnpb.ApplicationIdentifiers, + _ []string, + ) ([]*ttnpb.MACSettingsProfile, error) { + err := errors.New("ListFunc must not be called") + test.MustTFromContext(ctx).Error(err) + return nil, err + }, + ProfileRequest: &ttnpb.ListMACSettingsProfilesRequest{ + ApplicationIds: registeredProfileIDs.ApplicationIds, + FieldMask: ttnpb.FieldMask("mac_settings"), + }, + ProfileAssertion: nilProfileAssertion, + ErrorAssertion: permissionDeniedErrorAssertion, + ListCalls: 0, + }, + { + Name: "Not found", + ContextFunc: func(ctx context.Context) context.Context { + return rights.NewContext(ctx, &rights.Rights{ + ApplicationRights: *rights.NewMap(map[string]*ttnpb.Rights{ + unique.ID(test.Context(), &ttnpb.ApplicationIdentifiers{ + ApplicationId: "test-app-id", + }): ttnpb.RightsFrom( + ttnpb.Right_RIGHT_APPLICATION_DEVICES_READ, + ), + }), + }) + }, + ListFunc: func( + ctx context.Context, + ids *ttnpb.ApplicationIdentifiers, + paths []string, + ) ([]*ttnpb.MACSettingsProfile, error) { + a := assertions.New(test.MustTFromContext(ctx)) + a.So(ids, should.Resemble, ids) + a.So(paths, should.HaveSameElementsDeep, []string{ + "mac_settings", + }) + return nil, errNotFound.New() + }, + ProfileRequest: &ttnpb.ListMACSettingsProfilesRequest{ + ApplicationIds: registeredProfileIDs.ApplicationIds, + FieldMask: ttnpb.FieldMask("mac_settings"), + }, + ProfileAssertion: nilProfileAssertion, + ErrorAssertion: notFoundErrorAssertion, + ListCalls: 1, + }, + { + Name: "Found", + ContextFunc: func(ctx context.Context) context.Context { + return rights.NewContext(ctx, &rights.Rights{ + ApplicationRights: *rights.NewMap(map[string]*ttnpb.Rights{ + unique.ID(test.Context(), &ttnpb.ApplicationIdentifiers{ + ApplicationId: "test-app-id", + }): ttnpb.RightsFrom( + ttnpb.Right_RIGHT_APPLICATION_DEVICES_READ, + ), + }), + }) + }, + ListFunc: func( + ctx context.Context, + ids *ttnpb.ApplicationIdentifiers, + paths []string, + ) ([]*ttnpb.MACSettingsProfile, error) { + a := assertions.New(test.MustTFromContext(ctx)) + a.So(ids, should.Resemble, ids) + a.So(paths, should.HaveSameElementsDeep, []string{ + "ids", + "mac_settings", + }) + return []*ttnpb.MACSettingsProfile{ttnpb.Clone(&ttnpb.MACSettingsProfile{ + Ids: registeredProfileIDs, + MacSettings: &ttnpb.MACSettings{ + ResetsFCnt: &ttnpb.BoolValue{Value: true}, + }, + })}, nil + }, + ProfileRequest: &ttnpb.ListMACSettingsProfilesRequest{ + ApplicationIds: registeredProfileIDs.ApplicationIds, + FieldMask: ttnpb.FieldMask("ids", "mac_settings"), + }, + ProfileAssertion: func(t *testing.T, profile *ttnpb.ListMACSettingsProfilesResponse) bool { + t.Helper() + a := assertions.New(t) + a.So(profile, should.NotBeNil) + a.So(profile.MacSettingsProfiles, should.HaveLength, 1) + a.So(profile.TotalCount, should.Equal, 1) + return a.So(profile.MacSettingsProfiles, should.Resemble, []*ttnpb.MACSettingsProfile{{ + Ids: &ttnpb.MACSettingsProfileIdentifiers{ + ApplicationIds: &ttnpb.ApplicationIdentifiers{ + ApplicationId: "test-app-id", + }, + ProfileId: "test-profile-id", + }, + MacSettings: &ttnpb.MACSettings{ + ResetsFCnt: &ttnpb.BoolValue{Value: true}, + }, + }}) + }, + ErrorAssertion: nilErrorAssertion, + ListCalls: 1, + }, + } { + tc := tc + test.RunSubtest(t, test.SubtestConfig{ + Name: tc.Name, + Parallel: true, + Func: func(ctx context.Context, t *testing.T, a *assertions.Assertion) { + t.Helper() + var listCalls uint64 + + ns, ctx, _, stop := StartTest( + ctx, + TestConfig{ + NetworkServer: Config{ + MACSettingsProfileRegistry: &MockMACSettingsProfileRegistry{ + ListFunc: func( + ctx context.Context, + ids *ttnpb.ApplicationIdentifiers, + paths []string, + ) ([]*ttnpb.MACSettingsProfile, error) { + atomic.AddUint64(&listCalls, 1) + return tc.ListFunc(ctx, ids, paths) + }, + }, + }, + TaskStarter: StartTaskExclude( + DownlinkProcessTaskName, + DownlinkDispatchTaskName, + ), + }, + ) + defer stop() + + ns.AddContextFiller(tc.ContextFunc) + ns.AddContextFiller(func(ctx context.Context) context.Context { + return test.ContextWithTB(ctx, t) + }) + + req := ttnpb.Clone(tc.ProfileRequest) + + profile, err := ttnpb.NewNsMACSettingsProfileRegistryClient(ns.LoopbackConn()).List(ctx, req) + if a.So(tc.ErrorAssertion(t, err), should.BeTrue) { + a.So(tc.ProfileAssertion(t, profile), should.BeTrue) + } + a.So(req, should.Resemble, tc.ProfileRequest) + a.So(listCalls, should.Equal, tc.ListCalls) + }, + }) + } +} diff --git a/pkg/networkserver/networkserver_util_internal_test.go b/pkg/networkserver/networkserver_util_internal_test.go index e73a50c3194..dce552dc05b 100644 --- a/pkg/networkserver/networkserver_util_internal_test.go +++ b/pkg/networkserver/networkserver_util_internal_test.go @@ -2609,6 +2609,11 @@ type MockMACSettingsProfileRegistry struct { paths []string, f func(context.Context, *ttnpb.MACSettingsProfile) (*ttnpb.MACSettingsProfile, []string, error), ) (*ttnpb.MACSettingsProfile, error) + ListFunc func( + ctx context.Context, + ids *ttnpb.ApplicationIdentifiers, + paths []string, + ) ([]*ttnpb.MACSettingsProfile, error) } func (m MockMACSettingsProfileRegistry) Get( @@ -2633,3 +2638,14 @@ func (m MockMACSettingsProfileRegistry) Set( } return m.SetFunc(ctx, ids, paths, f) } + +func (m MockMACSettingsProfileRegistry) List( + ctx context.Context, + ids *ttnpb.ApplicationIdentifiers, + paths []string, +) ([]*ttnpb.MACSettingsProfile, error) { + if m.ListFunc == nil { + panic("ListFunc not set") + } + return m.ListFunc(ctx, ids, paths) +} diff --git a/pkg/networkserver/redis/mac_settings_profile.go b/pkg/networkserver/redis/mac_settings_profile.go index 6e2e45c0d71..a43a4f092b0 100644 --- a/pkg/networkserver/redis/mac_settings_profile.go +++ b/pkg/networkserver/redis/mac_settings_profile.go @@ -17,7 +17,6 @@ package redis import ( "context" - "fmt" "runtime/trace" "github.com/redis/go-redis/v9" @@ -25,6 +24,8 @@ import ( "go.thethings.network/lorawan-stack/v3/pkg/networkserver/internal/time" ttnredis "go.thethings.network/lorawan-stack/v3/pkg/redis" "go.thethings.network/lorawan-stack/v3/pkg/ttnpb" + "go.thethings.network/lorawan-stack/v3/pkg/unique" + "google.golang.org/protobuf/proto" ) // MACSettingsProfileRegistry is an implementation of networkserver.MACSettingsProfileRegistry. @@ -41,15 +42,18 @@ func applyMACSettingsProfileFieldMask(dst, src *ttnpb.MACSettingsProfile, paths return dst, dst.SetFields(src, paths...) } -func uniqueID(ids *ttnpb.MACSettingsProfileIdentifiers) string { - if ids == nil { - return "" - } - return fmt.Sprintf("%s.%s", ids.GetApplicationIds().IDString(), ids.GetProfileId()) +func (r *MACSettingsProfileRegistry) appKey(uid string) string { + return r.Redis.Key("uid", uid) } -func (r *MACSettingsProfileRegistry) uidKey(uid string) string { - return r.Redis.Key("uid", uid) +func (r *MACSettingsProfileRegistry) profileKey(appUID string, id string) string { + return r.Redis.Key("uid", appUID, id) +} + +func (r *MACSettingsProfileRegistry) makeProfileKeyFunc(appUID string) func(string) string { + return func(id string) string { + return r.profileKey(appUID, id) + } } // Init initializes the MAC settings profile registry. @@ -63,15 +67,15 @@ func (r *MACSettingsProfileRegistry) Get( ids *ttnpb.MACSettingsProfileIdentifiers, paths []string, ) (*ttnpb.MACSettingsProfile, error) { - defer trace.StartRegion(ctx, "get mac settings profile by id").End() + defer trace.StartRegion(ctx, "get mac settings profile").End() if err := ids.ValidateContext(ctx); err != nil { return nil, err } pb := &ttnpb.MACSettingsProfile{} - uid := uniqueID(ids) - if err := ttnredis.GetProto(ctx, r.Redis, r.uidKey(uid)).ScanProto(pb); err != nil { + appUID := unique.ID(ctx, ids.ApplicationIds) + if err := ttnredis.GetProto(ctx, r.Redis, r.profileKey(appUID, ids.ProfileId)).ScanProto(pb); err != nil { return nil, err } pb, err := applyMACSettingsProfileFieldMask(nil, pb, paths...) @@ -88,14 +92,14 @@ func (r *MACSettingsProfileRegistry) Set( //nolint:gocyclo paths []string, f func(context.Context, *ttnpb.MACSettingsProfile) (*ttnpb.MACSettingsProfile, []string, error), ) (*ttnpb.MACSettingsProfile, error) { - defer trace.StartRegion(ctx, "set mac settings profile by id").End() + defer trace.StartRegion(ctx, "set mac settings profile").End() if err := ids.ValidateContext(ctx); err != nil { return nil, err } - uid := uniqueID(ids) - uk := r.uidKey(uid) + appUID := unique.ID(ctx, ids.ApplicationIds) + pk := r.profileKey(appUID, ids.ProfileId) lockerID, err := ttnredis.GenerateLockerID() if err != nil { @@ -103,8 +107,8 @@ func (r *MACSettingsProfileRegistry) Set( //nolint:gocyclo } var pb *ttnpb.MACSettingsProfile - err = ttnredis.LockedWatch(ctx, r.Redis, uk, lockerID, r.LockTTL, func(tx *redis.Tx) error { - cmd := ttnredis.GetProto(ctx, tx, uk) + err = ttnredis.LockedWatch(ctx, r.Redis, pk, lockerID, r.LockTTL, func(tx *redis.Tx) error { + cmd := ttnredis.GetProto(ctx, tx, pk) stored := &ttnpb.MACSettingsProfile{} if err := cmd.ScanProto(stored); errors.IsNotFound(err) { stored = nil @@ -140,7 +144,8 @@ func (r *MACSettingsProfileRegistry) Set( //nolint:gocyclo var pipelined func(redis.Pipeliner) error if pb == nil && len(sets) == 0 { pipelined = func(p redis.Pipeliner) error { - p.Del(ctx, uk) + p.Del(ctx, pk) + p.SRem(ctx, r.appKey(appUID), stored.Ids.ProfileId) return nil } } else { @@ -187,9 +192,10 @@ func (r *MACSettingsProfileRegistry) Set( //nolint:gocyclo return err } pipelined = func(p redis.Pipeliner) error { - if _, err := ttnredis.SetProto(ctx, p, uk, updated, 0); err != nil { + if _, err := ttnredis.SetProto(ctx, p, pk, updated, 0); err != nil { return err } + p.SAdd(ctx, r.appKey(appUID), updated.Ids.ProfileId) return nil } pb, err = applyMACSettingsProfileFieldMask(nil, updated, paths...) @@ -209,3 +215,39 @@ func (r *MACSettingsProfileRegistry) Set( //nolint:gocyclo return pb, nil } + +// List lists MAC settings profiles by application identifiers. +func (r *MACSettingsProfileRegistry) List( + ctx context.Context, + ids *ttnpb.ApplicationIdentifiers, + paths []string, +) ([]*ttnpb.MACSettingsProfile, error) { + defer trace.StartRegion(ctx, "list mac settings profile").End() + + if err := ids.ValidateContext(ctx); err != nil { + return nil, err + } + + appUID := unique.ID(ctx, ids) + var pbs []*ttnpb.MACSettingsProfile + err := ttnredis.FindProtos( + ctx, + r.Redis, + r.appKey(appUID), + r.makeProfileKeyFunc(appUID), + ).Range(func() (proto.Message, func() (bool, error)) { + pb := &ttnpb.MACSettingsProfile{} + return pb, func() (bool, error) { + pb, err := applyMACSettingsProfileFieldMask(nil, pb, paths...) + if err != nil { + return false, err + } + pbs = append(pbs, pb) + return true, nil + } + }) + if err != nil { + return nil, err + } + return pbs, nil +} diff --git a/pkg/networkserver/redis/mac_settings_profile_test.go b/pkg/networkserver/redis/mac_settings_profile_test.go index c65058929c9..baf1828a5a4 100644 --- a/pkg/networkserver/redis/mac_settings_profile_test.go +++ b/pkg/networkserver/redis/mac_settings_profile_test.go @@ -143,4 +143,25 @@ func TestMACSettingsProfileRegistry(t *testing.T) { a.So(err, should.BeNil) a.So(deleted, should.BeNil) }) + + t.Run("List", func(t *testing.T) { + t.Parallel() + a, ctx := test.New(t) + profile, err := registry.Set(ctx, ids, []string{"ids", "mac_settings"}, createProfileFunc) + a.So(err, should.BeNil) + a.So(profile, should.NotBeNil) + a.So(profile.Ids, should.Resemble, ids) + a.So(profile.MacSettings, should.NotBeNil) + a.So(profile.MacSettings.ResetsFCnt, should.NotBeNil) + a.So(profile.MacSettings.ResetsFCnt.Value, should.BeTrue) + + profiles, err := registry.List(ctx, ids.ApplicationIds, []string{"ids", "mac_settings"}) + a.So(err, should.BeNil) + a.So(profiles, should.HaveLength, 1) + a.So(profiles[0], should.NotBeNil) + a.So(profiles[0].Ids, should.Resemble, ids) + a.So(profiles[0].MacSettings, should.NotBeNil) + a.So(profiles[0].MacSettings.ResetsFCnt, should.NotBeNil) + a.So(profiles[0].MacSettings.ResetsFCnt.Value, should.BeTrue) + }) } diff --git a/pkg/networkserver/registry.go b/pkg/networkserver/registry.go index c6257b334fe..9c81df9072d 100644 --- a/pkg/networkserver/registry.go +++ b/pkg/networkserver/registry.go @@ -304,4 +304,5 @@ type ScheduledDownlinkMatcher interface { type MACSettingsProfileRegistry interface { Get(ctx context.Context, ids *ttnpb.MACSettingsProfileIdentifiers, paths []string) (*ttnpb.MACSettingsProfile, error) // nolint: lll Set(ctx context.Context, ids *ttnpb.MACSettingsProfileIdentifiers, paths []string, f func(context.Context, *ttnpb.MACSettingsProfile) (*ttnpb.MACSettingsProfile, []string, error)) (*ttnpb.MACSettingsProfile, error) // nolint: lll + List(ctx context.Context, ids *ttnpb.ApplicationIdentifiers, paths []string) ([]*ttnpb.MACSettingsProfile, error) // nolint: lll }