Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
101 changes: 101 additions & 0 deletions internal/controller/httpapi/v1/devices_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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: &ethernetAdapterCount,
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()
Expand Down
106 changes: 99 additions & 7 deletions internal/entity/dto/v1/device.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dto

import (
"encoding/json"
"time"
)

Expand Down Expand Up @@ -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"`
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

map[string]any coerces numeric JSON values to float64 on Unmarshal. If any UPID subfield is ever sent as an integer (e.g. "oemPlatformIdType": 0), the re-marshaled value loses type fidelity, and any 64-bit identifier larger than 2^53 loses precision outright.

The Postman sample shows UPID has a knowable shape (oemPlatformIdType, oemId, csmeId) — a typed struct would be safer than map[string]any. If a typed struct isn't practical yet, map[string]json.RawMessage preserves the original bytes for round-trip without coercion.

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"`
Comment on lines +47 to +56
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")
Copy link
Copy Markdown
Member

@rsdmike rsdmike May 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drop the catch-all entirely. Unknown fields aren't part of the contract — if a field isn't known, it doesn't belong on the wire.

That means this whole UnmarshalJSON (and the matching MarshalJSON below) can be deleted, along with the ExtraFields map. Default encoding/json behavior on the plain struct is exactly what's wanted: declared fields round-trip, undeclared fields are dropped.

Follow-ups in this PR:

  • Remove ExtraFields from the DeviceInfo struct.
  • Remove the custom UnmarshalJSON and MarshalJSON.
  • Drop TestDeviceInfoJSONRoundTrip's ExtraFields assertion.
  • Decide on certProvisioningMode and amtPhase in the Postman sample: either add them as proper typed fields on DeviceInfo, or remove them from the request body.

Net result is roughly -50 lines and removes a class of silent-drift bugs.

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
Copy link
Copy Markdown
Member

@rsdmike rsdmike May 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moot — the entire MarshalJSON (and UnmarshalJSON) should be deleted along with ExtraFields. See the top-level comment on the deletes.


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
Copy link
Copy Markdown
Member

@rsdmike rsdmike May 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This concern goes away entirely if ExtraFields and the custom MarshalJSON are removed — see the top-level comment on the deletes. Leaving the thread for visibility only.

}

return json.Marshal(raw)
}

type Explorer struct {
Expand Down
67 changes: 67 additions & 0 deletions internal/entity/dto/v1/device_test.go
Original file line number Diff line number Diff line change
@@ -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: &ethernetAdapterCount,
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")
}
24 changes: 20 additions & 4 deletions internal/usecase/devices/repo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand All @@ -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":""}`,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pins the expected entity's DeviceInfo to a hand-written JSON string whose key order matches Go's declaration order of DeviceInfo fields. Any future reorder (e.g. grouping OS-related fields together for readability) silently breaks this test even though the behavior is unchanged.

require.JSONEq(t, expectedJSON, actualEntity.DeviceInfo) is order-insensitive, or assert on the unmarshaled DTO instead of the encoded blob.

Tags: "lab,floor-2",
MPSUsername: "admin",
Username: "amtadmin",
Expand All @@ -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",
Expand Down
Loading
Loading