Skip to content
Draft
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
3 changes: 2 additions & 1 deletion codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ coverage:
range: 25...100
# Specify files or directories to ignore
ignore:
- "internal/usecase/devices/wsman/*"
- "internal/usecase/devices/wsman/*"
- "internal/mocks/*"
63 changes: 63 additions & 0 deletions internal/controller/httpapi/v1/boot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package v1

import (
"net/http"

"github.com/gin-gonic/gin"

"github.com/device-management-toolkit/console/internal/entity/dto/v1"
)

func (r *deviceManagementRoutes) getBootCapabilities(c *gin.Context) {
guid := c.Param("guid")

capabilities, err := r.d.GetBootCapabilities(c.Request.Context(), guid)
if err != nil {
r.l.Error(err, "http - v1 - getBootCapabilities")
ErrorResponse(c, err)

return
}

c.JSON(http.StatusOK, capabilities)
}

func (r *deviceManagementRoutes) setRPEEnabled(c *gin.Context) {
guid := c.Param("guid")

var req dto.RPERequest
if err := c.ShouldBindJSON(&req); err != nil {
ErrorResponse(c, err)

return
}

if err := r.d.SetRPEEnabled(c.Request.Context(), guid, req.Enabled); err != nil {
r.l.Error(err, "http - v1 - setRPEEnabled")
ErrorResponse(c, err)

return
}

c.JSON(http.StatusOK, nil)
}

func (r *deviceManagementRoutes) sendRemoteErase(c *gin.Context) {
guid := c.Param("guid")

var req dto.RemoteEraseRequest
if err := c.ShouldBindJSON(&req); err != nil {
ErrorResponse(c, err)

return
}

if err := r.d.SendRemoteErase(c.Request.Context(), guid, req.EraseMask); err != nil {
r.l.Error(err, "http - v1 - sendRemoteErase")
ErrorResponse(c, err)

return
}

c.JSON(http.StatusOK, nil)
}
192 changes: 192 additions & 0 deletions internal/controller/httpapi/v1/boot_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package v1

import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/require"

"github.com/device-management-toolkit/console/internal/entity/dto/v1"
"github.com/device-management-toolkit/console/internal/mocks"
)

func TestGetBootCapabilities(t *testing.T) {
t.Parallel()

tests := []struct {
name string
mock func(m *mocks.MockDeviceManagementFeature)
expectedCode int
response interface{}
}{
{
name: "getBootCapabilities - successful retrieval",
mock: func(m *mocks.MockDeviceManagementFeature) {
m.EXPECT().GetBootCapabilities(context.Background(), "valid-guid").
Return(dto.BootCapabilities{IDER: true, SOL: true}, nil)
},
expectedCode: http.StatusOK,
response: dto.BootCapabilities{IDER: true, SOL: true},
},
{
name: "getBootCapabilities - service failure",
mock: func(m *mocks.MockDeviceManagementFeature) {
m.EXPECT().GetBootCapabilities(context.Background(), "valid-guid").
Return(dto.BootCapabilities{}, ErrGeneral)
},
expectedCode: http.StatusInternalServerError,
response: nil,
},
}

for _, tc := range tests {
tc := tc

t.Run(tc.name, func(t *testing.T) {
t.Parallel()

deviceManagement, engine := deviceManagementTest(t)
tc.mock(deviceManagement)

req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/api/v1/amt/boot/capabilities/valid-guid", http.NoBody)
require.NoError(t, err)

w := httptest.NewRecorder()
engine.ServeHTTP(w, req)

require.Equal(t, tc.expectedCode, w.Code)

if tc.expectedCode == http.StatusOK {
jsonBytes, _ := json.Marshal(tc.response)
require.Equal(t, string(jsonBytes), w.Body.String())
}
})
}
}

func TestSetRPEEnabled(t *testing.T) {
t.Parallel()

tests := []struct {
name string
requestBody interface{}
mock func(m *mocks.MockDeviceManagementFeature)
expectedCode int
}{
{
name: "setRPEEnabled - successful (enabled=true)",
requestBody: dto.RPERequest{Enabled: true},
mock: func(m *mocks.MockDeviceManagementFeature) {
m.EXPECT().SetRPEEnabled(context.Background(), "valid-guid", true).
Return(nil)
},
expectedCode: http.StatusOK,
},
{
name: "setRPEEnabled - successful (enabled=false)",
requestBody: dto.RPERequest{Enabled: false},
mock: func(m *mocks.MockDeviceManagementFeature) {
m.EXPECT().SetRPEEnabled(context.Background(), "valid-guid", false).
Return(nil)
},
expectedCode: http.StatusOK,
},
{
name: "setRPEEnabled - invalid JSON payload",
requestBody: "invalid-json",
mock: func(_ *mocks.MockDeviceManagementFeature) {
},
expectedCode: http.StatusInternalServerError,
},
{
name: "setRPEEnabled - service failure",
requestBody: dto.RPERequest{Enabled: true},
mock: func(m *mocks.MockDeviceManagementFeature) {
m.EXPECT().SetRPEEnabled(context.Background(), "valid-guid", true).
Return(ErrGeneral)
},
expectedCode: http.StatusInternalServerError,
},
}

for _, tc := range tests {
tc := tc

t.Run(tc.name, func(t *testing.T) {
t.Parallel()

deviceManagement, engine := deviceManagementTest(t)
tc.mock(deviceManagement)

reqBody, _ := json.Marshal(tc.requestBody)
req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, "/api/v1/amt/boot/rpe/valid-guid", bytes.NewBuffer(reqBody))
require.NoError(t, err)

w := httptest.NewRecorder()
engine.ServeHTTP(w, req)

require.Equal(t, tc.expectedCode, w.Code)
})
}
}

func TestSendRemoteErase(t *testing.T) {
t.Parallel()

tests := []struct {
name string
requestBody interface{}
mock func(m *mocks.MockDeviceManagementFeature)
expectedCode int
}{
{
name: "sendRemoteErase - successful",
requestBody: dto.RemoteEraseRequest{EraseMask: 3},
mock: func(m *mocks.MockDeviceManagementFeature) {
m.EXPECT().SendRemoteErase(context.Background(), "valid-guid", 3).
Return(nil)
},
expectedCode: http.StatusOK,
},
{
name: "sendRemoteErase - invalid JSON payload",
requestBody: "invalid-json",
mock: func(_ *mocks.MockDeviceManagementFeature) {
},
expectedCode: http.StatusInternalServerError,
},
{
name: "sendRemoteErase - service failure",
requestBody: dto.RemoteEraseRequest{EraseMask: 1},
mock: func(m *mocks.MockDeviceManagementFeature) {
m.EXPECT().SendRemoteErase(context.Background(), "valid-guid", 1).
Return(ErrGeneral)
},
expectedCode: http.StatusInternalServerError,
},
}

for _, tc := range tests {
tc := tc

t.Run(tc.name, func(t *testing.T) {
t.Parallel()

deviceManagement, engine := deviceManagementTest(t)
tc.mock(deviceManagement)

reqBody, _ := json.Marshal(tc.requestBody)
req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, "/api/v1/amt/remoteErase/valid-guid", bytes.NewBuffer(reqBody))
require.NoError(t, err)

w := httptest.NewRecorder()
engine.ServeHTTP(w, req)

require.Equal(t, tc.expectedCode, w.Code)
})
}
}
3 changes: 3 additions & 0 deletions internal/controller/httpapi/v1/devicemanagement.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ func NewAmtRoutes(handler *gin.RouterGroup, d devices.Feature, amt amtexplorer.F
h.POST("alarmOccurrences/:guid", r.createAlarmOccurrences)
h.DELETE("alarmOccurrences/:guid", r.deleteAlarmOccurrences)

h.GET("boot/capabilities/:guid", r.getBootCapabilities)
h.POST("boot/rpe/:guid", r.setRPEEnabled)
h.POST("remoteErase/:guid", r.sendRemoteErase)
h.GET("hardwareInfo/:guid", r.getHardwareInfo)
h.GET("diskInfo/:guid", r.getDiskInfo)
h.GET("power/state/:guid", r.getPowerState)
Expand Down
2 changes: 1 addition & 1 deletion internal/controller/httpapi/v1/devicemanagement_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ func TestDeviceManagement(t *testing.T) {
OCR: false,
OptInState: 0,
Redirection: false,
RemoteErase: false,
RemoteEraseEnabled: false,
UserConsent: "",
WinREBootSupported: false,
},
Expand Down
4 changes: 3 additions & 1 deletion internal/controller/httpapi/v1/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ func (r *deviceManagementRoutes) getFeatures(c *gin.Context) {
HTTPSBootSupported: features.HTTPSBootSupported,
WinREBootSupported: features.WinREBootSupported,
LocalPBABootSupported: features.LocalPBABootSupported,
RemoteErase: features.RemoteErase,
RemoteEraseEnabled: features.RemoteEraseEnabled,
RemoteEraseSupported: features.RemoteEraseSupported,
PlatformEraseCaps: features.PlatformEraseCaps,
}

c.JSON(http.StatusOK, v1Features)
Expand Down
22 changes: 22 additions & 0 deletions internal/controller/openapi/devicemanagement.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,20 @@ func (f *FuegoAdapter) registerPowerRoutes() {
fuego.OptionDescription("Retrieve power capabilities for a device"),
fuego.OptionPath("guid", "Device GUID"),
)

fuego.Get(f.server, "/api/v1/admin/amt/boot/capabilities/{guid}", f.getBootCapabilities,
fuego.OptionTags("Device Management"),
fuego.OptionSummary("Get Boot Capabilities"),
fuego.OptionDescription("Read AMT_BootCapabilities.PlatformErase to determine Remote Platform Erase (RPE) support in the BIOS"),
fuego.OptionPath("guid", "Device GUID"),
)

fuego.Post(f.server, "/api/v1/admin/amt/boot/rpe/{guid}", f.setRPEEnabled,
fuego.OptionTags("Device Management"),
fuego.OptionSummary("Set RPE Enabled"),
fuego.OptionDescription("Enable or disable Remote Platform Erase (RPE) in Intel AMT via CIM_BootService.RequestStateChange. Requires administrative privileges and BIOS support."),
fuego.OptionPath("guid", "Device GUID"),
)
}

func (f *FuegoAdapter) registerLogsAndAlarmRoutes() {
Expand Down Expand Up @@ -359,6 +373,14 @@ func (f *FuegoAdapter) getPowerCapabilities(_ fuego.ContextNoBody) (dto.PowerCap
return dto.PowerCapabilities{}, nil
}

func (f *FuegoAdapter) getBootCapabilities(_ fuego.ContextNoBody) (dto.BootCapabilities, error) {
return dto.BootCapabilities{}, nil
}

func (f *FuegoAdapter) setRPEEnabled(_ fuego.ContextWithBody[dto.RPERequest]) (any, error) {
return nil, nil
}

func (f *FuegoAdapter) getAlarmOccurrences(_ fuego.ContextNoBody) ([]dto.AlarmClockOccurrence, error) {
return []dto.AlarmClockOccurrence{}, nil
}
Expand Down
59 changes: 59 additions & 0 deletions internal/controller/openapi/devicemanagement_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package openapi

import (
"encoding/json"
"testing"

"github.com/stretchr/testify/require"

dto "github.com/device-management-toolkit/console/internal/entity/dto/v1"
"github.com/device-management-toolkit/console/internal/usecase"
"github.com/device-management-toolkit/console/pkg/logger"
)

func newTestAdapter() *FuegoAdapter {
log := logger.New("error")

return NewFuegoAdapter(usecase.Usecases{}, log)
}

func TestGetBootCapabilities(t *testing.T) {
t.Parallel()

f := newTestAdapter()

result, err := f.getBootCapabilities(nil)

require.NoError(t, err)
require.Equal(t, dto.BootCapabilities{}, result)
}

func TestSetRPEEnabled(t *testing.T) {
t.Parallel()

f := newTestAdapter()

result, err := f.setRPEEnabled(nil)

require.NoError(t, err)
require.Nil(t, result)
}

func TestRegisterPowerRoutes_IncludesBootEndpoints(t *testing.T) {
t.Parallel()

f := newTestAdapter()
f.RegisterDeviceManagementRoutes()

specBytes, err := f.GetOpenAPISpec()
require.NoError(t, err)

var spec map[string]interface{}
require.NoError(t, json.Unmarshal(specBytes, &spec))

paths, ok := spec["paths"].(map[string]interface{})
require.True(t, ok)

require.Contains(t, paths, "/api/v1/admin/amt/boot/capabilities/{guid}", "boot capabilities route should be registered")
require.Contains(t, paths, "/api/v1/admin/amt/boot/rpe/{guid}", "set RPE enabled route should be registered")
}
Loading
Loading