fleet/server/datastore/mysql/android_test.go
Victor Lyuboslavsky a86253d2bf
Refactoring Android datastore interface (#26982)
For #26219 

Refactoring the interface between Android service and Android datastore
to use 1 common datastore interface: `fleet.AndroidDatastore`

These changes are based on feedback from the recent Backend Sync.

```mermaid
---
title: Partial class diagram
---
classDiagram
    direction LR
    class `android.Service`
    <<interface>> `android.Service`
    class `android/service.Service`
    `android/service.Service` ..|> `android.Service`: implements

    class `fleet.AndroidDatastore`
    <<interface>> `fleet.AndroidDatastore`
    class `fleet.Datastore`
    <<interface>> `fleet.Datastore`
    class `android.Datastore`
    <<interface>> `android.Datastore`
    `android/service.Service` *-- `fleet.AndroidDatastore`: USES (THIS IS THE KEY CHANGE)
    `fleet.Datastore` *-- `fleet.AndroidDatastore`: contains
    `mysql.Datastore` ..|> `fleet.Datastore`: implements
    `fleet.AndroidDatastore` *-- `android.Datastore`: contains
    `mysql.Datastore` *-- `android.Datastore`: contains
    `android/mysql.Datastore` ..|> `android.Datastore`: implements
```
2025-03-13 14:28:52 -05:00

289 lines
9.3 KiB
Go

package mysql
import (
"context"
"testing"
"time"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm/android"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/test"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAndroid(t *testing.T) {
ds := CreateMySQLDS(t)
TruncateTables(t, ds)
cases := []struct {
name string
fn func(t *testing.T, ds *Datastore)
}{
{"NewAndroidHost", testNewAndroidHost},
{"UpdateAndroidHost", testUpdateAndroidHost},
{"AndroidMDMStats", testAndroidMDMStats},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
defer TruncateTables(t, ds)
c.fn(t, ds)
})
}
}
func testNewAndroidHost(t *testing.T, ds *Datastore) {
test.AddBuiltinLabels(t, ds)
const enterpriseSpecificID = "enterprise_specific_id"
host := createAndroidHost(enterpriseSpecificID)
result, err := ds.NewAndroidHost(testCtx(), host)
require.NoError(t, err)
assert.NotZero(t, result.Host.ID)
assert.NotZero(t, result.Device.ID)
lbls, err := ds.ListLabelsForHost(testCtx(), result.Host.ID)
require.NoError(t, err)
require.Len(t, lbls, 2)
names := []string{lbls[0].Name, lbls[1].Name}
require.ElementsMatch(t, []string{fleet.BuiltinLabelNameAllHosts, fleet.BuiltinLabelNameAndroid}, names)
resultLite, err := ds.AndroidHostLite(testCtx(), enterpriseSpecificID)
require.NoError(t, err)
assert.Equal(t, result.Host.ID, resultLite.Host.ID)
assert.Equal(t, result.Device.ID, resultLite.Device.ID)
// Inserting the same host again should be fine.
// This may occur when 2 Fleet servers received the same host information via pubsub.
resultCopy, err := ds.NewAndroidHost(testCtx(), host)
require.NoError(t, err)
assert.Equal(t, result.Host.ID, resultCopy.Host.ID)
assert.Equal(t, result.Device.ID, resultCopy.Device.ID)
// create another host, this time delete the Android label
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(testCtx(), `DELETE FROM labels WHERE name = ?`, fleet.BuiltinLabelNameAndroid)
return err
})
const enterpriseSpecificID2 = "enterprise_specific_id2"
host2 := createAndroidHost(enterpriseSpecificID2)
// still passes, but no label membership was recorded
result, err = ds.NewAndroidHost(testCtx(), host2)
require.NoError(t, err)
lbls, err = ds.ListLabelsForHost(testCtx(), result.Host.ID)
require.NoError(t, err)
require.Empty(t, lbls)
}
func createAndroidHost(enterpriseSpecificID string) *fleet.AndroidHost {
host := &fleet.AndroidHost{
Host: &fleet.Host{
Hostname: "hostname",
ComputerName: "computer_name",
Platform: "android",
OSVersion: "Android 14",
Build: "build",
Memory: 1024,
TeamID: nil,
HardwareSerial: "hardware_serial",
CPUType: "cpu_type",
HardwareModel: "hardware_model",
HardwareVendor: "hardware_vendor",
},
Device: &android.Device{
DeviceID: "device_id",
EnterpriseSpecificID: ptr.String(enterpriseSpecificID),
AndroidPolicyID: ptr.Uint(1),
LastPolicySyncTime: ptr.Time(time.Time{}),
},
}
host.SetNodeKey(enterpriseSpecificID)
return host
}
func testCtx() context.Context {
return context.Background()
}
func testUpdateAndroidHost(t *testing.T, ds *Datastore) {
const enterpriseSpecificID = "es_id_update"
host := createAndroidHost(enterpriseSpecificID)
result, err := ds.NewAndroidHost(testCtx(), host)
require.NoError(t, err)
assert.NotZero(t, result.Host.ID)
assert.NotZero(t, result.Device.ID)
// Dummy update
err = ds.UpdateAndroidHost(testCtx(), result, false)
require.NoError(t, err)
host = result
host.Host.DetailUpdatedAt = time.Now()
host.Host.LabelUpdatedAt = time.Now()
host.Host.Hostname = "hostname_updated"
host.Host.ComputerName = "computer_name_updated"
host.Host.Platform = "android_updated"
host.Host.OSVersion = "Android 15"
host.Host.Build = "build_updated"
host.Host.Memory = 2048
host.Host.HardwareSerial = "hardware_serial_updated"
host.Host.CPUType = "cpu_type_updated"
host.Host.HardwareModel = "hardware_model_updated"
host.Host.HardwareVendor = "hardware_vendor_updated"
host.Device.AndroidPolicyID = ptr.Uint(2)
err = ds.UpdateAndroidHost(testCtx(), host, false)
require.NoError(t, err)
resultLite, err := ds.AndroidHostLite(testCtx(), enterpriseSpecificID)
require.NoError(t, err)
assert.Equal(t, host.Host.ID, resultLite.Host.ID)
assert.EqualValues(t, host.Device, resultLite.Device)
}
func testAndroidMDMStats(t *testing.T, ds *Datastore) {
const appleMDMURL = "/mdm/apple/mdm"
const serverURL = "http://androidmdm.example.com"
appCfg, err := ds.AppConfig(testCtx())
require.NoError(t, err)
appCfg.ServerSettings.ServerURL = serverURL
err = ds.SaveAppConfig(testCtx(), appCfg)
require.NoError(t, err)
// create a few android hosts
hosts := make([]*fleet.Host, 3)
var androidHost0 *fleet.AndroidHost
for i := range hosts {
host := createAndroidHost(uuid.NewString())
result, err := ds.NewAndroidHost(testCtx(), host)
require.NoError(t, err)
hosts[i] = result.Host
if androidHost0 == nil {
androidHost0 = host
}
}
// create a non-android host
macHost, err := ds.NewHost(testCtx(), &fleet.Host{
Hostname: "test-host1-name",
OsqueryHostID: ptr.String("1337"),
NodeKey: ptr.String("1337"),
UUID: "test-uuid-1",
Platform: "darwin",
HardwareSerial: uuid.NewString(),
})
require.NoError(t, err)
nanoEnroll(t, ds, macHost, false)
err = ds.MDMAppleUpsertHost(testCtx(), macHost)
require.NoError(t, err)
// create a non-mdm host
linuxHost, err := ds.NewHost(testCtx(), &fleet.Host{
Hostname: "test-host2-name",
OsqueryHostID: ptr.String("1338"),
NodeKey: ptr.String("1338"),
UUID: "test-uuid-2",
Platform: "linux",
HardwareSerial: uuid.NewString(),
})
require.NoError(t, err)
require.NotNil(t, linuxHost)
// stats not computed yet
statusStats, _, err := ds.AggregatedMDMStatus(testCtx(), nil, "")
require.NoError(t, err)
solutionsStats, _, err := ds.AggregatedMDMSolutions(testCtx(), nil, "")
require.NoError(t, err)
require.Equal(t, fleet.AggregatedMDMStatus{}, statusStats)
require.Equal(t, []fleet.AggregatedMDMSolutions(nil), solutionsStats)
// compute stats
err = ds.GenerateAggregatedMunkiAndMDM(testCtx())
require.NoError(t, err)
statusStats, _, err = ds.AggregatedMDMStatus(testCtx(), nil, "")
require.NoError(t, err)
solutionsStats, _, err = ds.AggregatedMDMSolutions(testCtx(), nil, "")
require.NoError(t, err)
require.Equal(t, fleet.AggregatedMDMStatus{HostsCount: 4, EnrolledManualHostsCount: 4}, statusStats)
require.Len(t, solutionsStats, 2)
// both solutions are Fleet
require.Equal(t, fleet.WellKnownMDMFleet, solutionsStats[0].Name)
require.Equal(t, fleet.WellKnownMDMFleet, solutionsStats[1].Name)
// one is the Android server URL, one is the Apple URL
for _, sol := range solutionsStats {
switch sol.ServerURL {
case serverURL:
require.Equal(t, 3, sol.HostsCount)
case serverURL + appleMDMURL:
require.Equal(t, 1, sol.HostsCount)
default:
require.Failf(t, "unexpected server URL: %v", sol.ServerURL)
}
}
// filter on android
statusStats, _, err = ds.AggregatedMDMStatus(testCtx(), nil, "android")
require.NoError(t, err)
solutionsStats, _, err = ds.AggregatedMDMSolutions(testCtx(), nil, "android")
require.NoError(t, err)
require.Equal(t, fleet.AggregatedMDMStatus{HostsCount: 3, EnrolledManualHostsCount: 3}, statusStats)
require.Len(t, solutionsStats, 1)
require.Equal(t, 3, solutionsStats[0].HostsCount)
require.Equal(t, serverURL, solutionsStats[0].ServerURL)
// turn MDM off for android
err = ds.DeleteAllEnterprises(testCtx())
require.NoError(t, err)
err = ds.BulkSetAndroidHostsUnenrolled(testCtx())
require.NoError(t, err)
// compute stats
err = ds.GenerateAggregatedMunkiAndMDM(testCtx())
require.NoError(t, err)
statusStats, _, err = ds.AggregatedMDMStatus(testCtx(), nil, "")
require.NoError(t, err)
solutionsStats, _, err = ds.AggregatedMDMSolutions(testCtx(), nil, "")
require.NoError(t, err)
require.Equal(t, fleet.AggregatedMDMStatus{HostsCount: 4, EnrolledManualHostsCount: 1, UnenrolledHostsCount: 3}, statusStats)
require.Len(t, solutionsStats, 1)
require.Equal(t, 1, solutionsStats[0].HostsCount)
require.Equal(t, serverURL+appleMDMURL, solutionsStats[0].ServerURL)
// filter on android
statusStats, _, err = ds.AggregatedMDMStatus(testCtx(), nil, "android")
require.NoError(t, err)
solutionsStats, _, err = ds.AggregatedMDMSolutions(testCtx(), nil, "android")
require.NoError(t, err)
require.Equal(t, fleet.AggregatedMDMStatus{HostsCount: 3, UnenrolledHostsCount: 3}, statusStats)
require.Len(t, solutionsStats, 0)
// simulate an android host that re-enrolls
err = ds.UpdateAndroidHost(testCtx(), androidHost0, true)
require.NoError(t, err)
// compute stats
err = ds.GenerateAggregatedMunkiAndMDM(testCtx())
require.NoError(t, err)
// filter on android
statusStats, _, err = ds.AggregatedMDMStatus(testCtx(), nil, "android")
require.NoError(t, err)
solutionsStats, _, err = ds.AggregatedMDMSolutions(testCtx(), nil, "android")
require.NoError(t, err)
require.Equal(t, fleet.AggregatedMDMStatus{HostsCount: 3, UnenrolledHostsCount: 2, EnrolledManualHostsCount: 1}, statusStats)
require.Len(t, solutionsStats, 1)
require.Equal(t, 1, solutionsStats[0].HostsCount)
require.Equal(t, serverURL, solutionsStats[0].ServerURL)
}