Fix some error strings in application datastore which still referred to devices.

Add method SaveApplicationByDeviceUUID to add rows to `devices_applications` for the specified device uuid
Add method NewDatastore since NewDB was untestable using mocks.
Connect service now takes an applications datastore as one of its parameters.
ackInstalledApplicationList implemented but not tested for InstallApplicationList responses.
This commit is contained in:
Mosen
2016-07-17 00:21:14 +10:00
parent 1cc09e3938
commit 97a97d9378
4 changed files with 329 additions and 12 deletions

View File

@@ -15,10 +15,16 @@ type Datastore interface {
New(a *Application) (string, error)
Applications(params ...interface{}) ([]Application, error)
GetApplicationsByDeviceUUID(deviceUUID string) (*[]Application, error)
SaveApplicationByDeviceUUID(deviceUUID string, app *Application) error
}
type pgStore struct {
*sqlx.DB
logger kitlog.Logger
}
func NewDatastore(connection *sqlx.DB, logger kitlog.Logger) (Datastore, error) {
return pgStore{DB: connection, logger: logger}, nil
}
func NewDB(driver, conn string, logger kitlog.Logger) (Datastore, error) {
@@ -26,7 +32,7 @@ func NewDB(driver, conn string, logger kitlog.Logger) (Datastore, error) {
case "postgres":
db, err := sqlx.Open(driver, conn)
if err != nil {
return nil, errors.Wrap(err, "device datastore")
return nil, errors.Wrap(err, "applications datastore")
}
var dbError error
maxAttempts := 20
@@ -39,7 +45,7 @@ func NewDB(driver, conn string, logger kitlog.Logger) (Datastore, error) {
time.Sleep(time.Duration(attempts) * time.Second)
}
if dbError != nil {
return nil, errors.Wrap(dbError, "device datastore")
return nil, errors.Wrap(dbError, "applications datastore")
}
return pgStore{DB: db}, nil
default:
@@ -83,13 +89,13 @@ func (store pgStore) New(a *Application) (string, error) {
dynamic_size,
is_validated
)
VALUES ($1, $2, $3, $4, $5, $6, $7)
VALUES ($0, $1, $2, $3, $4, $5, $6)
ON CONFLICT (name, version) DO UPDATE SET
identifier=$2,
short_version=$3,
bundle_size=$5,
dynamic_size=$6,
is_validated=$7
identifier=$1,
short_version=$2,
bundle_size=$4,
dynamic_size=$5,
is_validated=$6
RETURNING application_uuid;`,
a.Name,
a.Identifier,
@@ -135,6 +141,16 @@ func (store pgStore) GetApplicationsByDeviceUUID(deviceUUID string) (*[]Applicat
return &apps, nil
}
// Associate the given applications with the given device uuid by inserting into `device_applications`.
func (store pgStore) SaveApplicationByDeviceUUID(deviceUUID string, app *Application) error {
stmt := `INSERT INTO devices_applications (
device_uuid, application_uuid
) VALUES ($1, $2)`
_, err := store.Exec(stmt, deviceUUID, app.UUID)
return err
}
// whereer is for building args passed into a method which finds resources
type whereer interface {
where() string

View File

@@ -1 +1,241 @@
package applications
import (
"database/sql"
"github.com/go-kit/kit/log"
"github.com/jmoiron/sqlx"
"gopkg.in/DATA-DOG/go-sqlmock.v1"
"testing"
)
const MockUUID string = "ABCD-EFGH-IJKL"
const MockName string = "Mock Application"
//func TestNewDB(t *testing.T) {
// var log log.Logger = log.NewNopLogger()
// appsDB, err := NewDB("postgres", "host=localhost", log)
//
// if err != nil {
// t.Error(err)
// }
//
// if _, ok := appsDB.(Datastore); !ok {
// t.Log("Did not get a datastore")
// t.Fail()
// }
//}
func TestNewDatastore(t *testing.T) {
var log log.Logger = log.NewNopLogger()
db, _, err := sqlmock.New()
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
}
dbx := sqlx.NewDb(db, "mock")
defer dbx.Close()
if _, err := NewDatastore(dbx, log); err != nil {
t.Error(err)
}
}
func TestNewApplication(t *testing.T) {
var log log.Logger = log.NewNopLogger()
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
}
dbx := sqlx.NewDb(db, "mock")
defer dbx.Close()
appsDs, err := NewDatastore(dbx, log)
if err != nil {
t.Error(err)
}
// macOS style: no DynamicSize, no IsValidated
fixture := Application{
Name: "Keychain Access",
Identifier: sql.NullString{"com.apple.keychainaccess", true},
ShortVersion: sql.NullString{"9.0", true},
Version: sql.NullString{"9.0", true},
BundleSize: sql.NullInt64{14166172, true},
}
newRow := sqlmock.NewRows([]string{"application_uuid"}).AddRow(MockUUID)
mock.ExpectQuery("INSERT INTO applications").WithArgs(
fixture.Name,
fixture.Identifier.String,
fixture.ShortVersion.String,
fixture.Version.String,
fixture.BundleSize.Int64,
nil,
nil,
).WillReturnRows(newRow)
appUuid, err := appsDs.New(&fixture)
if err != nil {
t.Error(err)
}
if appUuid != MockUUID {
t.Errorf("inserting a mock application did not return the mock uuid, got: %s", appUuid)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled expectations: %s", err)
}
}
func TestApplications(t *testing.T) {
var log log.Logger = log.NewNopLogger()
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
}
dbx := sqlx.NewDb(db, "mock")
defer dbx.Close()
appsDs, err := NewDatastore(dbx, log)
if err != nil {
t.Error(err)
}
mock.ExpectQuery(`SELECT .* FROM applications`).WillReturnRows(
sqlmock.NewRows([]string{"application_uuid"}),
)
if _, err := appsDs.Applications(); err != nil {
t.Error(err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled expectations: %s", err)
}
}
func TestApplicationsWhereUUID(t *testing.T) {
var log log.Logger = log.NewNopLogger()
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
}
dbx := sqlx.NewDb(db, "mock")
defer dbx.Close()
appsDs, err := NewDatastore(dbx, log)
if err != nil {
t.Error(err)
}
mockRow := sqlmock.NewRows([]string{"application_uuid"}).AddRow(MockUUID)
mock.ExpectQuery(`WHERE application_uuid =`).WithArgs(MockUUID).WillReturnRows(mockRow)
apps, err := appsDs.Applications(UUID{MockUUID})
if err != nil {
t.Error(err)
}
if len(apps) != 1 {
t.Fatalf("unexpected number of results returned: %d", len(apps))
}
if apps[0].UUID != MockUUID {
t.Errorf("unexpected application uuid when querying: %s", apps[0].UUID)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled expectations: %s", err)
}
}
func TestApplicationsWhereName(t *testing.T) {
var log log.Logger = log.NewNopLogger()
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
}
dbx := sqlx.NewDb(db, "mock")
defer dbx.Close()
appsDs, err := NewDatastore(dbx, log)
if err != nil {
t.Error(err)
}
mockRow := sqlmock.NewRows([]string{"application_uuid", "name"}).AddRow(MockUUID, MockName)
mock.ExpectQuery(`WHERE name =`).WithArgs(MockName).WillReturnRows(mockRow)
apps, err := appsDs.Applications(Name{MockName})
if err != nil {
t.Error(err)
}
if len(apps) != 1 {
t.Fatalf("unexpected number of results returned: %d", len(apps))
}
if apps[0].UUID != MockUUID {
t.Errorf("unexpected application uuid when querying: %s", apps[0].UUID)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled expectations: %s", err)
}
}
func TestGetApplicationsByDeviceUUID(t *testing.T) {
var log log.Logger = log.NewNopLogger()
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
}
dbx := sqlx.NewDb(db, "mock")
defer dbx.Close()
appsDs, err := NewDatastore(dbx, log)
if err != nil {
t.Error(err)
}
mockRow := sqlmock.NewRows([]string{"application_uuid", "name"}).AddRow(MockUUID, MockName)
mock.ExpectQuery(`WHERE devices_applications.device_uuid=`).WithArgs(MockUUID).WillReturnRows(mockRow)
if _, err := appsDs.GetApplicationsByDeviceUUID(MockUUID); err != nil {
t.Error(err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled expectations: %s", err)
}
}
func TestSaveApplicationByDeviceUUID(t *testing.T) {
var log log.Logger = log.NewNopLogger()
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
}
dbx := sqlx.NewDb(db, "mock")
defer dbx.Close()
appsDs, err := NewDatastore(dbx, log)
if err != nil {
t.Error(err)
}
fixture := Application{
Name: "Keychain Access",
Identifier: sql.NullString{"com.apple.keychainaccess", true},
ShortVersion: sql.NullString{"9.0", true},
Version: sql.NullString{"9.0", true},
BundleSize: sql.NullInt64{14166172, true},
}
if err := appsDs.SaveApplicationByDeviceUUID(MockUUID, &fixture); err != nil {
t.Error(err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled expectations: %s", err)
}
}

View File

@@ -2,7 +2,9 @@ package connect
import (
"encoding/json"
"fmt"
"github.com/micromdm/mdm"
"github.com/micromdm/micromdm/applications"
"github.com/micromdm/micromdm/command"
"github.com/micromdm/micromdm/device"
"github.com/pkg/errors"
@@ -18,15 +20,17 @@ type Service interface {
}
// NewService creates a mdm service
func NewService(devices device.Datastore, cs command.Service) Service {
func NewService(devices device.Datastore, apps applications.Datastore, cs command.Service) Service {
return &service{
commands: cs,
devices: devices,
apps: apps,
}
}
type service struct {
devices device.Datastore
apps applications.Datastore
commands command.Service
}
@@ -126,6 +130,60 @@ func (svc service) ackQueryResponses(req mdm.Response) error {
return svc.devices.Save("queryResponses", &existing)
}
//func (svc service) ackInstalledApplicationList(req mdm.Response) error {
//
//}
// Acknowledge a response to `InstalledApplicationList`.
func (svc service) ackInstalledApplicationList(req mdm.Response) error {
device, err := svc.devices.GetDeviceByUDID(req.UDID)
if err != nil {
return err
}
deviceApps, err := svc.apps.GetApplicationsByDeviceUUID(device.UUID)
if err != nil {
return err
}
// Any installed applications that are already represented in the applications datastore should be skipped.
var updated []applications.Application = make([]applications.Application, len(req.InstalledApplicationList))
skip:
for _, ackApp := range req.InstalledApplicationList {
for _, app := range *deviceApps {
if app.Name == ackApp.Name && app.Version == ackApp.Version {
continue skip
}
}
updated = append(updated, ackApp)
}
if len(updated) == 0 {
return nil
}
// Determine applications which we have no record of at all, then insert them (find or create).
for _, newApp := range updated {
existing, err := svc.apps.Applications(applications.Name{newApp.Name}, applications.Version{newApp.Version})
if err != nil {
return err
}
switch {
case len(existing) > 1:
return fmt.Errorf("expected a single application match for application name: %s, got %d results", newApp.Name, len(existing))
case len(existing) == 0: // No record exists and therefore both the application row and device association must be created.
appUuid, err := svc.apps.New(newApp)
if err != nil {
return err
}
newApp.UUID = appUuid
}
// For both len(existing) == 0 and len(existing) == 1, the row must be inserted for devices_applications.
if err := svc.apps.SaveApplicationByDeviceUUID(device.UUID, newApp.UUID); err != nil {
return err
}
}
return nil
}

View File

@@ -204,12 +204,15 @@ func main() {
commandSvc := command.NewService(commandDB)
checkinSvc := checkin.NewService(deviceDB, mgmtSvc, commandSvc, enrollmentProfile)
connectSvc := connect.NewService(deviceDB, commandSvc)
connectSvc := connect.NewService(deviceDB, appsDB, commandSvc)
enrollSvc, _ := enroll.NewService(*flPushCert, *flPushPass, *flTLSCACert, *flSCEPURL, *flURL)
httpLogger := log.NewContext(logger).With("component", "http")
managementHandler := management.ServiceHandler(ctx, mgmtSvc, httpLogger)
commandHandler := command.ServiceHandler(ctx, commandSvc, httpLogger)
checkinHandler := checkin.ServiceHandler(ctx, checkinSvc, httpLogger)
connectHandler := connect.ServiceHandler(ctx, connectSvc, httpLogger)
enrollHandler := enroll.ServiceHandler(ctx, enrollSvc, httpLogger)
mux := http.NewServeMux()