Files
fleet/server/service/packs_test.go
T
Lucas Manuel Rodriguez 057e1615b4 Move mysql/testing_utils.go to a separate mysql/mysqltest package (#45406)
Resolves #45220 (one of several PRs to achieve removing "testing"
package as dependency in production binary)

## Testing

- [x] QA'd all new/changed functionality manually.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Tests**
* Switched many tests to use a dedicated MySQL test helper package and
consolidated test-only utilities for datastore setup, cleanup, ad‑hoc
SQL, certificate generation, and activity/aggregation helpers.
* Added expanded test utilities for replication, DB connections and test
data seeding to improve integration-test reliability.

* **Chores**
  * No production behavior or user-facing APIs were changed.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/fleetdm/fleet/pull/45406)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-14 11:18:20 -03:00

373 lines
9.1 KiB
Go

package service
import (
"context"
"testing"
"github.com/fleetdm/fleet/v4/server/authz"
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
"github.com/fleetdm/fleet/v4/server/datastore/mysql/mysqltest"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mock"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetPack(t *testing.T) {
ds := new(mock.Store)
svc, ctx := newTestService(t, ds, nil, nil)
ds.PackFunc = func(ctx context.Context, id uint) (*fleet.Pack, error) {
return &fleet.Pack{
ID: 1,
TeamIDs: []uint{1},
}, nil
}
pack, err := svc.GetPack(test.UserContext(ctx, test.UserAdmin), 1)
require.NoError(t, err)
require.Equal(t, uint(1), pack.ID)
_, err = svc.GetPack(test.UserContext(ctx, test.UserNoRoles), 1)
require.Error(t, err)
require.Contains(t, err.Error(), authz.ForbiddenErrorMessage)
}
func TestNewPackSavesTargets(t *testing.T) {
ds := new(mock.Store)
opts := &TestServerOpts{}
svc, ctx := newTestService(t, ds, nil, nil, opts)
ds.NewPackFunc = func(ctx context.Context, pack *fleet.Pack, opts ...fleet.OptionalArg) (*fleet.Pack, error) {
return pack, nil
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{}, nil
}
packPayload := fleet.PackPayload{
Name: ptr.String("foo"),
HostIDs: &[]uint{123},
LabelIDs: &[]uint{456},
TeamIDs: &[]uint{789},
}
pack, err := svc.NewPack(test.UserContext(ctx, test.UserAdmin), packPayload)
require.NoError(t, err)
require.Len(t, pack.HostIDs, 1)
require.Len(t, pack.LabelIDs, 1)
require.Len(t, pack.TeamIDs, 1)
assert.Equal(t, uint(123), pack.HostIDs[0])
assert.Equal(t, uint(456), pack.LabelIDs[0])
assert.Equal(t, uint(789), pack.TeamIDs[0])
assert.True(t, ds.NewPackFuncInvoked)
assert.True(t, opts.ActivityMock.NewActivityFuncInvoked)
}
func TestNewPackRejectsMissingName(t *testing.T) {
cases := []struct {
name string
payload fleet.PackPayload
}{
{name: "nil name", payload: fleet.PackPayload{}},
{name: "empty string name", payload: fleet.PackPayload{Name: ptr.String("")}},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
ds := new(mock.Store)
svc, ctx := newTestService(t, ds, nil, nil)
ds.NewPackFunc = func(ctx context.Context, pack *fleet.Pack, opts ...fleet.OptionalArg) (*fleet.Pack, error) {
return pack, nil
}
_, err := svc.NewPack(test.UserContext(ctx, test.UserAdmin), c.payload)
require.Error(t, err)
var badReqErr *fleet.BadRequestError
require.ErrorAs(t, err, &badReqErr)
assert.Contains(t, badReqErr.Message, "pack name cannot be empty")
assert.False(t, ds.NewPackFuncInvoked, "datastore should not be called when name is missing")
})
}
}
func TestPacksWithDS(t *testing.T) {
ds := mysqltest.CreateMySQLDS(t)
cases := []struct {
name string
fn func(t *testing.T, ds *mysql.Datastore)
}{
{"ListPacks", testPacksListPacks},
{"DeletePack", testPacksDeletePack},
{"DeletePackByID", testPacksDeletePackByID},
{"ApplyPackSpecs", testPacksApplyPackSpecs},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
defer mysqltest.TruncateTables(t, ds)
c.fn(t, ds)
})
}
}
func testPacksListPacks(t *testing.T, ds *mysql.Datastore) {
svc, ctx := newTestService(t, ds, nil, nil)
queries, err := svc.ListPacks(test.UserContext(ctx, test.UserAdmin), fleet.PackListOptions{IncludeSystemPacks: false})
require.NoError(t, err)
assert.Len(t, queries, 0)
_, err = ds.NewPack(ctx, &fleet.Pack{
Name: "foo",
})
require.NoError(t, err)
queries, err = svc.ListPacks(test.UserContext(ctx, test.UserAdmin), fleet.PackListOptions{IncludeSystemPacks: false})
require.NoError(t, err)
assert.Len(t, queries, 1)
}
func testPacksDeletePack(t *testing.T, ds *mysql.Datastore) {
test.AddAllHostsLabel(t, ds)
users := createTestUsers(t, ds)
user := users["admin1@example.com"]
type args struct {
ctx context.Context
name string
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "delete pack that doesn't exist",
args: args{
ctx: test.UserContext(context.Background(), &user),
name: "foo",
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
svc, _ := newTestService(t, ds, nil, nil)
if err := svc.DeletePack(tt.args.ctx, tt.args.name); (err != nil) != tt.wantErr {
t.Errorf("DeletePack() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func testPacksDeletePackByID(t *testing.T, ds *mysql.Datastore) {
test.AddAllHostsLabel(t, ds)
type args struct {
ctx context.Context
id uint
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "cannot delete pack that doesn't exists",
args: args{
ctx: test.UserContext(context.Background(), test.UserAdmin),
id: 123456,
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
svc, _ := newTestService(t, ds, nil, nil)
if err := svc.DeletePackByID(tt.args.ctx, tt.args.id); (err != nil) != tt.wantErr {
t.Errorf("DeletePackByID() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func testPacksApplyPackSpecs(t *testing.T, ds *mysql.Datastore) {
test.AddAllHostsLabel(t, ds)
users := createTestUsers(t, ds)
user := users["admin1@example.com"]
type args struct {
ctx context.Context
specs []*fleet.PackSpec
}
tests := []struct {
name string
args args
want []*fleet.PackSpec
wantErr bool
}{
{
name: "cannot modify global pack",
args: args{
ctx: test.UserContext(context.Background(), &user),
specs: []*fleet.PackSpec{
{Name: "Foo Pack", Description: "Foo Desc", Platform: "MacOS"},
{Name: "Bar Pack", Description: "Bar Desc", Platform: "MacOS"},
},
},
want: []*fleet.PackSpec{
{Name: "Foo Pack", Description: "Foo Desc", Platform: "MacOS"},
{Name: "Bar Pack", Description: "Bar Desc", Platform: "MacOS"},
},
wantErr: false,
},
{
name: "cannot modify team pack",
args: args{
ctx: test.UserContext(context.Background(), &user),
specs: []*fleet.PackSpec{
{Name: "Test", Description: "Test Desc", Platform: "linux"},
},
},
want: []*fleet.PackSpec{
{Name: "Test", Description: "Test Desc", Platform: "linux"},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
svc, _ := newTestService(t, ds, nil, nil)
got, err := svc.ApplyPackSpecs(tt.args.ctx, tt.args.specs)
if (err != nil) != tt.wantErr {
t.Errorf("ApplyPackSpecs() error = %v, wantErr %v", err, tt.wantErr)
return
}
require.Equal(t, tt.want, got)
})
}
}
func TestUserIsGitOpsOnly(t *testing.T) {
for _, tc := range []struct {
name string
user *fleet.User
expectedFn func(value bool, err error) bool
}{
{
name: "missing user in context",
user: nil,
expectedFn: func(value bool, err error) bool {
return err != nil && !value
},
},
{
name: "no roles",
user: &fleet.User{},
expectedFn: func(value bool, err error) bool {
return err != nil && !value
},
},
{
name: "global gitops",
user: &fleet.User{
GlobalRole: ptr.String(fleet.RoleGitOps),
},
expectedFn: func(value bool, err error) bool {
return err == nil && value
},
},
{
name: "global non-gitops",
user: &fleet.User{
GlobalRole: ptr.String(fleet.RoleObserver),
},
expectedFn: func(value bool, err error) bool {
return err == nil && !value
},
},
{
name: "team gitops",
user: &fleet.User{
GlobalRole: nil,
Teams: []fleet.UserTeam{
{
Team: fleet.Team{ID: 1},
Role: fleet.RoleGitOps,
},
},
},
expectedFn: func(value bool, err error) bool {
return err == nil && value
},
},
{
name: "multiple team gitops",
user: &fleet.User{
GlobalRole: nil,
Teams: []fleet.UserTeam{
{
Team: fleet.Team{ID: 1},
Role: fleet.RoleGitOps,
},
{
Team: fleet.Team{ID: 2},
Role: fleet.RoleGitOps,
},
},
},
expectedFn: func(value bool, err error) bool {
return err == nil && value
},
},
{
name: "multiple teams, not all gitops",
user: &fleet.User{
GlobalRole: nil,
Teams: []fleet.UserTeam{
{
Team: fleet.Team{ID: 1},
Role: fleet.RoleObserver,
},
{
Team: fleet.Team{ID: 2},
Role: fleet.RoleGitOps,
},
},
},
expectedFn: func(value bool, err error) bool {
return err == nil && !value
},
},
{
name: "multiple teams, none gitops",
user: &fleet.User{
GlobalRole: nil,
Teams: []fleet.UserTeam{
{
Team: fleet.Team{ID: 1},
Role: fleet.RoleObserver,
},
{
Team: fleet.Team{ID: 2},
Role: fleet.RoleMaintainer,
},
},
},
expectedFn: func(value bool, err error) bool {
return err == nil && !value
},
},
} {
t.Run(tc.name, func(t *testing.T) {
actual, err := userIsGitOpsOnly(viewer.NewContext(context.Background(), viewer.Viewer{User: tc.user}))
require.True(t, tc.expectedFn(actual, err))
})
}
}