VAULT-14735: generate mock clients for activity log (#20252)

* first part of segment client generation

* fix imports

* initial pr fixes

* refactor and fix

* update comments

* assign client type
This commit is contained in:
miagilepner 2023-05-23 11:58:51 +02:00 committed by GitHub
parent 4f77524ad4
commit bff8931640
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 324 additions and 47 deletions

View File

@ -439,12 +439,11 @@ type Client struct {
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Count int32 `protobuf:"varint,2,opt,name=count,proto3" json:"count,omitempty"` Count int32 `protobuf:"varint,2,opt,name=count,proto3" json:"count,omitempty"`
TimesSeen int32 `protobuf:"varint,3,opt,name=times_seen,json=timesSeen,proto3" json:"times_seen,omitempty"` Repeated bool `protobuf:"varint,3,opt,name=repeated,proto3" json:"repeated,omitempty"`
Repeated bool `protobuf:"varint,4,opt,name=repeated,proto3" json:"repeated,omitempty"` RepeatedFromMonth int32 `protobuf:"varint,4,opt,name=repeated_from_month,json=repeatedFromMonth,proto3" json:"repeated_from_month,omitempty"`
RepeatedFromMonth int32 `protobuf:"varint,5,opt,name=repeated_from_month,json=repeatedFromMonth,proto3" json:"repeated_from_month,omitempty"` Namespace string `protobuf:"bytes,5,opt,name=namespace,proto3" json:"namespace,omitempty"`
Namespace string `protobuf:"bytes,6,opt,name=namespace,proto3" json:"namespace,omitempty"` Mount string `protobuf:"bytes,6,opt,name=mount,proto3" json:"mount,omitempty"`
Mount string `protobuf:"bytes,7,opt,name=mount,proto3" json:"mount,omitempty"` NonEntity bool `protobuf:"varint,7,opt,name=non_entity,json=nonEntity,proto3" json:"non_entity,omitempty"`
NonEntity bool `protobuf:"varint,8,opt,name=non_entity,json=nonEntity,proto3" json:"non_entity,omitempty"`
} }
func (x *Client) Reset() { func (x *Client) Reset() {
@ -493,13 +492,6 @@ func (x *Client) GetCount() int32 {
return 0 return 0
} }
func (x *Client) GetTimesSeen() int32 {
if x != nil {
return x.TimesSeen
}
return 0
}
func (x *Client) GetRepeated() bool { func (x *Client) GetRepeated() bool {
if x != nil { if x != nil {
return x.Repeated return x.Repeated
@ -584,36 +576,34 @@ var file_vault_activity_generation_generate_data_proto_rawDesc = []byte{
0x74, 0x73, 0x12, 0x2c, 0x0a, 0x07, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x74, 0x73, 0x12, 0x2c, 0x0a, 0x07, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20,
0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e,
0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x07, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x07, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73,
0x22, 0xec, 0x01, 0x0a, 0x06, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x22, 0xcd, 0x01, 0x0a, 0x06, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69,
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x63,
0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e,
0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x18, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x70, 0x65, 0x61, 0x74, 0x65, 0x64, 0x18, 0x03, 0x20,
0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x53, 0x65, 0x65, 0x6e, 0x01, 0x28, 0x08, 0x52, 0x08, 0x72, 0x65, 0x70, 0x65, 0x61, 0x74, 0x65, 0x64, 0x12, 0x2e, 0x0a,
0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x70, 0x65, 0x61, 0x74, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x13, 0x72, 0x65, 0x70, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x6d,
0x28, 0x08, 0x52, 0x08, 0x72, 0x65, 0x70, 0x65, 0x61, 0x74, 0x65, 0x64, 0x12, 0x2e, 0x0a, 0x13, 0x6f, 0x6e, 0x74, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x11, 0x72, 0x65, 0x70, 0x65,
0x72, 0x65, 0x70, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x6d, 0x6f, 0x61, 0x74, 0x65, 0x64, 0x46, 0x72, 0x6f, 0x6d, 0x4d, 0x6f, 0x6e, 0x74, 0x68, 0x12, 0x1c, 0x0a,
0x6e, 0x74, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, 0x11, 0x72, 0x65, 0x70, 0x65, 0x61, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09,
0x74, 0x65, 0x64, 0x46, 0x72, 0x6f, 0x6d, 0x4d, 0x6f, 0x6e, 0x74, 0x68, 0x12, 0x1c, 0x0a, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6d,
0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x75, 0x6e,
0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x6e, 0x6f, 0x6e, 0x5f, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18,
0x75, 0x6e, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x6e, 0x6f, 0x6e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79,
0x12, 0x1d, 0x0a, 0x0a, 0x6e, 0x6f, 0x6e, 0x5f, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x08, 0x2a, 0xa0, 0x01, 0x0a, 0x0c, 0x57, 0x72, 0x69, 0x74, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e,
0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x6e, 0x6f, 0x6e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2a, 0x73, 0x12, 0x11, 0x0a, 0x0d, 0x57, 0x52, 0x49, 0x54, 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f,
0xa0, 0x01, 0x0a, 0x0c, 0x57, 0x72, 0x69, 0x74, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x1d, 0x0a, 0x19, 0x57, 0x52, 0x49, 0x54, 0x45, 0x5f, 0x50, 0x52,
0x12, 0x11, 0x0a, 0x0d, 0x57, 0x52, 0x49, 0x54, 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x45, 0x43, 0x4f, 0x4d, 0x50, 0x55, 0x54, 0x45, 0x44, 0x5f, 0x51, 0x55, 0x45, 0x52, 0x49, 0x45,
0x4e, 0x10, 0x00, 0x12, 0x1d, 0x0a, 0x19, 0x57, 0x52, 0x49, 0x54, 0x45, 0x5f, 0x50, 0x52, 0x45, 0x53, 0x10, 0x01, 0x12, 0x1a, 0x0a, 0x16, 0x57, 0x52, 0x49, 0x54, 0x45, 0x5f, 0x44, 0x49, 0x53,
0x43, 0x4f, 0x4d, 0x50, 0x55, 0x54, 0x45, 0x44, 0x5f, 0x51, 0x55, 0x45, 0x52, 0x49, 0x45, 0x53, 0x54, 0x49, 0x4e, 0x43, 0x54, 0x5f, 0x43, 0x4c, 0x49, 0x45, 0x4e, 0x54, 0x53, 0x10, 0x02, 0x12,
0x10, 0x01, 0x12, 0x1a, 0x0a, 0x16, 0x57, 0x52, 0x49, 0x54, 0x45, 0x5f, 0x44, 0x49, 0x53, 0x54, 0x12, 0x0a, 0x0e, 0x57, 0x52, 0x49, 0x54, 0x45, 0x5f, 0x45, 0x4e, 0x54, 0x49, 0x54, 0x49, 0x45,
0x49, 0x4e, 0x43, 0x54, 0x5f, 0x43, 0x4c, 0x49, 0x45, 0x4e, 0x54, 0x53, 0x10, 0x02, 0x12, 0x12, 0x53, 0x10, 0x03, 0x12, 0x17, 0x0a, 0x13, 0x57, 0x52, 0x49, 0x54, 0x45, 0x5f, 0x44, 0x49, 0x52,
0x0a, 0x0e, 0x57, 0x52, 0x49, 0x54, 0x45, 0x5f, 0x45, 0x4e, 0x54, 0x49, 0x54, 0x49, 0x45, 0x53, 0x45, 0x43, 0x54, 0x5f, 0x54, 0x4f, 0x4b, 0x45, 0x4e, 0x53, 0x10, 0x04, 0x12, 0x15, 0x0a, 0x11,
0x10, 0x03, 0x12, 0x17, 0x0a, 0x13, 0x57, 0x52, 0x49, 0x54, 0x45, 0x5f, 0x44, 0x49, 0x52, 0x45, 0x57, 0x52, 0x49, 0x54, 0x45, 0x5f, 0x49, 0x4e, 0x54, 0x45, 0x4e, 0x54, 0x5f, 0x4c, 0x4f, 0x47,
0x43, 0x54, 0x5f, 0x54, 0x4f, 0x4b, 0x45, 0x4e, 0x53, 0x10, 0x04, 0x12, 0x15, 0x0a, 0x11, 0x57, 0x53, 0x10, 0x05, 0x42, 0x36, 0x5a, 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f,
0x52, 0x49, 0x54, 0x45, 0x5f, 0x49, 0x4e, 0x54, 0x45, 0x4e, 0x54, 0x5f, 0x4c, 0x4f, 0x47, 0x53, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x76, 0x61, 0x75, 0x6c,
0x10, 0x05, 0x42, 0x36, 0x5a, 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x74, 0x2f, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x2f, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79,
0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f,
0x2f, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x2f, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2f, 0x74, 0x6f, 0x33,
0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x33,
} }
var ( var (

View File

@ -48,10 +48,9 @@ message Clients {
message Client { message Client {
string id = 1; string id = 1;
int32 count = 2; int32 count = 2;
int32 times_seen = 3; bool repeated = 3;
bool repeated = 4; int32 repeated_from_month = 4;
int32 repeated_from_month = 5; string namespace = 5;
string namespace = 6; string mount = 6;
string mount = 7; bool non_entity = 7;
bool non_entity = 8;
} }

View File

@ -7,9 +7,14 @@ package vault
import ( import (
"context" "context"
"errors"
"fmt"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical" "github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/vault/activity"
"github.com/hashicorp/vault/vault/activity/generation" "github.com/hashicorp/vault/vault/activity/generation"
"google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/encoding/protojson"
) )
@ -51,3 +56,123 @@ func (b *SystemBackend) handleActivityWriteData(ctx context.Context, request *lo
} }
return nil, nil return nil, nil
} }
// singleMonthActivityClients holds a single month's client IDs, in the order they were seen
type singleMonthActivityClients struct {
// clients are indexed by ID
clients []*activity.EntityRecord
}
// multipleMonthsActivityClients holds multiple month's data
type multipleMonthsActivityClients struct {
// months are in order, with month 0 being the current month and index 1 being 1 month ago
months []*singleMonthActivityClients
}
// addNewClients generates clients according to the given parameters, and adds them to the month
// the client will always have the mountAccessor as its mount accessor
func (s *singleMonthActivityClients) addNewClients(c *generation.Client, mountAccessor string) error {
count := 1
if c.Count > 1 {
count = int(c.Count)
}
clientType := entityActivityType
if c.NonEntity {
clientType = nonEntityTokenActivityType
}
for i := 0; i < count; i++ {
record := &activity.EntityRecord{
ClientID: c.Id,
NamespaceID: c.Namespace,
NonEntity: c.NonEntity,
MountAccessor: mountAccessor,
ClientType: clientType,
}
if record.ClientID == "" {
var err error
record.ClientID, err = uuid.GenerateUUID()
if err != nil {
return err
}
}
s.clients = append(s.clients, record)
}
return nil
}
// processMonth populates a month of client data
func (m *multipleMonthsActivityClients) processMonth(ctx context.Context, core *Core, month *generation.Data) error {
if month.GetAll() == nil {
return errors.New("segmented monthly data is not yet supported")
}
// default to using the root namespace and the first mount on the root namespace
mounts, err := core.ListMounts()
if err != nil {
return err
}
defaultMountAccessorRootNS := ""
for _, mount := range mounts {
if mount.NamespaceID == namespace.RootNamespaceID {
defaultMountAccessorRootNS = mount.Accessor
break
}
}
addingTo := m.months[month.GetMonthsAgo()]
for _, clients := range month.GetAll().Clients {
if clients.Repeated || clients.RepeatedFromMonth > 0 {
return errors.New("repeated clients are not yet supported")
}
if clients.Namespace == "" {
clients.Namespace = namespace.RootNamespaceID
}
// verify that the namespace exists
ns, err := core.NamespaceByID(ctx, clients.Namespace)
if err != nil {
return err
}
// verify that the mount exists
if clients.Mount != "" {
nctx := namespace.ContextWithNamespace(ctx, ns)
mountEntry := core.router.MatchingMountEntry(nctx, clients.Mount)
if mountEntry == nil {
return fmt.Errorf("unable to find matching mount in namespace %s", clients.Namespace)
}
}
mountAccessor := defaultMountAccessorRootNS
if clients.Namespace != namespace.RootNamespaceID && clients.Mount == "" {
// if we're not using the root namespace, find a mount on the namespace that we are using
found := false
for _, mount := range mounts {
if mount.NamespaceID == clients.Namespace {
mountAccessor = mount.Accessor
found = true
break
}
}
if !found {
return fmt.Errorf("unable to find matching mount in namespace %s", clients.Namespace)
}
}
err = addingTo.addNewClients(clients, mountAccessor)
if err != nil {
return err
}
}
return nil
}
func newMultipleMonthsActivityClients(numberOfMonths int) *multipleMonthsActivityClients {
m := &multipleMonthsActivityClients{
months: make([]*singleMonthActivityClients, numberOfMonths),
}
for i := 0; i < numberOfMonths; i++ {
m.months[i] = new(singleMonthActivityClients)
}
return m
}

View File

@ -6,10 +6,12 @@
package vault package vault
import ( import (
"context"
"testing" "testing"
"github.com/hashicorp/vault/helper/namespace" "github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/sdk/logical" "github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/vault/activity/generation"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -82,3 +84,164 @@ func TestSystemBackend_handleActivityWriteData(t *testing.T) {
}) })
} }
} }
// Test_singleMonthActivityClients_addNewClients verifies that new clients are
// created correctly, adhering to the requested parameters. The clients should
// use the inputted mount and a generated ID if one is not supplied. The new
// client should be added to the month's `clients` slice
func Test_singleMonthActivityClients_addNewClients(t *testing.T) {
tests := []struct {
name string
mount string
clients *generation.Client
wantNamespace string
wantMount string
wantID string
}{
{
name: "default mount is used",
mount: "default_mount",
wantMount: "default_mount",
clients: &generation.Client{},
},
{
name: "record namespace is used, default mount is used",
mount: "default_mount",
wantNamespace: "ns",
wantMount: "default_mount",
clients: &generation.Client{
Namespace: "ns",
Mount: "mount",
},
},
{
name: "predefined ID is used",
clients: &generation.Client{
Id: "client_id",
},
wantID: "client_id",
},
{
name: "non zero count",
clients: &generation.Client{
Count: 5,
},
},
{
name: "non entity client",
clients: &generation.Client{
NonEntity: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := &singleMonthActivityClients{}
err := m.addNewClients(tt.clients, tt.mount)
require.NoError(t, err)
numNew := tt.clients.Count
if numNew == 0 {
numNew = 1
}
require.Len(t, m.clients, int(numNew))
for _, rec := range m.clients {
require.NotNil(t, rec)
require.Equal(t, tt.wantNamespace, rec.NamespaceID)
require.Equal(t, tt.wantMount, rec.MountAccessor)
require.Equal(t, tt.clients.NonEntity, rec.NonEntity)
if tt.wantID != "" {
require.Equal(t, tt.wantID, rec.ClientID)
} else {
require.NotEqual(t, "", rec.ClientID)
}
}
})
}
}
// Test_multipleMonthsActivityClients_processMonth verifies that a month of data
// is added correctly. The test checks that default values are handled correctly
// for mounts and namespaces.
func Test_multipleMonthsActivityClients_processMonth(t *testing.T) {
core, _, _ := TestCoreUnsealed(t)
tests := []struct {
name string
clients *generation.Data
wantError bool
numMonths int
}{
{
name: "specified namespace and mount exist",
clients: &generation.Data{
Clients: &generation.Data_All{All: &generation.Clients{Clients: []*generation.Client{{
Namespace: namespace.RootNamespaceID,
Mount: "identity/",
}}}},
},
numMonths: 1,
},
{
name: "specified namespace exists, mount empty",
clients: &generation.Data{
Clients: &generation.Data_All{All: &generation.Clients{Clients: []*generation.Client{{
Namespace: namespace.RootNamespaceID,
}}}},
},
numMonths: 1,
},
{
name: "empty namespace and mount",
clients: &generation.Data{
Clients: &generation.Data_All{All: &generation.Clients{Clients: []*generation.Client{{}}}},
},
numMonths: 1,
},
{
name: "namespace doesn't exist",
clients: &generation.Data{
Clients: &generation.Data_All{All: &generation.Clients{Clients: []*generation.Client{{
Namespace: "abcd",
}}}},
},
wantError: true,
numMonths: 1,
},
{
name: "namespace exists, mount doesn't exist",
clients: &generation.Data{
Clients: &generation.Data_All{All: &generation.Clients{Clients: []*generation.Client{{
Namespace: namespace.RootNamespaceID,
Mount: "mount",
}}}},
},
wantError: true,
numMonths: 1,
},
{
name: "older month",
clients: &generation.Data{
Month: &generation.Data_MonthsAgo{MonthsAgo: 4},
Clients: &generation.Data_All{All: &generation.Clients{Clients: []*generation.Client{{}}}},
},
numMonths: 5,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := newMultipleMonthsActivityClients(tt.numMonths)
err := m.processMonth(context.Background(), core, tt.clients)
if tt.wantError {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Len(t, m.months[tt.clients.GetMonthsAgo()].clients, len(tt.clients.GetAll().Clients))
for _, month := range m.months {
for _, c := range month.clients {
require.NotEmpty(t, c.NamespaceID)
require.NotEmpty(t, c.MountAccessor)
}
}
}
})
}
}