Skip to content

Commit

Permalink
Added integration test.
Browse files Browse the repository at this point in the history
  • Loading branch information
getvictor committed Jan 7, 2025
1 parent 143fbbd commit 9bc0bc9
Show file tree
Hide file tree
Showing 4 changed files with 311 additions and 1 deletion.
19 changes: 19 additions & 0 deletions server/datastore/s3/software_installer.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,22 @@ func NewSoftwareInstallerStore(config config.S3Config) (*SoftwareInstallerStore,
},
}, nil
}

// NewTestSoftwareInstallerStore is used in tests.
func NewTestSoftwareInstallerStore(conf config.S3Config) (*SoftwareInstallerStore, error) {
store := &s3store{
bucket: "test-bucket",
cloudFrontConfig: &config.S3CloudFrontConfig{
BaseURL: conf.SoftwareInstallersCloudFrontURL,
SigningPublicKeyID: conf.SoftwareInstallersCloudFrontURLSigningPublicKeyID,
Signer: conf.SoftwareInstallersCloudFrontSigner,
},
}
return &SoftwareInstallerStore{
&commonFileStore{
s3store: store,
pathPrefix: softwareInstallersPrefix,
fileLabel: "software installer",
},
}, nil
}
3 changes: 2 additions & 1 deletion server/mock/datastore.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import (
//go:generate go run ./mockimpl/impl.go -o nanodep/storage.go "s *Storage" "github.com/fleetdm/fleet/v4/server/mdm/nanodep/storage.AllDEPStorage"
//go:generate go run ./mockimpl/impl.go -o mdm/datastore_mdm_mock.go "fs *MDMAppleStore" "fleet.MDMAppleStore"
//go:generate go run ./mockimpl/impl.go -o scep/depot.go "d *Depot" "depot.Depot"
//go:generate go run ./mockimpl/impl.go -o mdm/bootstrap_package_store.go "fs *MDMBootstrapPackageStore" "fleet.MDMBootstrapPackageStore"
//go:generate go run ./mockimpl/impl.go -o mdm/bootstrap_package_store.go "s *MDMBootstrapPackageStore" "fleet.MDMBootstrapPackageStore"
//go:generate go run ./mockimpl/impl.go -o software/software_installer_store.go "s *SoftwareInstallerStore" "fleet.SoftwareInstallerStore"

var _ fleet.Datastore = (*Store)(nil)

Expand Down
78 changes: 78 additions & 0 deletions server/mock/software/software_installer_store.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Automatically generated by mockimpl. DO NOT EDIT!

package mock

import (
"context"
"io"
"sync"
"time"

"github.com/fleetdm/fleet/v4/server/fleet"
)

var _ fleet.SoftwareInstallerStore = (*SoftwareInstallerStore)(nil)

type GetFunc func(ctx context.Context, installerID string) (io.ReadCloser, int64, error)

type PutFunc func(ctx context.Context, installerID string, content io.ReadSeeker) error

type ExistsFunc func(ctx context.Context, installerID string) (bool, error)

type CleanupFunc func(ctx context.Context, usedInstallerIDs []string, removeCreatedBefore time.Time) (int, error)

type SignFunc func(ctx context.Context, fileID string) (string, error)

type SoftwareInstallerStore struct {
GetFunc GetFunc
GetFuncInvoked bool

PutFunc PutFunc
PutFuncInvoked bool

ExistsFunc ExistsFunc
ExistsFuncInvoked bool

CleanupFunc CleanupFunc
CleanupFuncInvoked bool

SignFunc SignFunc
SignFuncInvoked bool

mu sync.Mutex
}

func (s *SoftwareInstallerStore) Get(ctx context.Context, installerID string) (io.ReadCloser, int64, error) {
s.mu.Lock()
s.GetFuncInvoked = true
s.mu.Unlock()
return s.GetFunc(ctx, installerID)
}

func (s *SoftwareInstallerStore) Put(ctx context.Context, installerID string, content io.ReadSeeker) error {
s.mu.Lock()
s.PutFuncInvoked = true
s.mu.Unlock()
return s.PutFunc(ctx, installerID, content)
}

func (s *SoftwareInstallerStore) Exists(ctx context.Context, installerID string) (bool, error) {
s.mu.Lock()
s.ExistsFuncInvoked = true
s.mu.Unlock()
return s.ExistsFunc(ctx, installerID)
}

func (s *SoftwareInstallerStore) Cleanup(ctx context.Context, usedInstallerIDs []string, removeCreatedBefore time.Time) (int, error) {
s.mu.Lock()
s.CleanupFuncInvoked = true
s.mu.Unlock()
return s.CleanupFunc(ctx, usedInstallerIDs, removeCreatedBefore)
}

func (s *SoftwareInstallerStore) Sign(ctx context.Context, fileID string) (string, error) {
s.mu.Lock()
s.SignFuncInvoked = true
s.mu.Unlock()
return s.SignFunc(ctx, fileID)
}
212 changes: 212 additions & 0 deletions server/service/integration_install_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
package service

import (
"context"
"crypto/rand"
"crypto/rsa"
"errors"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"testing"

"github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
"github.com/fleetdm/fleet/v4/server/datastore/s3"
"github.com/fleetdm/fleet/v4/server/fleet"
software_mock "github.com/fleetdm/fleet/v4/server/mock/software"
"github.com/go-kit/log"
kitlog "github.com/go-kit/log"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)

func TestIntegrationsInstall(t *testing.T) {
testingSuite := new(integrationInstallTestSuite)
testingSuite.withServer.s = &testingSuite.Suite
suite.Run(t, testingSuite)
}

type integrationInstallTestSuite struct {
withServer
suite.Suite
softwareInstallStore *software_mock.SoftwareInstallerStore
}

func (s *integrationInstallTestSuite) SetupSuite() {
s.withDS.SetupSuite("integrationInstallTestSuite")

// Create a mock S3 software install store
softwareInstallStore := &software_mock.SoftwareInstallerStore{}
s.softwareInstallStore = softwareInstallStore

fleetConfig := config.TestConfig()
signer, _ := rsa.GenerateKey(rand.Reader, 2048)
fleetConfig.S3.SoftwareInstallersCloudFrontSigner = signer
installConfig := TestServerOpts{
License: &fleet.LicenseInfo{
Tier: fleet.TierPremium,
},
Logger: log.NewLogfmtLogger(os.Stdout),
EnableCachedDS: true,
SoftwareInstallStore: softwareInstallStore,
FleetConfig: &fleetConfig,
}
if os.Getenv("FLEET_INTEGRATION_TESTS_DISABLE_LOG") != "" {
installConfig.Logger = kitlog.NewNopLogger()
}
users, server := RunServerForTestsWithDS(s.T(), s.ds, &installConfig)
s.server = server
s.users = users
s.token = s.getTestAdminToken()
s.cachedTokens = make(map[string]string)
}

func (s *integrationInstallTestSuite) TearDownTest() {
s.withServer.commonTearDownTest(s.T())
}

// TestSoftwareInstallerSignedURL tests that the software installer signed URL is returned.
// We are
func (s *integrationInstallTestSuite) TestSoftwareInstallerSignedURL() {
t := s.T()

openFile := func(name string) *os.File {
f, err := os.Open(filepath.Join("testdata", "software-installers", name))
require.NoError(t, err)
return f
}

filename := "ruby.deb"
var expectBytes []byte
var expectLen int
f := openFile(filename)
st, err := f.Stat()
require.NoError(t, err)
expectLen = int(st.Size())
require.Equal(t, expectLen, 11340)
expectBytes = make([]byte, expectLen)
n, err := f.Read(expectBytes)
require.NoError(t, err)
require.Equal(t, n, expectLen)
f.Close()

// Set up mocks
var myInstallerID string
s.softwareInstallStore.ExistsFunc = func(ctx context.Context, installerID string) (bool, error) {
return installerID == myInstallerID, nil
}
s.softwareInstallStore.PutFunc = func(ctx context.Context, installerID string, content io.ReadSeeker) error {
myInstallerID = installerID
return nil
}
s.softwareInstallStore.SignFunc = func(ctx context.Context, fileID string) (string, error) {
return "https://example.com/signed", nil
}

var createTeamResp teamResponse
s.DoJSON("POST", "/api/latest/fleet/teams", &fleet.Team{
Name: t.Name(),
}, http.StatusOK, &createTeamResp)
require.NotZero(t, createTeamResp.Team.ID)

payload := &fleet.UploadSoftwareInstallerPayload{
TeamID: &createTeamResp.Team.ID,
InstallScript: "another install script",
PreInstallQuery: "another pre install query",
PostInstallScript: "another post install script",
Filename: filename,
// additional fields below are pre-populated so we can re-use the payload later for the test assertions
Title: "ruby",
Version: "1:2.5.1",
Source: "deb_packages",
StorageID: "df06d9ce9e2090d9cb2e8cd1f4d7754a803dc452bf93e3204e3acd3b95508628",
Platform: "linux",
SelfService: true,
}
s.uploadSoftwareInstaller(t, payload, http.StatusOK, "")

// check the software installer
var id uint
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(context.Background(), q, &id,
`SELECT id FROM software_installers WHERE global_or_team_id = ? AND filename = ?`, payload.TeamID, payload.Filename)
})
require.NotZero(t, id)

meta, err := s.ds.GetSoftwareInstallerMetadataByID(context.Background(), id)
require.NoError(t, err)
titleID := *meta.TitleID

// create an orbit host, assign to team
hostInTeam := createOrbitEnrolledHost(t, "linux", "orbit-host-team", s.ds)
require.NoError(t, s.ds.AddHostsToTeam(context.Background(), &createTeamResp.Team.ID, []uint{hostInTeam.ID}))

// Create a software installation request
s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", hostInTeam.ID, titleID), installSoftwareRequest{},
http.StatusAccepted)

// Get the InstallerUUID
var installUUID string
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(context.Background(), q, &installUUID,
"SELECT execution_id FROM host_software_installs WHERE host_id = ?", hostInTeam.ID)
})

// Fetch installer details
var orbitSoftwareResp orbitGetSoftwareInstallResponse
s.DoJSON("POST", "/api/fleet/orbit/software_install/details", orbitGetSoftwareInstallRequest{
InstallUUID: installUUID,
OrbitNodeKey: *hostInTeam.OrbitNodeKey,
}, http.StatusOK, &orbitSoftwareResp)
assert.Equal(t, meta.InstallerID, orbitSoftwareResp.InstallerID)
require.NotNil(t, orbitSoftwareResp.SoftwareInstallerURL)
assert.Equal(t, "https://example.com/signed", orbitSoftwareResp.SoftwareInstallerURL.URL)
require.Equal(t, filename, orbitSoftwareResp.SoftwareInstallerURL.Filename)

// Error in signing -- we simply don't return the URL
s.softwareInstallStore.SignFunc = func(ctx context.Context, fileID string) (string, error) {
return "", errors.New("error signing")
}
orbitSoftwareResp = orbitGetSoftwareInstallResponse{}
s.DoJSON("POST", "/api/fleet/orbit/software_install/details", orbitGetSoftwareInstallRequest{
InstallUUID: installUUID,
OrbitNodeKey: *hostInTeam.OrbitNodeKey,
}, http.StatusOK, &orbitSoftwareResp)
assert.Equal(t, meta.InstallerID, orbitSoftwareResp.InstallerID)
assert.Nil(t, orbitSoftwareResp.SoftwareInstallerURL)

// Now test with the real sign function
signer, _ := rsa.GenerateKey(rand.Reader, 2048)

s3Config := config.S3Config{
SoftwareInstallersCloudFrontURL: "https://example.cloudfront.net",
SoftwareInstallersCloudFrontURLSigningPublicKeyID: "ABC123XYZ",
SoftwareInstallersCloudFrontSigner: signer,
}
s3Store, err := s3.NewTestSoftwareInstallerStore(s3Config)
require.NoError(t, err)
s.softwareInstallStore.SignFunc = func(ctx context.Context, fileID string) (string, error) {
return s3Store.Sign(ctx, fileID)
}
s.DoJSON("POST", "/api/fleet/orbit/software_install/details", orbitGetSoftwareInstallRequest{
InstallUUID: installUUID,
OrbitNodeKey: *hostInTeam.OrbitNodeKey,
}, http.StatusOK, &orbitSoftwareResp)
assert.Equal(t, meta.InstallerID, orbitSoftwareResp.InstallerID)
require.NotNil(t, orbitSoftwareResp.SoftwareInstallerURL)
assert.True(t,
strings.HasPrefix(orbitSoftwareResp.SoftwareInstallerURL.URL,
s3Config.SoftwareInstallersCloudFrontURL+"/software-installers/"+payload.StorageID+"?Expires="),
orbitSoftwareResp.SoftwareInstallerURL.URL)
assert.Contains(t, orbitSoftwareResp.SoftwareInstallerURL.URL, "&Signature=")
assert.Contains(t, orbitSoftwareResp.SoftwareInstallerURL.URL,
"&Key-Pair-Id="+s3Config.SoftwareInstallersCloudFrontURLSigningPublicKeyID)
require.Equal(t, filename, orbitSoftwareResp.SoftwareInstallerURL.Filename)

}

0 comments on commit 9bc0bc9

Please sign in to comment.