diff --git a/integration-test/collections/console_mps_apis.postman_collection.json b/integration-test/collections/console_mps_apis.postman_collection.json index e7b29ebfc..0cbb46cd5 100644 --- a/integration-test/collections/console_mps_apis.postman_collection.json +++ b/integration-test/collections/console_mps_apis.postman_collection.json @@ -1148,7 +1148,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\r\n \"guid\": \"143e4567-e89b-12d3-a456-426614174000\",\r\n \"friendlyName\": \"friendlyName\",\r\n \"hostname\": \"hostname\",\r\n \"tags\": [],\r\n \"mpsusername\": \"admin\"\r\n}", + "raw": "{\r\n \"guid\": \"143e4567-e89b-12d3-a456-426614174000\",\r\n \"friendlyName\": \"friendlyName\",\r\n \"hostname\": \"hostname\",\r\n \"tags\": [],\r\n \"mpsusername\": \"admin\",\r\n \"deviceInfo\": {\r\n \"fwVersion\": \"16.1.30\",\r\n \"fwBuild\": \"3400\",\r\n \"fwSku\": \"11\",\r\n \"currentMode\": \"Admin\",\r\n \"features\": \"SOL,IDER,KVM\",\r\n \"ipAddress\": \"10.0.0.12\",\r\n \"lastUpdated\": \"2026-05-21T00:00:00Z\",\r\n \"tlsMode\": \"TLS 1.2\",\r\n \"upid\": {\r\n \"oemPlatformIdType\": \"Not Set (0)\",\r\n \"oemId\": \"\",\r\n \"csmeId\": \"4A45A39C5ED9462082510000\"\r\n },\r\n \"amtEnabledInBIOS\": true,\r\n \"meInterfaceVersion\": \"16.1.25.2124\",\r\n \"dhcpEnabled\": true,\r\n \"certHashes\": [\r\n \"a1b2c3\",\r\n \"d4e5f6\"\r\n ],\r\n \"lmsInstalled\": true,\r\n \"lmsVersion\": \"2410.5.0.0\",\r\n \"osName\": \"linux\",\r\n \"osVersion\": \"6.8.0-51-generic\",\r\n \"osDistro\": \"Ubuntu 24.04 LTS\",\r\n \"cpuModel\": \"Intel(R) Core(TM) Ultra 7 165H\",\r\n \"osIpAddress\": \"10.49.76.163\",\r\n \"ethernetAdapterCount\": 2,\r\n \"monitorConnected\": true,\r\n \"ieee8021xEnabled\": false,\r\n \"lastDiscovered\": \"2026-05-14T10:23:00Z\",\r\n \"certProvisioningMode\": \"none\",\r\n \"amtPhase\": \"post\"\r\n }\r\n}", "options": { "raw": { "language": "json" diff --git a/internal/controller/httpapi/v1/devices_test.go b/internal/controller/httpapi/v1/devices_test.go index 0d20315d3..9975a6d5f 100644 --- a/internal/controller/httpapi/v1/devices_test.go +++ b/internal/controller/httpapi/v1/devices_test.go @@ -442,6 +442,107 @@ func TestDevicesUpdatePartialPatchMixedCaseKeys(t *testing.T) { require.Equal(t, http.StatusOK, w.Code) } +func TestDevicesInsertAcceptsFullDeviceInfo(t *testing.T) { + t.Parallel() + + lmsInstalled := false + amtEnabledInBIOS := true + dhcpEnabled := true + ethernetAdapterCount := 2 + monitorConnected := true + ieee8021xEnabled := false + lastDiscovered := time.Date(2026, 5, 14, 10, 23, 0, 0, time.UTC) + + incoming := &dto.Device{ + GUID: testDeviceGUID, + Hostname: "test-device", + DeviceInfo: &dto.DeviceInfo{ + FWVersion: "16.1.30", + FWBuild: "3400", + FWSku: "11", + CurrentMode: "Admin", + Features: "SOL,IDER,KVM", + IPAddress: "10.0.0.12", + LastUpdated: timeNow, + TLSMode: "TLS 1.2", + UPID: map[string]any{"oemPlatformIdType": "Not Set (0)", "oemId": "", "csmeId": "4A45A39C5ED9462082510000"}, + AMTEnabledInBIOS: &amtEnabledInBIOS, + MEInterfaceVersion: "16.1.25.2124", + DHCPEnabled: &dhcpEnabled, + CertHashes: []string{"a1b2c3", "d4e5f6"}, + LMSInstalled: &lmsInstalled, + LMSVersion: "2410.5.0.0", + OSName: "linux", + OSVersion: "6.8.0-51-generic", + OSDistro: "Ubuntu 24.04 LTS", + CPUModel: "Intel(R) Core(TM) Ultra 7 165H", + OSIPAddress: "10.49.76.163", + EthernetAdapterCount: ðernetAdapterCount, + MonitorConnected: &monitorConnected, + IEEE8021XEnabled: &ieee8021xEnabled, + LastDiscovered: &lastDiscovered, + ExtraFields: map[string]json.RawMessage{ + "certProvisioningMode": json.RawMessage(`"none"`), + "amtPhase": json.RawMessage(`"post"`), + }, + }, + } + + devicesFeature, engine := devicesTest(t) + + devicesFeature.EXPECT(). + Insert(context.Background(), incoming). + Return(incoming, nil) + + body := []byte(`{ + "guid":"` + testDeviceGUID + `", + "hostname":"test-device", + "deviceInfo":{ + "fwVersion":"16.1.30", + "fwBuild":"3400", + "fwSku":"11", + "currentMode":"Admin", + "features":"SOL,IDER,KVM", + "ipAddress":"10.0.0.12", + "lastUpdated":"` + timeNow.Format(time.RFC3339Nano) + `", + "tlsMode":"TLS 1.2", + "upid":{ + "oemPlatformIdType":"Not Set (0)", + "oemId":"", + "csmeId":"4A45A39C5ED9462082510000" + }, + "amtEnabledInBIOS":true, + "meInterfaceVersion":"16.1.25.2124", + "dhcpEnabled":true, + "certHashes":["a1b2c3","d4e5f6"], + "lmsInstalled":false, + "lmsVersion":"2410.5.0.0", + "osName":"linux", + "osVersion":"6.8.0-51-generic", + "osDistro":"Ubuntu 24.04 LTS", + "cpuModel":"Intel(R) Core(TM) Ultra 7 165H", + "osIpAddress":"10.49.76.163", + "ethernetAdapterCount":2, + "monitorConnected":true, + "ieee8021xEnabled":false, + "lastDiscovered":"` + lastDiscovered.Format(time.RFC3339) + `", + "certProvisioningMode":"none", + "amtPhase":"post" + } + }`) + + req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, "/api/v1/devices", bytes.NewBuffer(body)) + require.NoError(t, err) + + w := httptest.NewRecorder() + engine.ServeHTTP(w, req) + + require.Equal(t, http.StatusCreated, w.Code) + + expected, _ := json.Marshal(incoming) + require.Equal(t, string(expected), w.Body.String()) +} + // TestLoginRedirection verifies the device redirection token endpoint func TestLoginRedirection(t *testing.T) { t.Parallel() diff --git a/internal/entity/dto/v1/device.go b/internal/entity/dto/v1/device.go index fccfd0676..d3f754f6a 100644 --- a/internal/entity/dto/v1/device.go +++ b/internal/entity/dto/v1/device.go @@ -1,6 +1,7 @@ package dto import ( + "encoding/json" "time" ) @@ -37,13 +38,104 @@ type Device struct { } type DeviceInfo struct { - FWVersion string `json:"fwVersion"` - FWBuild string `json:"fwBuild"` - FWSku string `json:"fwSku"` - CurrentMode string `json:"currentMode"` - Features string `json:"features"` - IPAddress string `json:"ipAddress"` - LastUpdated time.Time `json:"lastUpdated"` + FWVersion string `json:"fwVersion"` + FWBuild string `json:"fwBuild"` + FWSku string `json:"fwSku"` + CurrentMode string `json:"currentMode"` + Features string `json:"features"` + IPAddress string `json:"ipAddress"` + LastUpdated time.Time `json:"lastUpdated"` + TLSMode string `json:"tlsMode"` + UPID map[string]any `json:"upid,omitempty"` + AMTEnabledInBIOS *bool `json:"amtEnabledInBIOS,omitempty"` + MEInterfaceVersion string `json:"meInterfaceVersion"` + DHCPEnabled *bool `json:"dhcpEnabled,omitempty"` + CertHashes []string `json:"certHashes,omitempty"` + LMSInstalled *bool `json:"lmsInstalled,omitempty"` + LMSVersion string `json:"lmsVersion"` + OSName string `json:"osName"` + OSVersion string `json:"osVersion"` + OSDistro string `json:"osDistro"` + CPUModel string `json:"cpuModel"` + OSIPAddress string `json:"osIpAddress"` + EthernetAdapterCount *int `json:"ethernetAdapterCount,omitempty"` + MonitorConnected *bool `json:"monitorConnected,omitempty"` + IEEE8021XEnabled *bool `json:"ieee8021xEnabled,omitempty"` + LastDiscovered *time.Time `json:"lastDiscovered,omitempty"` + ExtraFields map[string]json.RawMessage `json:"-"` +} + +func (d *DeviceInfo) UnmarshalJSON(data []byte) error { + type alias DeviceInfo + + var base alias + if err := json.Unmarshal(data, &base); err != nil { + return err + } + + var raw map[string]json.RawMessage + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + delete(raw, "fwVersion") + delete(raw, "fwBuild") + delete(raw, "fwSku") + delete(raw, "currentMode") + delete(raw, "features") + delete(raw, "ipAddress") + delete(raw, "lastUpdated") + delete(raw, "tlsMode") + delete(raw, "upid") + delete(raw, "amtEnabledInBIOS") + delete(raw, "meInterfaceVersion") + delete(raw, "dhcpEnabled") + delete(raw, "certHashes") + delete(raw, "lmsInstalled") + delete(raw, "lmsVersion") + delete(raw, "osName") + delete(raw, "osVersion") + delete(raw, "osDistro") + delete(raw, "cpuModel") + delete(raw, "osIpAddress") + delete(raw, "ethernetAdapterCount") + delete(raw, "monitorConnected") + delete(raw, "ieee8021xEnabled") + delete(raw, "lastDiscovered") + + *d = DeviceInfo(base) + if len(raw) > 0 { + d.ExtraFields = raw + } + + return nil +} + +func (d DeviceInfo) MarshalJSON() ([]byte, error) { + type alias DeviceInfo + + base := alias(d) + base.ExtraFields = nil + + b, err := json.Marshal(base) + if err != nil { + return nil, err + } + + if len(d.ExtraFields) == 0 { + return b, nil + } + + var raw map[string]json.RawMessage + if err := json.Unmarshal(b, &raw); err != nil { + return nil, err + } + + for key, value := range d.ExtraFields { + raw[key] = value + } + + return json.Marshal(raw) } type Explorer struct { diff --git a/internal/entity/dto/v1/device_test.go b/internal/entity/dto/v1/device_test.go new file mode 100644 index 000000000..e546be4d1 --- /dev/null +++ b/internal/entity/dto/v1/device_test.go @@ -0,0 +1,67 @@ +package dto + +import ( + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestDeviceInfoJSONRoundTrip(t *testing.T) { + t.Parallel() + + amtEnabled := true + dhcpEnabled := true + lmsInstalled := true + ethernetAdapterCount := 2 + monitorConnected := true + ieee8021xEnabled := false + lastDiscovered := time.Date(2026, 5, 14, 10, 23, 0, 0, time.UTC) + + info := DeviceInfo{ + FWVersion: "16.1.30", + FWBuild: "3400", + FWSku: "11", + CurrentMode: "Admin", + Features: "SOL,IDER,KVM", + IPAddress: "10.0.0.12", + LastUpdated: time.Date(2026, 5, 21, 0, 0, 0, 0, time.UTC), + TLSMode: "TLS 1.2", + UPID: map[string]any{"oemPlatformIdType": "Not Set (0)", "oemId": "", "csmeId": "4A45A39C5ED94620"}, + AMTEnabledInBIOS: &amtEnabled, + MEInterfaceVersion: "16.1.25.2124", + DHCPEnabled: &dhcpEnabled, + CertHashes: []string{"a1b2c3", "d4e5f6"}, + LMSInstalled: &lmsInstalled, + LMSVersion: "2410.5.0.0", + OSName: "linux", + OSVersion: "6.8.0-51-generic", + OSDistro: "Ubuntu 24.04 LTS", + CPUModel: "Intel(R) Core(TM) Ultra 7 165H", + OSIPAddress: "10.49.76.163", + EthernetAdapterCount: ðernetAdapterCount, + MonitorConnected: &monitorConnected, + IEEE8021XEnabled: &ieee8021xEnabled, + LastDiscovered: &lastDiscovered, + ExtraFields: map[string]json.RawMessage{ + "customFlag": json.RawMessage("true"), + }, + } + + encoded, err := json.Marshal(info) + require.NoError(t, err) + + var decoded DeviceInfo + require.NoError(t, json.Unmarshal(encoded, &decoded)) + + require.Equal(t, info.TLSMode, decoded.TLSMode) + require.Equal(t, info.MEInterfaceVersion, decoded.MEInterfaceVersion) + require.Equal(t, info.CertHashes, decoded.CertHashes) + require.Equal(t, info.LMSVersion, decoded.LMSVersion) + require.NotNil(t, decoded.LMSInstalled) + require.Equal(t, *info.LMSInstalled, *decoded.LMSInstalled) + require.NotNil(t, decoded.LastDiscovered) + require.True(t, decoded.LastDiscovered.Equal(lastDiscovered)) + require.Contains(t, decoded.ExtraFields, "customFlag") +} diff --git a/internal/usecase/devices/repo_test.go b/internal/usecase/devices/repo_test.go index aba5caeac..95396a47d 100644 --- a/internal/usecase/devices/repo_test.go +++ b/internal/usecase/devices/repo_test.go @@ -18,6 +18,10 @@ func ptr(s string) *string { return &s } +func boolPtr(v bool) *bool { + return &v +} + type testUsecase struct { name string guid string @@ -740,6 +744,7 @@ func TestUpdatePartial(t *testing.T) { GUID: "device-guid-123", TenantID: "tenant-id-456", Hostname: "old-hostname", + DeviceInfo: `{"fwVersion":"11.8.50","ipAddress":"10.0.0.1","lmsInstalled":false}`, Tags: "lab,floor-2", MPSUsername: "admin", Username: "amtadmin", @@ -753,14 +758,20 @@ func TestUpdatePartial(t *testing.T) { GUID: "device-guid-123", TenantID: "tenant-id-456", Hostname: "new-hostname", + DeviceInfo: &dto.DeviceInfo{ + FWVersion: "16.1.30", + IPAddress: "10.0.0.55", + LMSInstalled: boolPtr(true), + }, } - fields := map[string]bool{"guid": true, "tenantId": true, "hostname": true} + fields := map[string]bool{"guid": true, "tenantId": true, "hostname": true, "deviceinfo": true} // After merge + dtoToEntity (MockCrypto re-encrypts plaintext to "encrypted"): expectedEntity := &entity.Device{ GUID: "device-guid-123", TenantID: "tenant-id-456", Hostname: "new-hostname", + DeviceInfo: `{"fwVersion":"16.1.30","fwBuild":"","fwSku":"","currentMode":"","features":"","ipAddress":"10.0.0.55","lastUpdated":"0001-01-01T00:00:00Z","tlsMode":"","meInterfaceVersion":"","lmsInstalled":true,"lmsVersion":"","osName":"","osVersion":"","osDistro":"","cpuModel":"","osIpAddress":""}`, Tags: "lab,floor-2", MPSUsername: "admin", Username: "amtadmin", @@ -770,9 +781,14 @@ func TestUpdatePartial(t *testing.T) { } expectedDTO := &dto.Device{ - GUID: "device-guid-123", - TenantID: "tenant-id-456", - Hostname: "new-hostname", + GUID: "device-guid-123", + TenantID: "tenant-id-456", + Hostname: "new-hostname", + DeviceInfo: &dto.DeviceInfo{ + FWVersion: "16.1.30", + IPAddress: "10.0.0.55", + LMSInstalled: boolPtr(true), + }, Tags: []string{"lab", "floor-2"}, MPSUsername: "admin", Username: "amtadmin", diff --git a/internal/usecase/devices/usecase.go b/internal/usecase/devices/usecase.go index a61fb361a..c1908aa04 100644 --- a/internal/usecase/devices/usecase.go +++ b/internal/usecase/devices/usecase.go @@ -1,6 +1,7 @@ package devices import ( + "encoding/json" "strings" "sync" @@ -74,6 +75,11 @@ func (uc *UseCase) dtoToEntity(d *dto.Device) (*entity.Device, error) { tags := strings.Join(d.Tags, ",") + deviceInfo, err := marshalDeviceInfo(d.DeviceInfo) + if err != nil { + return nil, ErrDeviceUseCase.Wrap("dtoToEntity", "marshalDeviceInfo", err) + } + d1 := &entity.Device{ ConnectionStatus: d.ConnectionStatus, MPSInstance: d.MPSInstance, @@ -87,15 +93,13 @@ func (uc *UseCase) dtoToEntity(d *dto.Device) (*entity.Device, error) { LastConnected: d.LastConnected, LastSeen: d.LastSeen, LastDisconnected: d.LastDisconnected, - // DeviceInfo: d.DeviceInfo, - Username: d.Username, - Password: d.Password, - UseTLS: d.UseTLS, - AllowSelfSigned: d.AllowSelfSigned, + DeviceInfo: deviceInfo, + Username: d.Username, + Password: d.Password, + UseTLS: d.UseTLS, + AllowSelfSigned: d.AllowSelfSigned, } - var err error - d1.Password, err = uc.safeRequirements.Encrypt(d1.Password) if err != nil { return nil, ErrDeviceUseCase.Wrap("dtoToEntity", "failed to encrypt password", err) @@ -133,8 +137,7 @@ func (uc *UseCase) dtoToEntity(d *dto.Device) (*entity.Device, error) { } // Keys are lowercased to match encoding/json's case-insensitive unmarshal. -// guid and tenantId identify the record; deviceInfo doesn't round-trip through -// dtoToEntity/entityToDTO — all three are intentionally omitted. +// guid and tenantId identify the record and are intentionally omitted. var deviceFieldSetters = map[string]func(dst, src *dto.Device){ "connectionstatus": func(dst, src *dto.Device) { dst.ConnectionStatus = src.ConnectionStatus }, "mpsinstance": func(dst, src *dto.Device) { dst.MPSInstance = src.MPSInstance }, @@ -146,6 +149,7 @@ var deviceFieldSetters = map[string]func(dst, src *dto.Device){ "lastconnected": func(dst, src *dto.Device) { dst.LastConnected = src.LastConnected }, "lastseen": func(dst, src *dto.Device) { dst.LastSeen = src.LastSeen }, "lastdisconnected": func(dst, src *dto.Device) { dst.LastDisconnected = src.LastDisconnected }, + "deviceinfo": func(dst, src *dto.Device) { dst.DeviceInfo = src.DeviceInfo }, "username": func(dst, src *dto.Device) { dst.Username = src.Username }, "password": func(dst, src *dto.Device) { dst.Password = src.Password }, "mpspassword": func(dst, src *dto.Device) { dst.MPSPassword = src.MPSPassword }, @@ -171,6 +175,8 @@ func (uc *UseCase) entityToDTO(d *entity.Device) *dto.Device { tags = strings.Split(d.Tags, ",") } + deviceInfo := unmarshalDeviceInfo(d.DeviceInfo) + d1 := &dto.Device{ ConnectionStatus: d.ConnectionStatus, MPSInstance: d.MPSInstance, @@ -184,8 +190,8 @@ func (uc *UseCase) entityToDTO(d *entity.Device) *dto.Device { LastConnected: d.LastConnected, LastSeen: d.LastSeen, LastDisconnected: d.LastDisconnected, - // DeviceInfo: d.DeviceInfo, - Username: d.Username, + DeviceInfo: deviceInfo, + Username: d.Username, // Password: d.Password, UseTLS: d.UseTLS, AllowSelfSigned: d.AllowSelfSigned, @@ -205,3 +211,29 @@ func (uc *UseCase) entityToDTO(d *entity.Device) *dto.Device { return d1 } + +func marshalDeviceInfo(info *dto.DeviceInfo) (string, error) { + if info == nil { + return "", nil + } + + b, err := json.Marshal(info) + if err != nil { + return "", err + } + + return string(b), nil +} + +func unmarshalDeviceInfo(raw string) *dto.DeviceInfo { + if strings.TrimSpace(raw) == "" { + return nil + } + + var info dto.DeviceInfo + if err := json.Unmarshal([]byte(raw), &info); err != nil { + return nil + } + + return &info +}