@@ -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\n template_format : go-template\n content_template: '{{ .Device.ID }}'\n " ,
1208+ body : "name: gateway\n content_format : go-template\n content_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'\n template_format = 'go-template'\n content_template = '{{ .Device.ID }}'\n " ,
1218+ body : "name = 'gateway'\n content_format = 'go-template'\n content_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\n content_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'\n content_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+
12491356func 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
13501520type config struct {
0 commit comments