Skip to content

Commit 683809d

Browse files
authored
NOISSUE - Update bootstrap content format, update profile method and add profile search (#3515)
Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
1 parent f380c8d commit 683809d

26 files changed

Lines changed: 950 additions & 284 deletions

bootstrap/api/endpoint.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -353,13 +353,27 @@ func renderPreviewEndpoint(svc bootstrap.Service) endpoint.Endpoint {
353353
}
354354

355355
cfg := req.Config
356+
bindings := req.Bindings
357+
358+
if req.ConfigID != "" {
359+
stored, err := svc.View(ctx, session, req.ConfigID)
360+
if err != nil {
361+
return nil, err
362+
}
363+
cfg = stored
364+
bindings, err = svc.ListBindings(ctx, session, req.ConfigID)
365+
if err != nil {
366+
return nil, err
367+
}
368+
}
369+
356370
cfg.DomainID = session.DomainID
357371
cfg.ProfileID = p.ID
358372
if cfg.RenderContext == nil {
359373
cfg.RenderContext = req.RenderContext
360374
}
361375

362-
rendered, err := bootstrap.NewRenderer().Render(p, cfg, req.Bindings)
376+
rendered, err := bootstrap.NewRenderer().Render(p, cfg, bindings)
363377
if err != nil {
364378
return nil, err
365379
}
@@ -379,10 +393,11 @@ func updateProfileEndpoint(svc bootstrap.Service) endpoint.Endpoint {
379393
return nil, svcerr.ErrAuthorization
380394
}
381395
req.Profile.ID = req.profileID
382-
if err := svc.UpdateProfile(ctx, session, req.Profile); err != nil {
396+
updated, err := svc.UpdateProfile(ctx, session, req.Profile)
397+
if err != nil {
383398
return nil, err
384399
}
385-
return profileRes{Profile: req.Profile}, nil
400+
return profileRes{Profile: updated}, nil
386401
}
387402
}
388403

@@ -413,7 +428,7 @@ func listProfilesEndpoint(svc bootstrap.Service) endpoint.Endpoint {
413428
if !ok {
414429
return nil, svcerr.ErrAuthorization
415430
}
416-
page, err := svc.ListProfiles(ctx, session, req.offset, req.limit)
431+
page, err := svc.ListProfiles(ctx, session, req.offset, req.limit, req.name)
417432
if err != nil {
418433
return nil, err
419434
}

bootstrap/api/endpoint_test.go

Lines changed: 213 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1182,7 +1182,7 @@ func TestUploadProfile(t *testing.T) {
11821182
saved := bootstrap.Profile{
11831183
ID: testsutil.GenerateUUID(t),
11841184
Name: "gateway",
1185-
TemplateFormat: bootstrap.TemplateFormatGoTemplate,
1185+
ContentFormat: bootstrap.ContentFormatGoTemplate,
11861186
ContentTemplate: "{{ .Device.ID }}",
11871187
}
11881188

@@ -1195,30 +1195,60 @@ func TestUploadProfile(t *testing.T) {
11951195
{
11961196
desc: "upload JSON profile",
11971197
contentType: "application/json",
1198-
body: `{"name":"gateway","template_format":"go-template","content_template":"{{ .Device.ID }}"}`,
1198+
body: `{"name":"gateway","content_format":"go-template","content_template":"{{ .Device.ID }}"}`,
11991199
profile: bootstrap.Profile{
12001200
Name: "gateway",
1201-
TemplateFormat: bootstrap.TemplateFormatGoTemplate,
1201+
ContentFormat: bootstrap.ContentFormatGoTemplate,
12021202
ContentTemplate: "{{ .Device.ID }}",
12031203
},
12041204
},
12051205
{
12061206
desc: "upload YAML profile",
12071207
contentType: "application/yaml",
1208-
body: "name: gateway\ntemplate_format: go-template\ncontent_template: '{{ .Device.ID }}'\n",
1208+
body: "name: gateway\ncontent_format: go-template\ncontent_template: '{{ .Device.ID }}'\n",
12091209
profile: bootstrap.Profile{
12101210
Name: "gateway",
1211-
TemplateFormat: bootstrap.TemplateFormatGoTemplate,
1211+
ContentFormat: bootstrap.ContentFormatGoTemplate,
12121212
ContentTemplate: "{{ .Device.ID }}",
12131213
},
12141214
},
12151215
{
12161216
desc: "upload TOML profile",
12171217
contentType: "application/toml",
1218-
body: "name = 'gateway'\ntemplate_format = 'go-template'\ncontent_template = '{{ .Device.ID }}'\n",
1218+
body: "name = 'gateway'\ncontent_format = 'go-template'\ncontent_template = '{{ .Device.ID }}'\n",
12191219
profile: bootstrap.Profile{
12201220
Name: "gateway",
1221-
TemplateFormat: bootstrap.TemplateFormatGoTemplate,
1221+
ContentFormat: bootstrap.ContentFormatGoTemplate,
1222+
ContentTemplate: "{{ .Device.ID }}",
1223+
},
1224+
},
1225+
{
1226+
desc: "upload JSON profile without content_format infers json",
1227+
contentType: "application/json",
1228+
body: `{"name":"gateway","content_template":"{{ .Device.ID }}"}`,
1229+
profile: bootstrap.Profile{
1230+
Name: "gateway",
1231+
ContentFormat: bootstrap.ContentFormatJSON,
1232+
ContentTemplate: "{{ .Device.ID }}",
1233+
},
1234+
},
1235+
{
1236+
desc: "upload YAML profile without content_format infers yaml",
1237+
contentType: "application/yaml",
1238+
body: "name: gateway\ncontent_template: '{{ .Device.ID }}'\n",
1239+
profile: bootstrap.Profile{
1240+
Name: "gateway",
1241+
ContentFormat: bootstrap.ContentFormatYAML,
1242+
ContentTemplate: "{{ .Device.ID }}",
1243+
},
1244+
},
1245+
{
1246+
desc: "upload TOML profile without content_format infers toml",
1247+
contentType: "application/toml",
1248+
body: "name = 'gateway'\ncontent_template = '{{ .Device.ID }}'\n",
1249+
profile: bootstrap.Profile{
1250+
Name: "gateway",
1251+
ContentFormat: bootstrap.ContentFormatTOML,
12221252
ContentTemplate: "{{ .Device.ID }}",
12231253
},
12241254
},
@@ -1246,6 +1276,83 @@ func TestUploadProfile(t *testing.T) {
12461276
}
12471277
}
12481278

1279+
func TestListProfiles(t *testing.T) {
1280+
bs, svc, auth := newBootstrapServer()
1281+
defer bs.Close()
1282+
1283+
session := smqauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}
1284+
path := fmt.Sprintf("%s/%s/clients/bootstrap/profiles", bs.URL, domainID)
1285+
1286+
profiles := []bootstrap.Profile{
1287+
{ID: testsutil.GenerateUUID(t), DomainID: domainID, Name: "gateway-profile"},
1288+
{ID: testsutil.GenerateUUID(t), DomainID: domainID, Name: "sensor-profile"},
1289+
}
1290+
fullPage := bootstrap.ProfilesPage{Total: 2, Offset: 0, Limit: 10, Profiles: profiles}
1291+
filteredPage := bootstrap.ProfilesPage{Total: 1, Offset: 0, Limit: 10, Profiles: profiles[:1]}
1292+
1293+
cases := []struct {
1294+
desc string
1295+
token string
1296+
session smqauthn.Session
1297+
url string
1298+
name string
1299+
svcPage bootstrap.ProfilesPage
1300+
svcErr error
1301+
authenticateErr error
1302+
status int
1303+
}{
1304+
{
1305+
desc: "list profiles successfully",
1306+
token: validToken,
1307+
session: session,
1308+
url: fmt.Sprintf("%s?offset=0&limit=10", path),
1309+
svcPage: fullPage,
1310+
status: http.StatusOK,
1311+
},
1312+
{
1313+
desc: "list profiles filtered by name",
1314+
token: validToken,
1315+
session: session,
1316+
url: fmt.Sprintf("%s?offset=0&limit=10&name=gateway-profile", path),
1317+
name: "gateway-profile",
1318+
svcPage: filteredPage,
1319+
status: http.StatusOK,
1320+
},
1321+
{
1322+
desc: "list profiles with invalid token",
1323+
token: invalidToken,
1324+
url: fmt.Sprintf("%s?offset=0&limit=10", path),
1325+
authenticateErr: svcerr.ErrAuthentication,
1326+
status: http.StatusUnauthorized,
1327+
},
1328+
{
1329+
desc: "list profiles with limit exceeding max",
1330+
token: validToken,
1331+
session: session,
1332+
url: fmt.Sprintf("%s?offset=0&limit=101", path),
1333+
status: http.StatusBadRequest,
1334+
},
1335+
}
1336+
1337+
for _, tc := range cases {
1338+
t.Run(tc.desc, func(t *testing.T) {
1339+
authCall := auth.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authenticateErr)
1340+
svcCall := svc.On("ListProfiles", mock.Anything, tc.session, mock.Anything, mock.Anything, tc.name).Return(tc.svcPage, tc.svcErr)
1341+
req := testRequest{
1342+
client: bs.Client(),
1343+
method: http.MethodGet,
1344+
url: tc.url,
1345+
token: tc.token,
1346+
}
1347+
res, err := req.make()
1348+
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
1349+
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status %d got %d", tc.desc, tc.status, res.StatusCode))
1350+
authCall.Unset()
1351+
svcCall.Unset()
1352+
})
1353+
}
1354+
}
1355+
12491356
func TestProfileSlots(t *testing.T) {
12501357
bs, svc, auth := newBootstrapServer()
12511358
defer bs.Close()
@@ -1295,56 +1402,119 @@ func TestRenderPreview(t *testing.T) {
12951402
profile := bootstrap.Profile{
12961403
ID: profileID,
12971404
Name: "gateway",
1298-
TemplateFormat: bootstrap.TemplateFormatGoTemplate,
1405+
ContentFormat: bootstrap.ContentFormatGoTemplate,
12991406
ContentTemplate: `device={{ .Device.ID }} site={{ .Vars.site }} topic={{ index (index .Bindings "telemetry").Snapshot "topic" }}`,
13001407
}
1301-
authCall := auth.On("Authenticate", mock.Anything, validToken).Return(session, nil)
1302-
svcCall := svc.On("ViewProfile", mock.Anything, session, profileID).Return(profile, nil)
13031408

1304-
reqBody := struct {
1409+
storedConfig := bootstrap.Config{
1410+
ID: configID,
1411+
ExternalID: "gw-001",
1412+
DomainID: domainID,
1413+
RenderContext: map[string]any{
1414+
"site": "warehouse-1",
1415+
},
1416+
}
1417+
storedBindings := []bootstrap.BindingSnapshot{
1418+
{
1419+
Slot: "telemetry",
1420+
Type: "channel",
1421+
ResourceID: "ch-1",
1422+
Snapshot: map[string]any{"topic": "devices/gw-001/telemetry"},
1423+
},
1424+
}
1425+
1426+
inlineReqBody := struct {
13051427
Config bootstrap.Config `json:"config"`
13061428
Bindings []bootstrap.BindingSnapshot `json:"bindings"`
13071429
}{
13081430
Config: bootstrap.Config{
1309-
ID: configID,
1310-
ExternalID: "gw-001",
1311-
RenderContext: map[string]any{
1312-
"site": "warehouse-1",
1313-
},
1314-
},
1315-
Bindings: []bootstrap.BindingSnapshot{
1316-
{
1317-
Slot: "telemetry",
1318-
Type: "channel",
1319-
ResourceID: "ch-1",
1320-
Snapshot: map[string]any{
1321-
"topic": "devices/gw-001/telemetry",
1322-
},
1323-
},
1431+
ID: configID,
1432+
ExternalID: "gw-001",
1433+
RenderContext: map[string]any{"site": "warehouse-1"},
13241434
},
1435+
Bindings: storedBindings,
13251436
}
13261437

1327-
req := testRequest{
1328-
client: bs.Client(),
1329-
method: http.MethodPost,
1330-
url: fmt.Sprintf("%s/%s/clients/bootstrap/profiles/%s/render-preview", bs.URL, domainID, profileID),
1331-
contentType: contentType,
1332-
token: validToken,
1333-
body: strings.NewReader(toJSON(reqBody)),
1438+
configIDReqBody := struct {
1439+
ConfigID string `json:"config_id"`
1440+
}{
1441+
ConfigID: configID,
13341442
}
1335-
res, err := req.make()
1336-
assert.Nil(t, err, fmt.Sprintf("render preview unexpected error %s", err))
1337-
assert.Equal(t, http.StatusOK, res.StatusCode, fmt.Sprintf("expected status code %d got %d", http.StatusOK, res.StatusCode))
13381443

1339-
var got struct {
1340-
Content string `json:"content"`
1444+
expectedContent := "device=" + configID + " site=warehouse-1 topic=devices/gw-001/telemetry"
1445+
1446+
cases := []struct {
1447+
desc string
1448+
body string
1449+
profileErr error
1450+
configErr error
1451+
bindingsErr error
1452+
status int
1453+
}{
1454+
{
1455+
desc: "render preview with inline config and bindings",
1456+
body: toJSON(inlineReqBody),
1457+
status: http.StatusOK,
1458+
},
1459+
{
1460+
desc: "render preview with config_id loads from db",
1461+
body: toJSON(configIDReqBody),
1462+
status: http.StatusOK,
1463+
},
1464+
{
1465+
desc: "render preview with config_id and config not found",
1466+
body: toJSON(configIDReqBody),
1467+
configErr: svcerr.ErrNotFound,
1468+
status: http.StatusNotFound,
1469+
},
1470+
{
1471+
desc: "render preview with config_id and bindings error",
1472+
body: toJSON(configIDReqBody),
1473+
bindingsErr: svcerr.ErrViewEntity,
1474+
status: http.StatusUnprocessableEntity,
1475+
},
1476+
{
1477+
desc: "render preview with profile not found",
1478+
body: toJSON(inlineReqBody),
1479+
profileErr: svcerr.ErrNotFound,
1480+
status: http.StatusNotFound,
1481+
},
13411482
}
1342-
err = json.NewDecoder(res.Body).Decode(&got)
1343-
assert.Nil(t, err, fmt.Sprintf("decoding render preview expected to succeed: %s", err))
1344-
assert.Equal(t, "device="+configID+" site=warehouse-1 topic=devices/gw-001/telemetry", got.Content)
13451483

1346-
svcCall.Unset()
1347-
authCall.Unset()
1484+
for _, tc := range cases {
1485+
t.Run(tc.desc, func(t *testing.T) {
1486+
authCall := auth.On("Authenticate", mock.Anything, validToken).Return(session, nil)
1487+
svcCall := svc.On("ViewProfile", mock.Anything, session, profileID).Return(profile, tc.profileErr)
1488+
svcCall2 := svc.On("View", mock.Anything, session, configID).Return(storedConfig, tc.configErr)
1489+
svcCall3 := svc.On("ListBindings", mock.Anything, session, configID).Return(storedBindings, tc.bindingsErr)
1490+
1491+
req := testRequest{
1492+
client: bs.Client(),
1493+
method: http.MethodPost,
1494+
url: fmt.Sprintf("%s/%s/clients/bootstrap/profiles/%s/render-preview", bs.URL, domainID, profileID),
1495+
contentType: contentType,
1496+
token: validToken,
1497+
body: strings.NewReader(tc.body),
1498+
}
1499+
res, err := req.make()
1500+
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
1501+
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
1502+
1503+
if tc.status == http.StatusOK {
1504+
var got struct {
1505+
Content string `json:"content"`
1506+
}
1507+
err = json.NewDecoder(res.Body).Decode(&got)
1508+
assert.Nil(t, err, fmt.Sprintf("%s: decoding expected to succeed: %s", tc.desc, err))
1509+
assert.Equal(t, expectedContent, got.Content, fmt.Sprintf("%s: expected content %q got %q", tc.desc, expectedContent, got.Content))
1510+
}
1511+
1512+
svcCall3.Unset()
1513+
svcCall2.Unset()
1514+
svcCall.Unset()
1515+
authCall.Unset()
1516+
})
1517+
}
13481518
}
13491519

13501520
type config struct {

bootstrap/api/requests.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ func (req updateProfileReq) validate() error {
178178

179179
type renderPreviewReq struct {
180180
profileID string
181+
ConfigID string `json:"config_id,omitempty"`
181182
Config bootstrap.Config `json:"config"`
182183
RenderContext map[string]any `json:"render_context,omitempty"`
183184
Bindings []bootstrap.BindingSnapshot `json:"bindings,omitempty"`
@@ -204,6 +205,7 @@ func (req deleteProfileReq) validate() error {
204205
type listProfilesReq struct {
205206
offset uint64
206207
limit uint64
208+
name string
207209
}
208210

209211
func (req listProfilesReq) validate() error {

0 commit comments

Comments
 (0)