fleet/server/service/transport_test.go
Victor Lyuboslavsky 4d7f947529
GET /hosts API endpoint can now populate policies with populate_policies=true query parameter. (#17270)
GET /hosts API endpoint can now populate policies with
populate_policies=true query parameter.
#16242

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [x] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [x] Added/updated tests
- [x] Manual QA for all new/changed functionality
2024-03-01 11:20:21 -06:00

510 lines
15 KiB
Go

package service
import (
"fmt"
"net/http"
"net/url"
"strings"
"testing"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestListOptionsFromRequest(t *testing.T) {
listOptionsTests := []struct {
// url string to parse
url string
// expected list options
listOptions fleet.ListOptions
// should cause a BadRequest error
shouldErr400 bool
}{
// both params provided
{
url: "/foo?page=1&per_page=10",
listOptions: fleet.ListOptions{Page: 1, PerPage: 10},
},
// only per_page (page should default to 0)
{
url: "/foo?per_page=10",
listOptions: fleet.ListOptions{Page: 0, PerPage: 10},
},
// only page (per_page should default to defaultPerPage
{
url: "/foo?page=10",
listOptions: fleet.ListOptions{Page: 10, PerPage: defaultPerPage},
},
// no params provided (defaults to empty ListOptions indicating
// unlimited)
{
url: "/foo?unrelated=foo",
listOptions: fleet.ListOptions{},
},
// Both order params provided
{
url: "/foo?order_key=foo&order_direction=desc",
listOptions: fleet.ListOptions{OrderKey: "foo", OrderDirection: fleet.OrderDescending},
},
// Both order params provided (asc)
{
url: "/foo?order_key=bar&order_direction=asc",
listOptions: fleet.ListOptions{OrderKey: "bar", OrderDirection: fleet.OrderAscending},
},
// Default order direction
{
url: "/foo?order_key=foo",
listOptions: fleet.ListOptions{OrderKey: "foo", OrderDirection: fleet.OrderAscending},
},
// All params defined
{
url: "/foo?order_key=foo&order_direction=desc&page=1&per_page=100&after=bar",
listOptions: fleet.ListOptions{
OrderKey: "foo",
OrderDirection: fleet.OrderDescending,
Page: 1,
PerPage: 100,
After: "bar",
},
},
// various 400 error cases
{
url: "/foo?page=foo&per_page=10",
shouldErr400: true,
},
{
url: "/foo?page=1&per_page=foo",
shouldErr400: true,
},
{
url: "/foo?page=-1",
shouldErr400: true,
},
{
url: "/foo?page=1&per_page=-10",
shouldErr400: true,
},
// order_direction without order_key
{
url: "/foo?page=1&order_direction=desc",
shouldErr400: true,
},
// bad order_direction
{
url: "/foo?&order_direction=foo&order_key=foo",
shouldErr400: true,
},
// after without order_key
{
url: "/foo?page=1&after=foo",
shouldErr400: true,
},
}
for _, tt := range listOptionsTests {
t.Run(
tt.url, func(t *testing.T) {
urlStruct, _ := url.Parse(tt.url)
req := &http.Request{URL: urlStruct}
opt, err := listOptionsFromRequest(req)
if tt.shouldErr400 {
assert.NotNil(t, err)
var be *fleet.BadRequestError
require.ErrorAs(t, err, &be)
return
}
assert.Nil(t, err)
assert.Equal(t, tt.listOptions, opt)
},
)
}
}
func TestHostListOptionsFromRequest(t *testing.T) {
hostListOptionsTests := map[string]struct {
// url string to parse
url string
// expected options
hostListOptions fleet.HostListOptions
// expected error message, if any
errorMessage string
}{
"no params passed": {
url: "/foo",
hostListOptions: fleet.HostListOptions{},
},
"embedded list options params defined": {
url: "/foo?order_key=foo&order_direction=desc&page=1&per_page=100",
hostListOptions: fleet.HostListOptions{
ListOptions: fleet.ListOptions{
OrderKey: "foo",
OrderDirection: fleet.OrderDescending,
Page: 1,
PerPage: 100,
},
},
},
"all params defined": {
url: "/foo?order_key=foo&order_direction=asc&page=10&per_page=1&device_mapping=T&additional_info_filters" +
"=filter1,filter2&status=new&team_id=2&policy_id=3&policy_response=passing&software_id=4&os_id=5" +
"&os_name=osName&os_version=osVersion&os_version_id=5&disable_failing_policies=1&macos_settings=verified" +
"&macos_settings_disk_encryption=enforcing&os_settings=pending&os_settings_disk_encryption=failed" +
"&bootstrap_package=installed&mdm_id=6&mdm_name=mdmName&mdm_enrollment_status=automatic" +
"&munki_issue_id=7&low_disk_space=99&vulnerability=CVE-2023-42887&populate_policies=true",
hostListOptions: fleet.HostListOptions{
ListOptions: fleet.ListOptions{
OrderKey: "foo",
OrderDirection: fleet.OrderAscending,
Page: 10,
PerPage: 1,
},
DeviceMapping: true,
AdditionalFilters: []string{"filter1", "filter2"},
StatusFilter: fleet.StatusNew,
TeamFilter: ptr.Uint(2),
PolicyIDFilter: ptr.Uint(3),
PolicyResponseFilter: ptr.Bool(true),
SoftwareIDFilter: ptr.Uint(4),
OSIDFilter: ptr.Uint(5),
OSVersionIDFilter: ptr.Uint(5),
OSNameFilter: ptr.String("osName"),
OSVersionFilter: ptr.String("osVersion"),
DisableFailingPolicies: true,
MacOSSettingsFilter: fleet.OSSettingsVerified,
MacOSSettingsDiskEncryptionFilter: fleet.DiskEncryptionEnforcing,
OSSettingsFilter: fleet.OSSettingsPending,
OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionFailed,
MDMBootstrapPackageFilter: (*fleet.MDMBootstrapPackageStatus)(ptr.String(string(fleet.MDMBootstrapPackageInstalled))),
MDMIDFilter: ptr.Uint(6),
MDMNameFilter: ptr.String("mdmName"),
MDMEnrollmentStatusFilter: fleet.MDMEnrollStatusAutomatic,
MunkiIssueIDFilter: ptr.Uint(7),
LowDiskSpaceFilter: ptr.Int(99),
VulnerabilityFilter: ptr.String("CVE-2023-42887"),
PopulatePolicies: true,
},
},
"policy_id and policy_response params (for coverage)": {
url: "/foo?policy_id=100&policy_response=failing",
hostListOptions: fleet.HostListOptions{
PolicyIDFilter: ptr.Uint(100),
PolicyResponseFilter: ptr.Bool(false),
},
},
"error in page (embedded list options)": {
url: "/foo?page=-1",
errorMessage: "negative page value",
},
"error in status": {
url: "/foo?status=foo",
errorMessage: "Invalid status",
},
"error in team_id (number too large)": {
url: "/foo?team_id=9,223,372,036,854,775,808",
errorMessage: "Invalid team_id",
},
"error in team_id (not a number)": {
url: "/foo?team_id=foo",
errorMessage: "Invalid team_id",
},
"error in policy_id": {
url: "/foo?policy_id=foo",
errorMessage: "Invalid policy_id",
},
"error when policy_response specified without policy_id": {
url: "/foo?policy_response=passing",
errorMessage: "Missing policy_id",
},
"error in policy_response (invalid option)": {
url: "/foo?policy_id=1&policy_response=foo",
errorMessage: "Invalid policy_response",
},
"error in software_id": {
url: "/foo?software_id=foo",
errorMessage: "Invalid software_id",
},
"error in os_id": {
url: "/foo?os_id=foo",
errorMessage: "Invalid os_id",
},
"error in disable_failing_policies": {
url: "/foo?disable_failing_policies=foo",
errorMessage: "Invalid disable_failing_policies",
},
"error in device_mapping": {
url: "/foo?device_mapping=foo",
errorMessage: "Invalid device_mapping",
},
"error in mdm_id": {
url: "/foo?mdm_id=foo",
errorMessage: "Invalid mdm_id",
},
"error in mdm_enrollment_status (invalid option)": {
url: "/foo?mdm_enrollment_status=foo",
errorMessage: "Invalid mdm_enrollment_status",
},
"error in macos_settings (invalid option)": {
url: "/foo?macos_settings=foo",
errorMessage: "Invalid macos_settings",
},
"error in macos_settings_disk_encryption (invalid option)": {
url: "/foo?macos_settings_disk_encryption=foo",
errorMessage: "Invalid macos_settings_disk_encryption",
},
"error in os_settings (invalid option)": {
url: "/foo?os_settings=foo",
errorMessage: "Invalid os_settings",
},
"error in os_settings_disk_encryption (invalid option)": {
url: "/foo?os_settings_disk_encryption=foo",
errorMessage: "Invalid os_settings_disk_encryption",
},
"error in bootstrap_package (invalid option)": {
url: "/foo?bootstrap_package=foo",
errorMessage: "Invalid bootstrap_package",
},
"error in munki_issue_id": {
url: "/foo?munki_issue_id=foo",
errorMessage: "Invalid munki_issue_id",
},
"error in low_disk_space (not a number)": {
url: "/foo?low_disk_space=foo",
errorMessage: "Invalid low_disk_space",
},
"error in low_disk_space (too low)": {
url: "/foo?low_disk_space=0",
errorMessage: "Invalid low_disk_space",
},
"error in low_disk_space (too high)": {
url: "/foo?low_disk_space=101",
errorMessage: "Invalid low_disk_space",
},
"error in os_name/os_version (os_name missing)": {
url: "/foo?os_version=1.0",
errorMessage: "Invalid os_name",
},
"error in os_name/os_version (os_version missing)": {
url: "/foo?os_name=foo",
errorMessage: "Invalid os_version",
},
"negative software_id": {
url: "/foo?software_id=-10",
errorMessage: "Invalid software_id",
},
"negative software_version_id": {
url: "/foo?software_version_id=-10",
errorMessage: "Invalid software_version_id",
},
"negative software_title_id": {
url: "/foo?software_title_id=-10",
errorMessage: "Invalid software_title_id",
},
"software_title_id too big": {
url: "/foo?software_title_id=" + fmt.Sprint(1<<33),
errorMessage: "Invalid software_title_id",
},
"software_version_id can be > 32bits": {
url: "/foo?software_version_id=" + fmt.Sprint(1<<33),
hostListOptions: fleet.HostListOptions{
SoftwareVersionIDFilter: ptr.Uint(1 << 33),
},
},
"good software_version_id": {
url: "/foo?software_version_id=1",
hostListOptions: fleet.HostListOptions{
SoftwareVersionIDFilter: ptr.Uint(1),
},
},
"good software_title_id": {
url: "/foo?software_title_id=1",
hostListOptions: fleet.HostListOptions{
SoftwareTitleIDFilter: ptr.Uint(1),
},
},
"invalid combination software_title_id and software_version_id": {
url: "/foo?software_title_id=1&software_version_id=2",
errorMessage: "The combination of software_version_id and software_title_id is not allowed",
},
"invalid combination software_id and software_version_id": {
url: "/foo?software_id=1&software_version_id=2",
errorMessage: "The combination of software_id and software_version_id is not allowed",
},
"invalid populate_policies": {
url: "/foo?populate_policies=foo",
errorMessage: "populate_policies",
},
}
for name, tt := range hostListOptionsTests {
t.Run(
name, func(t *testing.T) {
urlStruct, _ := url.Parse(tt.url)
req := &http.Request{URL: urlStruct}
opt, err := hostListOptionsFromRequest(req)
if tt.errorMessage != "" {
assert.NotNil(t, err)
var be *fleet.BadRequestError
require.ErrorAs(t, err, &be)
require.Contains(t, err.Error(), tt.errorMessage)
return
}
assert.Nil(t, err)
assert.Equal(t, tt.hostListOptions, opt)
},
)
}
}
func TestCarveListOptionsFromRequest(t *testing.T) {
carveListOptionsTests := map[string]struct {
// url string to parse
url string
// expected options
carveListOptions fleet.CarveListOptions
// expected error message, if any
errorMessage string
}{
"no params passed": {
url: "/foo",
carveListOptions: fleet.CarveListOptions{},
},
"embedded list options params defined": {
url: "/foo?order_key=foo&order_direction=desc&page=1&per_page=100",
carveListOptions: fleet.CarveListOptions{
ListOptions: fleet.ListOptions{
OrderKey: "foo",
OrderDirection: fleet.OrderDescending,
Page: 1,
PerPage: 100,
},
},
},
"all params defined": {
url: "/foo?order_key=foo&order_direction=asc&page=10&per_page=1&expired=true",
carveListOptions: fleet.CarveListOptions{
ListOptions: fleet.ListOptions{
OrderKey: "foo",
OrderDirection: fleet.OrderAscending,
Page: 10,
PerPage: 1,
},
Expired: true,
},
},
"error in page (embedded list options)": {
url: "/foo?page=-1",
errorMessage: "negative page value",
},
"error in expired": {
url: "/foo?expired=foo",
errorMessage: "Invalid expired",
},
}
for name, tt := range carveListOptionsTests {
t.Run(
name, func(t *testing.T) {
urlStruct, _ := url.Parse(tt.url)
req := &http.Request{URL: urlStruct}
opt, err := carveListOptionsFromRequest(req)
if tt.errorMessage != "" {
assert.NotNil(t, err)
var be *fleet.BadRequestError
require.ErrorAs(t, err, &be)
assert.True(
t, strings.Contains(err.Error(), tt.errorMessage),
"error message '%v' should contain '%v'", err.Error(), tt.errorMessage,
)
return
}
assert.Nil(t, err)
assert.Equal(t, tt.carveListOptions, opt)
},
)
}
}
func TestUserListOptionsFromRequest(t *testing.T) {
userListOptionsTests := map[string]struct {
// url string to parse
url string
// expected options
userListOptions fleet.UserListOptions
// expected error message, if any
errorMessage string
}{
"no params passed": {
url: "/foo",
userListOptions: fleet.UserListOptions{},
},
"embedded list options params defined": {
url: "/foo?order_key=foo&order_direction=desc&page=1&per_page=100",
userListOptions: fleet.UserListOptions{
ListOptions: fleet.ListOptions{
OrderKey: "foo",
OrderDirection: fleet.OrderDescending,
Page: 1,
PerPage: 100,
},
},
},
"all params defined": {
url: "/foo?order_key=foo&order_direction=asc&page=10&per_page=1&team_id=1",
userListOptions: fleet.UserListOptions{
ListOptions: fleet.ListOptions{
OrderKey: "foo",
OrderDirection: fleet.OrderAscending,
Page: 10,
PerPage: 1,
},
TeamID: 1,
},
},
"error in page (embedded list options)": {
url: "/foo?page=-1",
errorMessage: "negative page value",
},
"error in team_id (negative_number)": {
url: "/foo?team_id=-1",
errorMessage: "Invalid team_id",
},
"error in team_id (not a number)": {
url: "/foo?team_id=foo",
errorMessage: "Invalid team_id",
},
}
for name, tt := range userListOptionsTests {
t.Run(
name, func(t *testing.T) {
urlStruct, _ := url.Parse(tt.url)
req := &http.Request{URL: urlStruct}
opt, err := userListOptionsFromRequest(req)
if tt.errorMessage != "" {
assert.NotNil(t, err)
var be *fleet.BadRequestError
require.ErrorAs(t, err, &be)
assert.True(
t, strings.Contains(err.Error(), tt.errorMessage),
"error message '%v' should contain '%v'", err.Error(), tt.errorMessage,
)
return
}
assert.Nil(t, err)
assert.Equal(t, tt.userListOptions, opt)
},
)
}
}