SCIM integration tests (#27750)

For #27287

This PR adds integration tests for SCIM API endpoints as well as some
bug fixes found by these tests.

# Checklist for submitter

- [x] Added/updated automated tests
- [x] Manual QA for all new/changed functionality
This commit is contained in:
Victor Lyuboslavsky
2025-04-04 11:00:46 -05:00
committed by GitHub
parent 28232b5c11
commit 61a7b70b5d
16 changed files with 1824 additions and 159 deletions
+1 -1
View File
@@ -191,7 +191,7 @@ func TestCreateBulkUsers(t *testing.T) {
user15,user15@example.com,false,false,,1:admin
user16,user16@example.com,false,false,,1:admin 2:maintainer`)
expectedText := `{"kind":"user_roles","apiVersion":"v1","spec":{"roles":{"admin1@example.com":{"global_role":"admin","teams":null},"user11@example.com":{"global_role":"maintainer","teams":null},"user12@example.com":{"global_role":"observer","teams":null},"user13@example.com":{"global_role":"admin","teams":null},"user14@example.com":{"global_role":null,"teams":[{"team":"","role":"maintainer"}]},"user15@example.com":{"global_role":null,"teams":[{"team":"","role":"admin"}]},"user16@example.com":{"global_role":null,"teams":[{"team":"","role":"admin"},{"team":"","role":"maintainer"}]},"user1@example.com":{"global_role":"observer","teams":null},"user2@example.com":{"global_role":"observer","teams":null}}}}
expectedText := `{"kind":"user_roles","apiVersion":"v1","spec":{"roles":{"admin1@example.com":{"global_role":"admin","teams":null},"user11@example.com":{"global_role":"maintainer","teams":null},"user12@example.com":{"global_role":"observer","teams":null},"user13@example.com":{"global_role":"admin","teams":null},"user14@example.com":{"global_role":null,"teams":[{"team":"","role":"maintainer"}]},"user15@example.com":{"global_role":null,"teams":[{"team":"","role":"admin"}]},"user16@example.com":{"global_role":null,"teams":[{"team":"","role":"admin"},{"team":"","role":"maintainer"}]},"user1@example.com":{"global_role":"maintainer","teams":null},"user2@example.com":{"global_role":"observer","teams":null}}}}
`
assert.Equal(t, "", runAppForTest(t, []string{"user", "create-users", "--csv", csvFile}))
@@ -18,7 +18,7 @@ Create a SAML app in an IdP.
## Description
If the IT admin configured end user authentication, we change the `configuration_web_url` value in the [enrollment JSON profile](https://developer.apple.com/documentation/devicemanagement/profile) to be `{server_url}/api/v1/fleet/mdm/sso`. This page initiates the SSO flow in the setup assistant webview.
If the IT admin configured end user authentication, we change the `configuration_web_url` value in the [enrollment JSON profile](https://developer.apple.com/documentation/devicemanagement/profile) to be `{server_url}/mdm/sso`. This page gets the SAML Request from `{server_url}/api/v1/fleet/mdm/sso` and initiates the SSO flow in the setup assistant web view.
`end_user_authentication` setting is global, but `enable_end_user_authentication` is a team setting.
@@ -11,6 +11,10 @@
- https://developer.okta.com/docs/guides/scim-provisioning-integration-prepare/main/
Sample provisioning settings that work. Capabilities can be disabled and attributes can be removed as needed.
![Okta to Fleet provisioning](./assets/SCIM-Okta-provisioning.png)
### Testing Okta integration
First, create at least one SCIM user:
Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

File diff suppressed because it is too large Load Diff
+44
View File
@@ -0,0 +1,44 @@
package scim
import (
"os"
"testing"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/service"
"github.com/fleetdm/fleet/v4/server/service/integrationtest"
"github.com/go-kit/log"
)
type Suite struct {
integrationtest.BaseSuite
}
func SetUpSuite(t *testing.T, uniqueTestName string) *Suite {
ds, redisPool, fleetCfg, fleetSvc, ctx := integrationtest.SetUpDSRedisService(t, uniqueTestName)
logger := log.NewLogfmtLogger(os.Stdout)
users, server := service.RunServerForTestsWithServiceWithDS(t, ctx, ds, fleetSvc, &service.TestServerOpts{
License: &fleet.LicenseInfo{
Tier: fleet.TierFree,
},
FleetConfig: &fleetCfg,
Pool: redisPool,
Logger: logger,
EnableSCIM: true,
})
s := &Suite{
BaseSuite: integrationtest.BaseSuite{
Logger: logger,
DS: ds,
FleetCfg: fleetCfg,
Users: users,
Server: server,
},
}
integrationtest.SetUpServerURL(t, ds, server)
s.Token = s.GetTestAdminToken(t)
return s
}
+7 -5
View File
@@ -155,10 +155,12 @@ func createGroupResource(group *fleet.ScimGroup) scim.Resource {
return groupResource
}
// GetAll
// Pagination is 1-indexed.
func (g *GroupHandler) GetAll(r *http.Request, params scim.ListRequestParams) (scim.Page, error) {
page := params.StartIndex
if page < 1 {
page = 1
startIndex := params.StartIndex
if startIndex < 1 {
startIndex = 1
}
count := params.Count
if count > maxResults {
@@ -169,8 +171,8 @@ func (g *GroupHandler) GetAll(r *http.Request, params scim.ListRequestParams) (s
}
opts := fleet.ScimListOptions{
Page: uint(page), // nolint:gosec // ignore G115
PerPage: uint(count), // nolint:gosec // ignore G115
StartIndex: uint(startIndex), // nolint:gosec // ignore G115
PerPage: uint(count), // nolint:gosec // ignore G115
}
resourceFilter := r.URL.Query().Get("filter")
+26 -13
View File
@@ -50,6 +50,11 @@ func (u *UserHandler) Create(r *http.Request, attributes scim.ResourceAttributes
level.Error(u.logger).Log("msg", "failed to get userName", "err", err)
return scim.Resource{}, err
}
// In IETF documents, “non-empty” is generally used in the literal sense of “having at least one character.” That means if a value contains one or more spaces (and nothing else), it is still considered non-empty.
if len(userName) == 0 {
level.Info(u.logger).Log("msg", "userName is empty")
return scim.Resource{}, errors.ScimErrorBadParams([]string{userNameAttr})
}
_, err = u.ds.ScimUserByUserName(r.Context(), userName)
switch {
case err != nil && !fleet.IsNotFound(err):
@@ -260,6 +265,8 @@ func createUserResource(user *fleet.ScimUser) scim.Resource {
}
// GetAll
// Pagination is 1-indexed.
//
// Per RFC7644 3.4.2, SHOULD ignore any query parameters they do not recognize instead of rejecting the query for versioning compatibility reasons
// https://datatracker.ietf.org/doc/html/rfc7644#section-3.4.2
//
@@ -274,9 +281,9 @@ func createUserResource(user *fleet.ScimUser) scim.Resource {
// totalResults: The total number of results returned by the list or query operation. The value may be larger than the number of
// resources returned, such as when returning a single page (see Section 3.4.2.4) of results where multiple pages are available.
func (u *UserHandler) GetAll(r *http.Request, params scim.ListRequestParams) (scim.Page, error) {
page := params.StartIndex
if page < 1 {
page = 1
startIndex := params.StartIndex
if startIndex < 1 {
startIndex = 1
}
count := params.Count
if count > maxResults {
@@ -288,8 +295,8 @@ func (u *UserHandler) GetAll(r *http.Request, params scim.ListRequestParams) (sc
opts := fleet.ScimUsersListOptions{
ScimListOptions: fleet.ScimListOptions{
Page: uint(page), // nolint:gosec // ignore G115
PerPage: uint(count), // nolint:gosec // ignore G115
StartIndex: uint(startIndex), // nolint:gosec // ignore G115
PerPage: uint(count), // nolint:gosec // ignore G115
},
}
resourceFilter := r.URL.Query().Get("filter")
@@ -400,6 +407,10 @@ func (u *UserHandler) Patch(r *http.Request, id string, operations []scim.PatchO
return scim.Resource{}, err
}
if len(operations) > 1 {
level.Info(u.logger).Log("msg", "too many patch operations")
return scim.Resource{}, errors.ScimErrorBadParams([]string{"Operations"})
}
for _, op := range operations {
if op.Op != "replace" {
level.Info(u.logger).Log("msg", "unsupported patch operation", "op", op.Op)
@@ -435,14 +446,16 @@ func (u *UserHandler) Patch(r *http.Request, id string, operations []scim.PatchO
}
}
err = u.ds.ReplaceScimUser(r.Context(), user)
switch {
case fleet.IsNotFound(err):
level.Info(u.logger).Log("msg", "failed to find user to patch", "id", id)
return scim.Resource{}, errors.ScimErrorResourceNotFound(id)
case err != nil:
level.Error(u.logger).Log("msg", "failed to patch user", "id", id, "err", err)
return scim.Resource{}, err
if len(operations) != 0 {
err = u.ds.ReplaceScimUser(r.Context(), user)
switch {
case fleet.IsNotFound(err):
level.Info(u.logger).Log("msg", "failed to find user to patch", "id", id)
return scim.Resource{}, errors.ScimErrorResourceNotFound(id)
case err != nil:
level.Error(u.logger).Log("msg", "failed to patch user", "id", id, "err", err)
return scim.Resource{}, err
}
}
return createUserResource(user), nil
+6 -19
View File
@@ -227,13 +227,6 @@ func insertEmails(ctx context.Context, tx sqlx.ExtContext, user *fleet.ScimUser)
// DeleteScimUser deletes a SCIM user from the database
func (ds *Datastore) DeleteScimUser(ctx context.Context, id uint) error {
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
// Delete all email entries for the user
const deleteEmailsQuery = `DELETE FROM scim_user_emails WHERE scim_user_id = ?`
_, err := tx.ExecContext(ctx, deleteEmailsQuery, id)
if err != nil {
return ctxerr.Wrap(ctx, err, "delete scim user emails")
}
// Delete the user
const deleteUserQuery = `DELETE FROM scim_users WHERE id = ?`
result, err := tx.ExecContext(ctx, deleteUserQuery, id)
@@ -257,16 +250,13 @@ func (ds *Datastore) DeleteScimUser(ctx context.Context, id uint) error {
// ListScimUsers retrieves a list of SCIM users with optional filtering
func (ds *Datastore) ListScimUsers(ctx context.Context, opts fleet.ScimUsersListOptions) (users []fleet.ScimUser, totalResults uint, err error) {
// Default pagination values if not provided
if opts.Page == 0 {
opts.Page = 1
if opts.StartIndex == 0 {
opts.StartIndex = 1
}
if opts.PerPage == 0 {
opts.PerPage = SCIMDefaultResourcesPerPage
}
// Calculate offset for pagination
offset := (opts.Page - 1) * opts.PerPage
// Build the base query
baseQuery := `
SELECT DISTINCT
@@ -298,7 +288,7 @@ func (ds *Datastore) ListScimUsers(ctx context.Context, opts fleet.ScimUsersList
// Add pagination to the main query
query := baseQuery + whereClause + " ORDER BY scim_users.id LIMIT ? OFFSET ?"
params = append(params, opts.PerPage, offset)
params = append(params, opts.PerPage, opts.StartIndex-1)
// Execute the query
err = sqlx.SelectContext(ctx, ds.reader(ctx), &users, query, params...)
@@ -719,16 +709,13 @@ func (ds *Datastore) DeleteScimGroup(ctx context.Context, id uint) error {
// ListScimGroups retrieves a list of SCIM groups with pagination
func (ds *Datastore) ListScimGroups(ctx context.Context, opts fleet.ScimListOptions) (groups []fleet.ScimGroup, totalResults uint, err error) {
// Default pagination values if not provided
if opts.Page == 0 {
opts.Page = 1
if opts.StartIndex == 0 {
opts.StartIndex = 1
}
if opts.PerPage == 0 {
opts.PerPage = SCIMDefaultResourcesPerPage
}
// Calculate offset for pagination
offset := (opts.Page - 1) * opts.PerPage
// Build the query
baseQuery := `
SELECT DISTINCT
@@ -745,7 +732,7 @@ func (ds *Datastore) ListScimGroups(ctx context.Context, opts fleet.ScimListOpti
// Add pagination to the main query
query := baseQuery + " ORDER BY scim_groups.id LIMIT ? OFFSET ?"
params := []interface{}{opts.PerPage, offset}
params := []interface{}{opts.PerPage, opts.StartIndex - 1}
// Execute the query
err = sqlx.SelectContext(ctx, ds.reader(ctx), &groups, query, params...)
+22 -22
View File
@@ -521,8 +521,8 @@ func testListScimUsers(t *testing.T, ds *Datastore) {
// Test 1: List all users without filters
allUsers, totalResults, err := ds.ListScimUsers(context.Background(), fleet.ScimUsersListOptions{
ScimListOptions: fleet.ScimListOptions{
Page: 1,
PerPage: 10,
StartIndex: 1,
PerPage: 10,
},
})
require.Nil(t, err)
@@ -553,8 +553,8 @@ func testListScimUsers(t *testing.T, ds *Datastore) {
// Test 2: Pagination - first page with 2 items
page1Users, totalPage1, err := ds.ListScimUsers(context.Background(), fleet.ScimUsersListOptions{
ScimListOptions: fleet.ScimListOptions{
Page: 1,
PerPage: 2,
StartIndex: 1,
PerPage: 2,
},
})
require.Nil(t, err)
@@ -564,8 +564,8 @@ func testListScimUsers(t *testing.T, ds *Datastore) {
// Test 3: Pagination - second page with 2 items
page2Users, totalPage2, err := ds.ListScimUsers(context.Background(), fleet.ScimUsersListOptions{
ScimListOptions: fleet.ScimListOptions{
Page: 2,
PerPage: 2,
StartIndex: 3, // StartIndex is 1-based, so for the second page with 2 items per page, we start at index 3
PerPage: 2,
},
})
require.Nil(t, err)
@@ -582,8 +582,8 @@ func testListScimUsers(t *testing.T, ds *Datastore) {
// Test 4: Filter by username
listUsers, totalListUsers, err := ds.ListScimUsers(context.Background(), fleet.ScimUsersListOptions{
ScimListOptions: fleet.ScimListOptions{
Page: 1,
PerPage: 10,
StartIndex: 1,
PerPage: 10,
},
UserNameFilter: ptr.String("list-test-user2"),
})
@@ -596,8 +596,8 @@ func testListScimUsers(t *testing.T, ds *Datastore) {
// Test 5: Filter by email type and value
homeEmailUsers, totalHomeEmailUsers, err := ds.ListScimUsers(context.Background(), fleet.ScimUsersListOptions{
ScimListOptions: fleet.ScimListOptions{
Page: 1,
PerPage: 10,
StartIndex: 1,
PerPage: 10,
},
EmailTypeFilter: ptr.String("home"),
EmailValueFilter: ptr.String("personal.user2@example.com"),
@@ -611,8 +611,8 @@ func testListScimUsers(t *testing.T, ds *Datastore) {
// Test 6: Filter by email type and value - work emails
workEmailUsers, totalWorkEmailUsers, err := ds.ListScimUsers(context.Background(), fleet.ScimUsersListOptions{
ScimListOptions: fleet.ScimListOptions{
Page: 1,
PerPage: 10,
StartIndex: 1,
PerPage: 10,
},
EmailTypeFilter: ptr.String("work"),
EmailValueFilter: ptr.String("different.user3@example.com"),
@@ -624,8 +624,8 @@ func testListScimUsers(t *testing.T, ds *Datastore) {
// Test 7: No results for non-matching filters
noUsers, totalNoUsers1, err := ds.ListScimUsers(context.Background(), fleet.ScimUsersListOptions{
ScimListOptions: fleet.ScimListOptions{
Page: 1,
PerPage: 10,
StartIndex: 1,
PerPage: 10,
},
UserNameFilter: ptr.String("nonexistent"),
})
@@ -635,8 +635,8 @@ func testListScimUsers(t *testing.T, ds *Datastore) {
noUsers, totalNoUsers2, err := ds.ListScimUsers(context.Background(), fleet.ScimUsersListOptions{
ScimListOptions: fleet.ScimListOptions{
Page: 1,
PerPage: 10,
StartIndex: 1,
PerPage: 10,
},
EmailTypeFilter: ptr.String("nonexistent"),
EmailValueFilter: ptr.String("nonexistent"),
@@ -1030,8 +1030,8 @@ func testListScimGroups(t *testing.T, ds *Datastore) {
// Test 1: List all groups
allGroups, totalResults, err := ds.ListScimGroups(context.Background(), fleet.ScimListOptions{
Page: 1,
PerPage: 10,
StartIndex: 1,
PerPage: 10,
})
require.Nil(t, err)
assert.GreaterOrEqual(t, len(allGroups), 3) // There might be other groups from previous tests
@@ -1051,8 +1051,8 @@ func testListScimGroups(t *testing.T, ds *Datastore) {
// Test 2: Pagination - first page with 2 items
page1Groups, totalPage1, err := ds.ListScimGroups(context.Background(), fleet.ScimListOptions{
Page: 1,
PerPage: 2,
StartIndex: 1,
PerPage: 2,
})
require.Nil(t, err)
assert.Equal(t, 2, len(page1Groups))
@@ -1060,8 +1060,8 @@ func testListScimGroups(t *testing.T, ds *Datastore) {
// Test 3: Pagination - second page with 2 items
page2Groups, totalPage2, err := ds.ListScimGroups(context.Background(), fleet.ScimListOptions{
Page: 2,
PerPage: 2,
StartIndex: 3, // StartIndex is 1-based, so for the second page with 2 items per page, we start at index 3
PerPage: 2,
})
require.Nil(t, err)
assert.GreaterOrEqual(t, len(page2Groups), 1) // At least 1 item on the second page
+2 -2
View File
@@ -25,8 +25,8 @@ type ScimUserEmail struct {
}
type ScimListOptions struct {
// Which page to return (must be positive integer)
Page uint
// 1-based index of the first result to return (must be positive integer)
StartIndex uint
// How many results per page (must be positive integer)
PerPage uint
}
@@ -6,16 +6,15 @@ import (
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
"github.com/fleetdm/fleet/v4/server/mdm/android"
"github.com/fleetdm/fleet/v4/server/service/integrationtest"
"github.com/stretchr/testify/assert"
)
func TestAndroid(t *testing.T) {
s := integrationtest.SetUpSuite(t, "integrationtest.Android")
s := SetUpSuite(t, "integrationtest.Android")
cases := []struct {
name string
fn func(t *testing.T, s *integrationtest.Suite)
fn func(t *testing.T, s *Suite)
}{
{"HappyPath", testHappyPath},
}
@@ -27,14 +26,14 @@ func TestAndroid(t *testing.T) {
}
}
func testHappyPath(t *testing.T, s *integrationtest.Suite) {
func testHappyPath(t *testing.T, s *Suite) {
signupDetails := expectSignupDetails(t, s)
var signupURL android.EnterpriseSignupResponse
s.DoJSON(t, "GET", "/api/v1/fleet/android_enterprise/signup_url", nil, http.StatusOK, &signupURL)
assert.Equal(t, signupURL.Url, signupDetails.Url)
}
func expectSignupDetails(t *testing.T, s *integrationtest.Suite) *android.SignupDetails {
func expectSignupDetails(t *testing.T, s *Suite) *android.SignupDetails {
signupDetails := &android.SignupDetails{
Url: "URL",
Name: "Name",
@@ -0,0 +1,59 @@
package android
import (
"os"
"testing"
"github.com/fleetdm/fleet/v4/server/fleet"
android_mock "github.com/fleetdm/fleet/v4/server/mdm/android/mock"
android_service "github.com/fleetdm/fleet/v4/server/mdm/android/service"
"github.com/fleetdm/fleet/v4/server/service"
"github.com/fleetdm/fleet/v4/server/service/integrationtest"
"github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils"
"github.com/go-kit/log"
"github.com/stretchr/testify/require"
)
type Suite struct {
integrationtest.BaseSuite
AndroidProxy *android_mock.Proxy
}
func SetUpSuite(t *testing.T, uniqueTestName string) *Suite {
ds, redisPool, fleetCfg, fleetSvc, ctx := integrationtest.SetUpDSRedisService(t, uniqueTestName)
logger := log.NewLogfmtLogger(os.Stdout)
proxy := android_mock.Proxy{}
proxy.InitCommonMocks()
androidSvc, err := android_service.NewServiceWithProxy(
logger,
ds,
&proxy,
fleetSvc,
)
require.NoError(t, err)
users, server := service.RunServerForTestsWithServiceWithDS(t, ctx, ds, fleetSvc, &service.TestServerOpts{
License: &fleet.LicenseInfo{
Tier: fleet.TierFree,
},
FleetConfig: &fleetCfg,
Pool: redisPool,
Logger: logger,
FeatureRoutes: []endpoint_utils.HandlerRoutesFunc{android_service.GetRoutes(fleetSvc, androidSvc)},
})
s := &Suite{
BaseSuite: integrationtest.BaseSuite{
Logger: logger,
DS: ds,
FleetCfg: fleetCfg,
Users: users,
Server: server,
},
AndroidProxy: &proxy,
}
integrationtest.SetUpServerURL(t, ds, server)
s.Token = s.GetTestAdminToken(t)
return s
}
+4 -4
View File
@@ -12,7 +12,7 @@ import (
"github.com/stretchr/testify/require"
)
func (s *Suite) DoJSON(t *testing.T, verb, path string, params interface{}, expectedStatusCode int, v interface{}, queryParams ...string) {
func (s *BaseSuite) DoJSON(t *testing.T, verb, path string, params interface{}, expectedStatusCode int, v interface{}, queryParams ...string) {
resp := s.Do(t, verb, path, params, expectedStatusCode, queryParams...)
err := json.UnmarshalRead(resp.Body, v)
require.NoError(t, err)
@@ -21,7 +21,7 @@ func (s *Suite) DoJSON(t *testing.T, verb, path string, params interface{}, expe
}
}
func (s *Suite) Do(t *testing.T, verb, path string, params interface{}, expectedStatusCode int, queryParams ...string) *http.Response {
func (s *BaseSuite) Do(t *testing.T, verb, path string, params interface{}, expectedStatusCode int, queryParams ...string) *http.Response {
j, err := json.Marshal(params)
require.NoError(t, err)
@@ -33,13 +33,13 @@ func (s *Suite) Do(t *testing.T, verb, path string, params interface{}, expected
return resp
}
func (s *Suite) DoRaw(t *testing.T, verb string, path string, rawBytes []byte, expectedStatusCode int, queryParams ...string) *http.Response {
func (s *BaseSuite) DoRaw(t *testing.T, verb string, path string, rawBytes []byte, expectedStatusCode int, queryParams ...string) *http.Response {
return s.DoRawWithHeaders(t, verb, path, rawBytes, expectedStatusCode, map[string]string{
"Authorization": fmt.Sprintf("Bearer %s", s.Token),
}, queryParams...)
}
func (s *Suite) DoRawWithHeaders(
func (s *BaseSuite) DoRawWithHeaders(
t *testing.T, verb string, path string, rawBytes []byte, expectedStatusCode int, headers map[string]string, queryParams ...string,
) *http.Response {
return httptest.DoHTTPReq(t, decodeJSON, verb, rawBytes, s.Server.URL+path, headers, expectedStatusCode, queryParams...)
+21 -78
View File
@@ -3,74 +3,53 @@ package integrationtest
import (
"context"
"net/http/httptest"
"os"
"testing"
"github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
"github.com/fleetdm/fleet/v4/server/datastore/redis/redistest"
"github.com/fleetdm/fleet/v4/server/fleet"
android_mock "github.com/fleetdm/fleet/v4/server/mdm/android/mock"
android_service "github.com/fleetdm/fleet/v4/server/mdm/android/service"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/service"
"github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils"
"github.com/fleetdm/fleet/v4/server/test"
"github.com/go-kit/log"
"github.com/stretchr/testify/require"
)
type Suite struct {
Logger log.Logger
FleetCfg config.FleetConfig
Server *httptest.Server
DS *mysql.Datastore
Users map[string]fleet.User
Token string
AndroidProxy *android_mock.Proxy
type BaseSuite struct {
Logger log.Logger
FleetCfg config.FleetConfig
Server *httptest.Server
DS *mysql.Datastore
Users map[string]fleet.User
Token string
cachedAdminToken string
}
var testUsers = map[string]struct {
Email string
PlaintextPassword string
GlobalRole *string
}{
"admin1": {
PlaintextPassword: test.GoodPassword,
Email: "admin1@example.com",
GlobalRole: ptr.String(fleet.RoleAdmin),
},
"user1": {
PlaintextPassword: test.GoodPassword,
Email: "user1@example.com",
GlobalRole: ptr.String(fleet.RoleMaintainer),
},
"user2": {
PlaintextPassword: test.GoodPassword,
Email: "user2@example.com",
GlobalRole: ptr.String(fleet.RoleObserver),
},
}
func (s *Suite) GetTestAdminToken(t *testing.T) string {
testUser := testUsers["admin1"]
func (s *BaseSuite) GetTestAdminToken(t *testing.T) string {
// because the login endpoint is rate-limited, use the cached admin token
// if available (if for some reason a test needs to logout the admin user,
// then set cachedAdminToken = "" so that a new token is retrieved).
if s.cachedAdminToken == "" {
s.cachedAdminToken = s.GetTestToken(t, testUser.Email, testUser.PlaintextPassword)
s.cachedAdminToken = s.GetTestToken(t, service.TestAdminUserEmail, test.GoodPassword)
}
return s.cachedAdminToken
}
func (s *Suite) GetTestToken(t *testing.T, email string, password string) string {
func (s *BaseSuite) GetTestToken(t *testing.T, email string, password string) string {
return service.GetToken(t, email, password, s.Server.URL)
}
func SetUpSuite(t *testing.T, uniqueTestName string) *Suite {
func SetUpServerURL(t *testing.T, ds *mysql.Datastore, server *httptest.Server) {
appConf, err := ds.AppConfig(t.Context())
require.NoError(t, err)
appConf.ServerSettings.ServerURL = server.URL
err = ds.SaveAppConfig(t.Context(), appConf)
require.NoError(t, err)
}
func SetUpDSRedisService(t *testing.T, uniqueTestName string) (*mysql.Datastore, fleet.RedisPool, config.FleetConfig,
fleet.Service, context.Context) {
ds := mysql.CreateMySQLDS(t)
test.AddAllHostsLabel(t, ds)
@@ -85,44 +64,8 @@ func SetUpSuite(t *testing.T, uniqueTestName string) *Suite {
redisPool := redistest.SetupRedis(t, uniqueTestName, false, false, false)
fleetCfg := config.TestConfig()
logger := log.NewLogfmtLogger(os.Stdout)
fleetSvc, ctx := service.NewTestService(t, ds, fleetCfg)
proxy := android_mock.Proxy{}
proxy.InitCommonMocks()
androidSvc, err := android_service.NewServiceWithProxy(
logger,
ds,
&proxy,
fleetSvc,
)
require.NoError(t, err)
users, server := service.RunServerForTestsWithServiceWithDS(t, ctx, ds, fleetSvc, &service.TestServerOpts{
License: &fleet.LicenseInfo{
Tier: fleet.TierFree,
},
FleetConfig: &fleetCfg,
Pool: redisPool,
Logger: logger,
FeatureRoutes: []endpoint_utils.HandlerRoutesFunc{android_service.GetRoutes(fleetSvc, androidSvc)},
})
s := &Suite{
Logger: logger,
DS: ds,
FleetCfg: fleetCfg,
Users: users,
Server: server,
AndroidProxy: &proxy,
}
appConf, err = ds.AppConfig(ctx)
require.NoError(t, err)
appConf.ServerSettings.ServerURL = server.URL
err = ds.SaveAppConfig(ctx, appConf)
require.NoError(t, err)
s.Token = s.GetTestAdminToken(t)
return s
return ds, redisPool, fleetCfg, fleetSvc, ctx
}
func testContext() context.Context {
+16 -9
View File
@@ -10,12 +10,12 @@ import (
"net/http/httptest"
"os"
"sort"
"strings"
"sync"
"testing"
"time"
"github.com/WatchBeam/clock"
"github.com/fleetdm/fleet/v4/ee/server/scim"
eeservice "github.com/fleetdm/fleet/v4/ee/server/service"
"github.com/fleetdm/fleet/v4/ee/server/service/digicert"
"github.com/fleetdm/fleet/v4/server/config"
@@ -255,15 +255,11 @@ func createTestUsers(t *testing.T, ds fleet.Datastore) map[string]fleet.User {
userID := uint(1)
for _, key := range keys {
u := testUsers[key]
role := fleet.RoleObserver
if strings.Contains(u.Email, "admin") {
role = fleet.RoleAdmin
}
user := &fleet.User{
ID: userID, // We need to set this in case ds is a mocked Datastore.
Name: "Test Name " + u.Email,
Email: u.Email,
GlobalRole: &role,
GlobalRole: u.GlobalRole,
}
err := user.SetPassword(u.PlaintextPassword, 10, 10)
require.Nil(t, err)
@@ -275,6 +271,12 @@ func createTestUsers(t *testing.T, ds fleet.Datastore) map[string]fleet.User {
return users
}
const (
TestAdminUserEmail = "admin1@example.com"
TestMaintainerUserEmail = "user1@example.com"
TestObserverUserEmail = "user2@example.com"
)
var testUsers = map[string]struct {
Email string
PlaintextPassword string
@@ -282,17 +284,17 @@ var testUsers = map[string]struct {
}{
"admin1": {
PlaintextPassword: test.GoodPassword,
Email: "admin1@example.com",
Email: TestAdminUserEmail,
GlobalRole: ptr.String(fleet.RoleAdmin),
},
"user1": {
PlaintextPassword: test.GoodPassword,
Email: "user1@example.com",
Email: TestMaintainerUserEmail,
GlobalRole: ptr.String(fleet.RoleMaintainer),
},
"user2": {
PlaintextPassword: test.GoodPassword,
Email: "user2@example.com",
Email: TestObserverUserEmail,
GlobalRole: ptr.String(fleet.RoleObserver),
},
}
@@ -353,6 +355,7 @@ type TestServerOpts struct {
FeatureRoutes []endpoint_utils.HandlerRoutesFunc
SCEPConfigService fleet.SCEPConfigService
DigiCertService fleet.DigiCertService
EnableSCIM bool
}
func RunServerForTestsWithDS(t *testing.T, ds fleet.Datastore, opts ...*TestServerOpts) (map[string]fleet.User, *httptest.Server) {
@@ -451,6 +454,10 @@ func RunServerForTestsWithServiceWithDS(t *testing.T, ctx context.Context, ds fl
debugHandler := MakeDebugHandler(svc, cfg, logger, errHandler, ds)
rootMux.Handle("/debug/", debugHandler)
if len(opts) > 0 && opts[0].EnableSCIM {
require.NoError(t, scim.RegisterSCIM(rootMux, ds, svc, logger))
}
server := httptest.NewUnstartedServer(rootMux)
server.Config = cfg.Server.DefaultHTTPServer(ctx, rootMux)
// WriteTimeout is set for security purposes.