• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mendersoftware / mender-server / 1807452801

08 May 2025 01:22PM UTC coverage: 65.386% (+0.1%) from 65.27%
1807452801

Pull #653

gitlab-ci

bahaa-ghazal
refactor(inventory): Migrate from ant0nie/go-json-rest to gin-gonic/gin

Ticket: MEN-8236
Changelog: Title
Signed-off-by: Bahaa Aldeen Ghazal <bahaa.ghazal@northern.tech>
Pull Request #653: refactor(inventory): Migrate from ant0nie/go-json-rest to gin-gonic/gin

476 of 525 new or added lines in 6 files covered. (90.67%)

62 existing lines in 9 files now uncovered.

31949 of 48862 relevant lines covered (65.39%)

1.37 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.37
/backend/services/inventory/api/http/api_inventory.go
1
// Copyright 2023 Northern.tech AS
2
//
3
//    Licensed under the Apache License, Version 2.0 (the "License");
4
//    you may not use this file except in compliance with the License.
5
//    You may obtain a copy of the License at
6
//
7
//        http://www.apache.org/licenses/LICENSE-2.0
8
//
9
//    Unless required by applicable law or agreed to in writing, software
10
//    distributed under the License is distributed on an "AS IS" BASIS,
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
//    See the License for the specific language governing permissions and
13
//    limitations under the License.
14

15
package http
16

17
import (
18
        "context"
19
        "fmt"
20
        "net/http"
21
        "strconv"
22
        "strings"
23
        "time"
24

25
        "github.com/gin-gonic/gin"
26
        validation "github.com/go-ozzo/ozzo-validation/v4"
27
        "github.com/pkg/errors"
28

29
        "github.com/mendersoftware/mender-server/pkg/identity"
30
        "github.com/mendersoftware/mender-server/pkg/rest.utils"
31
        inventory "github.com/mendersoftware/mender-server/services/inventory/inv"
32
        "github.com/mendersoftware/mender-server/services/inventory/model"
33
        "github.com/mendersoftware/mender-server/services/inventory/store"
34
        "github.com/mendersoftware/mender-server/services/inventory/utils"
35
)
36

37
const (
38
        apiUrlLegacy       = "/api/0.1.0"
39
        apiUrlManagementV1 = "/api/management/v1/inventory"
40
        apiUrlDevicesV1    = "/api/devices/v1/inventory"
41

42
        uriDevices       = "/devices"
43
        uriDevice        = "/devices/:id"
44
        uriDeviceTags    = "/devices/:id/tags"
45
        uriDeviceGroups  = "/devices/:id/group"
46
        uriDeviceGroup   = "/devices/:id/group/:name"
47
        uriGroups        = "/groups"
48
        uriGroupsName    = "/groups/:name"
49
        uriGroupsDevices = "/groups/:name/devices"
50
        uriAttributes    = "/attributes"
51

52
        apiUrlInternalV1         = "/api/internal/v1/inventory"
53
        uriInternalAlive         = "/alive"
54
        uriInternalHealth        = "/health"
55
        uriInternalTenants       = "/tenants"
56
        uriInternalDevices       = "/tenants/:tenant_id/devices"
57
        urlInternalDevicesStatus = "/tenants/:tenant_id/devices/status/:status"
58
        uriInternalDeviceDetails = "/tenants/:tenant_id/devices/:device_id"
59
        uriInternalDeviceGroups  = "/tenants/:tenant_id/devices/:device_id/groups"
60
        urlInternalAttributes    = "/tenants/:tenant_id/device/:device_id/attribute/scope/:scope"
61
        urlInternalReindex       = "/tenants/:tenant_id/devices/:device_id/reindex"
62
        apiUrlManagementV2       = "/api/management/v2/inventory"
63
        urlFiltersAttributes     = "/filters/attributes"
64
        urlFiltersSearch         = "/filters/search"
65

66
        apiUrlInternalV2         = "/api/internal/v2/inventory"
67
        urlInternalFiltersSearch = "/tenants/:tenant_id/filters/search"
68

69
        hdrTotalCount = "X-Total-Count"
70
)
71

72
const (
73
        queryParamGroup          = "group"
74
        queryParamSort           = "sort"
75
        queryParamHasGroup       = "has_group"
76
        queryParamValueSeparator = ":"
77
        queryParamScopeSeparator = "/"
78
        sortOrderAsc             = "asc"
79
        sortOrderDesc            = "desc"
80
        sortAttributeNameIdx     = 0
81
        sortOrderIdx             = 1
82
)
83

84
const (
85
        DefaultTimeout = time.Second * 10
86
)
87

88
const (
89
        checkInTimeParamName  = "check_in_time"
90
        checkInTimeParamScope = "system"
91
)
92

93
// model of device's group name response at /devices/:id/group endpoint
94
type InventoryApiGroup struct {
95
        Group model.GroupName `json:"group"`
96
}
97

98
func (g InventoryApiGroup) Validate() error {
3✔
99
        return g.Group.Validate()
3✔
100
}
3✔
101

102
func (i *InternalAPI) LivelinessHandler(c *gin.Context) {
2✔
103
        c.Status(http.StatusNoContent)
2✔
104
}
2✔
105

106
func (i *InternalAPI) HealthCheckHandler(c *gin.Context) {
2✔
107
        ctx := c.Request.Context()
2✔
108
        ctx, cancel := context.WithTimeout(ctx, DefaultTimeout)
2✔
109
        defer cancel()
2✔
110

2✔
111
        err := i.App.HealthCheck(ctx)
2✔
112
        if err != nil {
3✔
113
                rest.RenderError(c, http.StatusServiceUnavailable, err)
1✔
114
                return
1✔
115
        }
1✔
116

117
        c.Status(http.StatusNoContent)
2✔
118
}
119

120
// `sort` paramater value is an attribute name with optional direction (desc or asc)
121
// separated by colon (:)
122
//
123
// eg. `sort=attr_name1` or `sort=attr_name1:asc`
124
func parseSortParam(c *gin.Context) (*store.Sort, error) {
3✔
125
        sortStr, err := utils.ParseQueryParmStr(c.Request, queryParamSort, false, nil)
3✔
126
        if err != nil {
3✔
127
                return nil, err
×
128
        }
×
129
        if sortStr == "" {
6✔
130
                return nil, nil
3✔
131
        }
3✔
132
        sortValArray := strings.Split(sortStr, queryParamValueSeparator)
2✔
133
        attrNameWithScope := strings.SplitN(
2✔
134
                sortValArray[sortAttributeNameIdx],
2✔
135
                queryParamScopeSeparator,
2✔
136
                2,
2✔
137
        )
2✔
138
        var scope, attrName string
2✔
139
        if len(attrNameWithScope) == 1 {
4✔
140
                scope = model.AttrScopeInventory
2✔
141
                attrName = attrNameWithScope[0]
2✔
142
        } else {
2✔
143
                scope = attrNameWithScope[0]
×
144
                attrName = attrNameWithScope[1]
×
145
        }
×
146
        sort := store.Sort{AttrName: attrName, AttrScope: scope}
2✔
147
        if len(sortValArray) == 2 {
4✔
148
                sortOrder := sortValArray[sortOrderIdx]
2✔
149
                if sortOrder != sortOrderAsc && sortOrder != sortOrderDesc {
3✔
150
                        return nil, errors.New("invalid sort order")
1✔
151
                }
1✔
152
                sort.Ascending = sortOrder == sortOrderAsc
2✔
153
        }
154
        return &sort, nil
2✔
155
}
156

157
// Filter paramaters name are attributes name. Value can be prefixed
158
// with equality operator code (`eq` for =), separated from value by colon (:).
159
// Equality operator default value is `eq`
160
//
161
// eg. `attr_name1=value1` or `attr_name1=eq:value1`
162
func parseFilterParams(c *gin.Context) ([]store.Filter, error) {
3✔
163
        knownParams := []string{
3✔
164
                utils.PageName,
3✔
165
                utils.PerPageName,
3✔
166
                queryParamSort,
3✔
167
                queryParamHasGroup,
3✔
168
                queryParamGroup,
3✔
169
        }
3✔
170
        filters := make([]store.Filter, 0)
3✔
171
        var filter store.Filter
3✔
172
        for name := range c.Request.URL.Query() {
6✔
173
                if utils.ContainsString(name, knownParams) {
6✔
174
                        continue
3✔
175
                }
176
                valueStr, err := utils.ParseQueryParmStr(c.Request, name, false, nil)
3✔
177
                if err != nil {
3✔
178
                        return nil, err
×
179
                }
×
180

181
                attrNameWithScope := strings.SplitN(name, queryParamScopeSeparator, 2)
3✔
182
                var scope, attrName string
3✔
183
                if len(attrNameWithScope) == 1 {
6✔
184
                        scope = model.AttrScopeInventory
3✔
185
                        attrName = attrNameWithScope[0]
3✔
186
                } else {
4✔
187
                        scope = attrNameWithScope[0]
1✔
188
                        attrName = attrNameWithScope[1]
1✔
189
                }
1✔
190
                filter = store.Filter{AttrName: attrName, AttrScope: scope}
3✔
191

3✔
192
                // make sure we parse ':'s in value, it's either:
3✔
193
                // not there
3✔
194
                // after a valid operator specifier
3✔
195
                // or/and inside the value itself(mac, etc), in which case leave it alone
3✔
196
                sepIdx := strings.Index(valueStr, ":")
3✔
197
                if sepIdx == -1 {
5✔
198
                        filter.Value = valueStr
2✔
199
                        filter.Operator = store.Eq
2✔
200
                } else {
4✔
201
                        validOps := []string{"eq"}
2✔
202
                        for _, o := range validOps {
4✔
203
                                if valueStr[:sepIdx] == o {
3✔
204
                                        switch o {
1✔
205
                                        case "eq":
1✔
206
                                                filter.Operator = store.Eq
1✔
207
                                                filter.Value = valueStr[sepIdx+1:]
1✔
208
                                        }
209
                                        break
1✔
210
                                }
211
                        }
212

213
                        if filter.Value == "" {
4✔
214
                                filter.Value = valueStr
2✔
215
                                filter.Operator = store.Eq
2✔
216
                        }
2✔
217
                }
218

219
                floatValue, err := strconv.ParseFloat(filter.Value, 64)
3✔
220
                if err == nil {
5✔
221
                        filter.ValueFloat = &floatValue
2✔
222
                }
2✔
223

224
                timeValue, err := time.Parse("2006-01-02T15:04:05Z", filter.Value)
3✔
225
                if err == nil {
4✔
226
                        filter.ValueTime = &timeValue
1✔
227
                }
1✔
228

229
                filters = append(filters, filter)
3✔
230
        }
231
        return filters, nil
3✔
232
}
233

234
func (i *ManagementAPI) GetDevicesHandler(c *gin.Context) {
3✔
235
        ctx := c.Request.Context()
3✔
236

3✔
237
        page, perPage, err := rest.ParsePagingParameters(c.Request)
3✔
238
        if err != nil {
4✔
239
                rest.RenderError(c, http.StatusBadRequest, err)
1✔
240
                return
1✔
241
        }
1✔
242

243
        hasGroup, err := utils.ParseQueryParmBool(c.Request, queryParamHasGroup, false, nil)
3✔
244
        if err != nil {
4✔
245
                rest.RenderError(c, http.StatusBadRequest, err)
1✔
246
                return
1✔
247
        }
1✔
248

249
        groupName, err := utils.ParseQueryParmStr(c.Request, "group", false, nil)
3✔
250
        if err != nil {
3✔
NEW
251
                rest.RenderError(c, http.StatusBadRequest, err)
×
252
                return
×
253
        }
×
254

255
        sort, err := parseSortParam(c)
3✔
256
        if err != nil {
4✔
257
                rest.RenderError(c, http.StatusBadRequest, err)
1✔
258
                return
1✔
259
        }
1✔
260

261
        filters, err := parseFilterParams(c)
3✔
262
        if err != nil {
3✔
NEW
263
                rest.RenderError(c, http.StatusBadRequest, err)
×
264
                return
×
265
        }
×
266

267
        ld := store.ListQuery{Skip: int((page - 1) * perPage),
3✔
268
                Limit:     int(perPage),
3✔
269
                Filters:   filters,
3✔
270
                Sort:      sort,
3✔
271
                HasGroup:  hasGroup,
3✔
272
                GroupName: groupName}
3✔
273

3✔
274
        devs, totalCount, err := i.App.ListDevices(ctx, ld)
3✔
275

3✔
276
        if err != nil {
4✔
277
                rest.RenderError(c,
1✔
278
                        http.StatusInternalServerError,
1✔
279
                        errors.New("internal error"),
1✔
280
                )
1✔
281
                return
1✔
282
        }
1✔
283

284
        hasNext := totalCount > int(page*perPage)
3✔
285

3✔
286
        hints := rest.NewPagingHints().
3✔
287
                SetPage(page).
3✔
288
                SetPerPage(perPage).
3✔
289
                SetHasNext(hasNext).
3✔
290
                SetTotalCount(int64(totalCount))
3✔
291

3✔
292
        links, err := rest.MakePagingHeaders(c.Request, hints)
3✔
293
        if err != nil {
3✔
NEW
UNCOV
294
                rest.RenderError(c,
×
NEW
295
                        http.StatusInternalServerError,
×
NEW
UNCOV
296
                        errors.New("internal error"),
×
NEW
UNCOV
297
                )
×
NEW
298
                return
×
NEW
299
        }
×
300
        for _, l := range links {
6✔
301
                c.Writer.Header().Add("Link", l)
3✔
302
        }
3✔
303
        // the response writer will ensure the header name is in Kebab-Pascal-Case
304
        c.Writer.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
3✔
305
        c.JSON(http.StatusOK, devs)
3✔
306
}
307

308
func (i *ManagementAPI) GetDeviceHandler(c *gin.Context) {
3✔
309
        ctx := c.Request.Context()
3✔
310

3✔
311
        deviceID := c.Param("id")
3✔
312

3✔
313
        dev, err := i.App.GetDevice(ctx, model.DeviceID(deviceID))
3✔
314
        if err != nil {
4✔
315
                rest.RenderError(c,
1✔
316
                        http.StatusInternalServerError,
1✔
317
                        errors.New("internal error"),
1✔
318
                )
1✔
319
                return
1✔
320
        }
1✔
321
        if dev == nil {
5✔
322
                rest.RenderError(c,
2✔
323
                        http.StatusNotFound,
2✔
324
                        store.ErrDevNotFound,
2✔
325
                )
2✔
326
                return
2✔
327
        }
2✔
328
        if dev.TagsEtag != "" {
4✔
329
                c.Header("ETag", dev.TagsEtag)
1✔
330
        }
1✔
331
        c.JSON(http.StatusOK, dev)
3✔
332
}
333

334
func (i *ManagementAPI) DeleteDeviceInventoryHandler(c *gin.Context) {
1✔
335
        ctx := c.Request.Context()
1✔
336

1✔
337
        deviceID := c.Param("id")
1✔
338

1✔
339
        err := i.App.ReplaceAttributes(ctx, model.DeviceID(deviceID),
1✔
340
                model.DeviceAttributes{}, model.AttrScopeInventory, "")
1✔
341
        if err != nil && err != store.ErrDevNotFound {
2✔
342
                rest.RenderError(c,
1✔
343
                        http.StatusInternalServerError,
1✔
344
                        errors.New("internal error"),
1✔
345
                )
1✔
346
                return
1✔
347
        }
1✔
348

349
        c.Status(http.StatusNoContent)
1✔
350
}
351

352
func (i *InternalAPI) DeleteDeviceHandler(c *gin.Context) {
2✔
353
        ctx := c.Request.Context()
2✔
354
        tenantId := c.Param("tenant_id")
2✔
355
        if tenantId != "" {
4✔
356
                id := &identity.Identity{
2✔
357
                        Tenant: tenantId,
2✔
358
                }
2✔
359
                ctx = identity.WithContext(ctx, id)
2✔
360
        }
2✔
361

362
        deviceID := c.Param("device_id")
2✔
363

2✔
364
        err := i.App.DeleteDevice(ctx, model.DeviceID(deviceID))
2✔
365
        if err != nil && err != store.ErrDevNotFound {
3✔
366
                rest.RenderError(c,
1✔
367
                        http.StatusInternalServerError,
1✔
368
                        errors.New("internal error"),
1✔
369
                )
1✔
370
                return
1✔
371
        }
1✔
372

373
        c.Status(http.StatusNoContent)
2✔
374
}
375

376
func (i *InternalAPI) AddDeviceHandler(c *gin.Context) {
3✔
377
        ctx := c.Request.Context()
3✔
378
        tenantId := c.Param("tenant_id")
3✔
379
        if tenantId != "" {
5✔
380
                id := &identity.Identity{
2✔
381
                        Tenant: tenantId,
2✔
382
                }
2✔
383
                ctx = identity.WithContext(ctx, id)
2✔
384
        }
2✔
385

386
        dev, err := parseDevice(c)
3✔
387
        if err != nil {
5✔
388
                rest.RenderError(c,
2✔
389
                        http.StatusBadRequest,
2✔
390
                        err,
2✔
391
                )
2✔
392
                return
2✔
393
        }
2✔
394

395
        err = dev.Attributes.Validate()
3✔
396
        if err != nil {
3✔
NEW
UNCOV
397
                rest.RenderError(c,
×
NEW
UNCOV
398
                        http.StatusBadRequest,
×
NEW
399
                        err,
×
NEW
400
                )
×
401
                return
×
UNCOV
402
        }
×
403

404
        err = i.App.AddDevice(ctx, dev)
3✔
405
        if err != nil {
4✔
406
                rest.RenderError(c,
1✔
407
                        http.StatusInternalServerError,
1✔
408
                        errors.New("internal error"),
1✔
409
                )
1✔
410
                return
1✔
411
        }
1✔
412

413
        c.Writer.Header().Add("Location", "devices/"+dev.ID.String())
3✔
414
        c.Status(http.StatusCreated)
3✔
415
}
416

417
func (i *ManagementAPI) UpdateDeviceAttributesHandler(c *gin.Context) {
2✔
418
        ctx := c.Request.Context()
2✔
419

2✔
420
        var idata *identity.Identity
2✔
421
        if idata = identity.FromContext(ctx); idata == nil || !idata.IsDevice {
3✔
422
                rest.RenderError(c,
1✔
423
                        http.StatusUnauthorized,
1✔
424
                        errors.New("unauthorized"),
1✔
425
                )
1✔
426
                return
1✔
427
        }
1✔
428
        deviceID := model.DeviceID(idata.Subject)
2✔
429
        //extract attributes from body
2✔
430
        attrs, err := parseAttributes(c)
2✔
431
        if err != nil {
4✔
432
                rest.RenderError(c,
2✔
433
                        http.StatusBadRequest,
2✔
434
                        err,
2✔
435
                )
2✔
436
                return
2✔
437
        }
2✔
438
        i.updateDeviceAttributes(c, attrs, deviceID, model.AttrScopeInventory, "")
2✔
439
}
440

441
func (i *ManagementAPI) UpdateDeviceTagsHandler(c *gin.Context) {
2✔
442
        // get device ID from uri
2✔
443
        deviceID := model.DeviceID(c.Param("id"))
2✔
444
        if len(deviceID) < 1 {
2✔
NEW
UNCOV
445
                rest.RenderError(c,
×
NEW
UNCOV
446
                        http.StatusBadRequest,
×
NEW
UNCOV
447
                        errors.New("device id cannot be empty"),
×
NEW
UNCOV
448
                )
×
UNCOV
449
                return
×
UNCOV
450
        }
×
451

452
        ifMatchHeader := c.Request.Header.Get("If-Match")
2✔
453

2✔
454
        // extract attributes from body
2✔
455
        attrs, err := parseAttributes(c)
2✔
456
        if err != nil {
2✔
NEW
UNCOV
457
                rest.RenderError(c,
×
NEW
458
                        http.StatusBadRequest,
×
NEW
459
                        err,
×
NEW
UNCOV
460
                )
×
UNCOV
461
                return
×
UNCOV
462
        }
×
463

464
        // set scope and timestamp for tags attributes
465
        now := time.Now()
2✔
466
        for i := range attrs {
4✔
467
                attrs[i].Scope = model.AttrScopeTags
2✔
468
                if attrs[i].Timestamp == nil {
4✔
469
                        attrs[i].Timestamp = &now
2✔
470
                }
2✔
471
        }
472

473
        i.updateDeviceAttributes(c, attrs, deviceID, model.AttrScopeTags, ifMatchHeader)
2✔
474
}
475

476
func (i *ManagementAPI) updateDeviceAttributes(
477
        c *gin.Context,
478
        attrs model.DeviceAttributes,
479
        deviceID model.DeviceID,
480
        scope string,
481
        etag string,
482
) {
3✔
483
        ctx := c.Request.Context()
3✔
484

3✔
485
        var err error
3✔
486

3✔
487
        // upsert or replace the attributes
3✔
488
        if c.Request.Method == http.MethodPatch {
6✔
489
                err = i.App.UpsertAttributesWithUpdated(ctx, deviceID, attrs, scope, etag)
3✔
490
        } else if c.Request.Method == http.MethodPut {
7✔
491
                err = i.App.ReplaceAttributes(ctx, deviceID, attrs, scope, etag)
2✔
492
        } else {
2✔
NEW
UNCOV
493
                rest.RenderError(c,
×
NEW
UNCOV
494
                        http.StatusMethodNotAllowed,
×
NEW
UNCOV
495
                        errors.New("method not alllowed"),
×
NEW
496
                )
×
UNCOV
497
                return
×
UNCOV
498
        }
×
499

500
        cause := errors.Cause(err)
3✔
501
        switch cause {
3✔
UNCOV
502
        case store.ErrNoAttrName:
×
503
        case inventory.ErrTooManyAttributes:
1✔
504
                rest.RenderError(c, http.StatusBadRequest, err)
1✔
505
                return
1✔
506
        case inventory.ErrETagDoesntMatch:
2✔
507
                rest.RenderError(c,
2✔
508
                        http.StatusPreconditionFailed,
2✔
509
                        cause,
2✔
510
                )
2✔
511
                return
2✔
512
        }
513
        if err != nil {
4✔
514
                rest.RenderError(c,
1✔
515
                        http.StatusInternalServerError,
1✔
516
                        errors.New("internal error"),
1✔
517
                )
1✔
518
                return
1✔
519
        }
1✔
520

521
        c.Status(http.StatusOK)
3✔
522
}
523

524
func (i *InternalAPI) PatchDeviceAttributesInternalHandler(
525
        c *gin.Context,
526
) {
2✔
527
        ctx := c.Request.Context()
2✔
528
        tenantId := c.Param("tenant_id")
2✔
529
        ctx = getTenantContext(ctx, tenantId)
2✔
530

2✔
531
        deviceId := c.Param("device_id")
2✔
532
        if len(deviceId) < 1 {
3✔
533
                rest.RenderError(c, http.StatusBadRequest, errors.New("device id cannot be empty"))
1✔
534
                return
1✔
535
        }
1✔
536
        //extract attributes from body
537
        attrs, err := parseAttributes(c)
2✔
538
        if err != nil {
4✔
539
                rest.RenderError(c, http.StatusBadRequest, err)
2✔
540
                return
2✔
541
        }
2✔
542
        for i := range attrs {
4✔
543
                attrs[i].Scope = c.Param("scope")
2✔
544
                if attrs[i].Name == checkInTimeParamName && attrs[i].Scope == checkInTimeParamScope {
3✔
545
                        t, err := time.Parse(time.RFC3339, fmt.Sprintf("%v", attrs[i].Value))
1✔
546
                        if err != nil {
1✔
NEW
UNCOV
547
                                rest.RenderError(c, http.StatusBadRequest, err)
×
UNCOV
548
                                return
×
549
                        }
×
550
                        attrs[i].Value = t
1✔
551
                }
552
        }
553

554
        //upsert the attributes
555
        err = i.App.UpsertAttributes(ctx, model.DeviceID(deviceId), attrs)
2✔
556
        cause := errors.Cause(err)
2✔
557
        switch cause {
2✔
UNCOV
558
        case store.ErrNoAttrName:
×
NEW
UNCOV
559
                rest.RenderError(c, http.StatusBadRequest, cause)
×
560
                return
×
561
        }
562
        if err != nil {
3✔
563
                rest.RenderError(c,
1✔
564
                        http.StatusInternalServerError,
1✔
565
                        errors.New("internal error"),
1✔
566
                )
1✔
567
                return
1✔
568
        }
1✔
569

570
        c.Status(http.StatusOK)
2✔
571
}
572

573
func (i *ManagementAPI) DeleteDeviceGroupHandler(c *gin.Context) {
2✔
574
        ctx := c.Request.Context()
2✔
575

2✔
576
        deviceID := c.Param("id")
2✔
577
        groupName := c.Param("name")
2✔
578

2✔
579
        err := i.App.UnsetDeviceGroup(ctx, model.DeviceID(deviceID), model.GroupName(groupName))
2✔
580
        if err != nil {
4✔
581
                cause := errors.Cause(err)
2✔
582
                if cause != nil {
4✔
583
                        if cause.Error() == store.ErrDevNotFound.Error() {
4✔
584
                                rest.RenderError(c, http.StatusNotFound, err)
2✔
585
                                return
2✔
586
                        }
2✔
587
                }
588
                rest.RenderError(c,
1✔
589
                        http.StatusInternalServerError,
1✔
590
                        errors.New("internal error"),
1✔
591
                )
1✔
592
                return
1✔
593
        }
594

595
        c.Status(http.StatusNoContent)
2✔
596
}
597

598
func (i *ManagementAPI) AddDeviceToGroupHandler(c *gin.Context) {
3✔
599
        ctx := c.Request.Context()
3✔
600

3✔
601
        devId := c.Param("id")
3✔
602

3✔
603
        var group InventoryApiGroup
3✔
604
        err := c.ShouldBindJSON(&group)
3✔
605
        if err != nil {
4✔
606
                rest.RenderError(c,
1✔
607
                        http.StatusBadRequest,
1✔
608
                        errors.Wrap(err, "failed to decode device group data"))
1✔
609
                return
1✔
610
        }
1✔
611

612
        if err = group.Validate(); err != nil {
4✔
613
                rest.RenderError(c, http.StatusBadRequest, err)
1✔
614
                return
1✔
615
        }
1✔
616

617
        err = i.App.UpdateDeviceGroup(ctx, model.DeviceID(devId), model.GroupName(group.Group))
3✔
618
        if err != nil {
4✔
619
                if cause := errors.Cause(err); cause != nil && cause == store.ErrDevNotFound {
2✔
620
                        rest.RenderError(c, http.StatusNotFound, err)
1✔
621
                        return
1✔
622
                }
1✔
623
                rest.RenderError(c,
1✔
624
                        http.StatusInternalServerError,
1✔
625
                        errors.New("internal error"),
1✔
626
                )
1✔
627
                return
1✔
628
        }
629
        c.Status(http.StatusNoContent)
3✔
630
}
631

632
func (i *ManagementAPI) GetDevicesByGroupHandler(c *gin.Context) {
2✔
633
        ctx := c.Request.Context()
2✔
634

2✔
635
        group := c.Param("name")
2✔
636

2✔
637
        page, perPage, err := rest.ParsePagingParameters(c.Request)
2✔
638
        if err != nil {
3✔
639
                rest.RenderError(c, http.StatusBadRequest, err)
1✔
640
                return
1✔
641
        }
1✔
642

643
        //get one extra device to see if there's a 'next' page
644
        ids, totalCount, err := i.App.ListDevicesByGroup(
2✔
645
                ctx,
2✔
646
                model.GroupName(group),
2✔
647
                int((page-1)*perPage),
2✔
648
                int(perPage),
2✔
649
        )
2✔
650
        if err != nil {
4✔
651
                if err == store.ErrGroupNotFound {
4✔
652
                        rest.RenderError(c, http.StatusNotFound, err)
2✔
653

2✔
654
                } else {
3✔
655
                        rest.RenderError(c,
1✔
656
                                http.StatusInternalServerError,
1✔
657
                                errors.New("internal error"),
1✔
658
                        )
1✔
659
                }
1✔
660
                return
2✔
661
        }
662

663
        hasNext := totalCount > int(page*perPage)
2✔
664

2✔
665
        hints := rest.NewPagingHints().
2✔
666
                SetPage(page).
2✔
667
                SetPerPage(perPage).
2✔
668
                SetHasNext(hasNext).
2✔
669
                SetTotalCount(int64(totalCount))
2✔
670

2✔
671
        links, err := rest.MakePagingHeaders(c.Request, hints)
2✔
672
        if err != nil {
2✔
NEW
UNCOV
673
                rest.RenderError(c,
×
NEW
UNCOV
674
                        http.StatusInternalServerError,
×
NEW
UNCOV
675
                        errors.New("internal error"),
×
NEW
676
                )
×
NEW
UNCOV
677
                return
×
NEW
UNCOV
678
        }
×
679
        for _, l := range links {
4✔
680
                c.Writer.Header().Add("Link", l)
2✔
681
        }
2✔
682
        // the response writer will ensure the header name is in Kebab-Pascal-Case
683
        c.Writer.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
2✔
684
        c.JSON(http.StatusOK, ids)
2✔
685
}
686

687
func (i *ManagementAPI) AppendDevicesToGroup(c *gin.Context) {
1✔
688
        var deviceIDs []model.DeviceID
1✔
689
        ctx := c.Request.Context()
1✔
690
        groupName := model.GroupName(c.Param("name"))
1✔
691
        if err := groupName.Validate(); err != nil {
2✔
692
                rest.RenderError(c, http.StatusBadRequest, err)
1✔
693
                return
1✔
694
        }
1✔
695

696
        if err := c.ShouldBindJSON(&deviceIDs); err != nil {
2✔
697
                rest.RenderError(c,
1✔
698
                        http.StatusBadRequest,
1✔
699
                        errors.Wrap(err, "invalid payload schema"),
1✔
700
                )
1✔
701
                return
1✔
702
        } else if len(deviceIDs) == 0 {
3✔
703
                rest.RenderError(c,
1✔
704
                        http.StatusBadRequest,
1✔
705
                        errors.New("no device IDs present in payload"),
1✔
706
                )
1✔
707
                return
1✔
708
        }
1✔
709
        updated, err := i.App.UpdateDevicesGroup(
1✔
710
                ctx, deviceIDs, groupName,
1✔
711
        )
1✔
712
        if err != nil {
2✔
713
                rest.RenderError(c,
1✔
714
                        http.StatusInternalServerError,
1✔
715
                        errors.New("internal error"),
1✔
716
                )
1✔
717
                return
1✔
718
        }
1✔
719
        c.JSON(http.StatusOK, updated)
1✔
720
}
721

722
func (i *ManagementAPI) DeleteGroupHandler(c *gin.Context) {
1✔
723
        ctx := c.Request.Context()
1✔
724

1✔
725
        groupName := model.GroupName(c.Param("name"))
1✔
726
        if err := groupName.Validate(); err != nil {
2✔
727
                rest.RenderError(c, http.StatusBadRequest, err)
1✔
728
                return
1✔
729
        }
1✔
730

731
        updated, err := i.App.DeleteGroup(ctx, groupName)
1✔
732
        if err != nil {
2✔
733
                rest.RenderError(c,
1✔
734
                        http.StatusInternalServerError,
1✔
735
                        errors.New("internal error"),
1✔
736
                )
1✔
737
                return
1✔
738
        }
1✔
739
        c.JSON(http.StatusOK, updated)
1✔
740
}
741

742
func (i *ManagementAPI) ClearDevicesGroupHandler(c *gin.Context) {
1✔
743
        var deviceIDs []model.DeviceID
1✔
744
        ctx := c.Request.Context()
1✔
745

1✔
746
        groupName := model.GroupName(c.Param("name"))
1✔
747
        if err := groupName.Validate(); err != nil {
2✔
748
                rest.RenderError(c, http.StatusBadRequest, err)
1✔
749
                return
1✔
750
        }
1✔
751

752
        if err := c.ShouldBindJSON(&deviceIDs); err != nil {
2✔
753
                rest.RenderError(c,
1✔
754
                        http.StatusBadRequest,
1✔
755
                        errors.Wrap(err, "invalid payload schema"),
1✔
756
                )
1✔
757
                return
1✔
758
        } else if len(deviceIDs) == 0 {
3✔
759
                rest.RenderError(c,
1✔
760
                        http.StatusBadRequest,
1✔
761
                        errors.New("no device IDs present in payload"),
1✔
762
                )
1✔
763
                return
1✔
764
        }
1✔
765

766
        updated, err := i.App.UnsetDevicesGroup(ctx, deviceIDs, groupName)
1✔
767
        if err != nil {
2✔
768
                rest.RenderError(c,
1✔
769
                        http.StatusInternalServerError,
1✔
770
                        errors.New("internal error"),
1✔
771
                )
1✔
772
                return
1✔
773
        }
1✔
774

775
        c.JSON(http.StatusOK, updated)
1✔
776
}
777

778
func parseDevice(c *gin.Context) (*model.Device, error) {
3✔
779
        dev := model.Device{}
3✔
780

3✔
781
        //decode body
3✔
782
        err := c.ShouldBindJSON(&dev)
3✔
783
        if err != nil {
5✔
784
                return nil, errors.Wrap(err, "failed to decode request body")
2✔
785
        }
2✔
786

787
        if err := dev.Validate(); err != nil {
4✔
788
                return nil, err
1✔
789
        }
1✔
790

791
        return &dev, nil
3✔
792
}
793

794
func parseAttributes(c *gin.Context) (model.DeviceAttributes, error) {
3✔
795
        var attrs model.DeviceAttributes
3✔
796

3✔
797
        err := c.ShouldBindJSON(&attrs)
3✔
798
        if err != nil {
5✔
799
                return nil, errors.Wrap(err, "failed to decode request body")
2✔
800
        }
2✔
801

802
        err = attrs.Validate()
3✔
803
        if err != nil {
5✔
804
                return nil, err
2✔
805
        }
2✔
806

807
        return attrs, nil
3✔
808
}
809

810
func (i *ManagementAPI) GetGroupsHandler(c *gin.Context) {
2✔
811
        var fltr []model.FilterPredicate
2✔
812
        ctx := c.Request.Context()
2✔
813

2✔
814
        query := c.Request.URL.Query()
2✔
815
        status := query.Get("status")
2✔
816
        if status != "" {
3✔
817
                fltr = []model.FilterPredicate{{
1✔
818
                        Attribute: "status",
1✔
819
                        Scope:     "identity",
1✔
820
                        Type:      "$eq",
1✔
821
                        Value:     status,
1✔
822
                }}
1✔
823
        }
1✔
824

825
        groups, err := i.App.ListGroups(ctx, fltr)
2✔
826
        if err != nil {
3✔
827
                rest.RenderError(c,
1✔
828
                        http.StatusInternalServerError,
1✔
829
                        errors.New("internal error"),
1✔
830
                )
1✔
831
                return
1✔
832
        }
1✔
833

834
        if groups == nil {
3✔
835
                groups = []model.GroupName{}
1✔
836
        }
1✔
837

838
        c.JSON(http.StatusOK, groups)
2✔
839
}
840

841
func (i *ManagementAPI) GetDeviceGroupHandler(c *gin.Context) {
1✔
842
        ctx := c.Request.Context()
1✔
843

1✔
844
        deviceID := c.Param("id")
1✔
845

1✔
846
        group, err := i.App.GetDeviceGroup(ctx, model.DeviceID(deviceID))
1✔
847
        if err != nil {
2✔
848
                if err == store.ErrDevNotFound {
2✔
849
                        rest.RenderError(c,
1✔
850
                                http.StatusNotFound,
1✔
851
                                store.ErrDevNotFound,
1✔
852
                        )
1✔
853
                } else {
2✔
854
                        rest.RenderError(c,
1✔
855
                                http.StatusInternalServerError,
1✔
856
                                errors.New("internal error"),
1✔
857
                        )
1✔
858
                }
1✔
859
                return
1✔
860
        }
861

862
        ret := map[string]*model.GroupName{"group": nil}
1✔
863

1✔
864
        if group != "" {
2✔
865
                ret["group"] = &group
1✔
866
        }
1✔
867

868
        c.JSON(http.StatusOK, ret)
1✔
869
}
870

871
type newTenantRequest struct {
872
        TenantID string `json:"tenant_id" valid:"required"`
873
}
874

875
func (t newTenantRequest) Validate() error {
2✔
876
        return validation.ValidateStruct(&t,
2✔
877
                validation.Field(&t.TenantID, validation.Required),
2✔
878
        )
2✔
879
}
2✔
880
func (i *InternalAPI) CreateTenantHandler(c *gin.Context) {
3✔
881
        ctx := c.Request.Context()
3✔
882

3✔
883
        var newTenant newTenantRequest
3✔
884

3✔
885
        if err := c.ShouldBindJSON(&newTenant); err != nil {
5✔
886
                rest.RenderError(c, http.StatusBadRequest, err)
2✔
887
                return
2✔
888
        }
2✔
889

890
        if err := newTenant.Validate(); err != nil {
4✔
891
                rest.RenderError(c, http.StatusBadRequest, err)
2✔
892
                return
2✔
893
        }
2✔
894

895
        err := i.App.CreateTenant(ctx, model.NewTenant{
2✔
896
                ID: newTenant.TenantID,
2✔
897
        })
2✔
898
        if err != nil {
3✔
899
                rest.RenderError(c,
1✔
900
                        http.StatusInternalServerError,
1✔
901
                        errors.New("internal error"),
1✔
902
                )
1✔
903
                return
1✔
904
        }
1✔
905

906
        c.Status(http.StatusCreated)
2✔
907
}
908

909
func (i *ManagementAPI) FiltersAttributesHandler(c *gin.Context) {
2✔
910
        ctx := c.Request.Context()
2✔
911

2✔
912
        // query the database
2✔
913
        attributes, err := i.App.GetFiltersAttributes(ctx)
2✔
914
        if err != nil {
3✔
915
                rest.RenderError(c,
1✔
916
                        http.StatusInternalServerError,
1✔
917
                        errors.New("internal error"),
1✔
918
                )
1✔
919
                return
1✔
920
        }
1✔
921

922
        // in case of nil make sure we return empty list
923
        if attributes == nil {
4✔
924
                attributes = []model.FilterAttribute{}
2✔
925
        }
2✔
926

927
        c.JSON(http.StatusOK, attributes)
2✔
928
}
929

930
func (i *ManagementAPI) FiltersSearchHandler(c *gin.Context) {
1✔
931
        ctx := c.Request.Context()
1✔
932

1✔
933
        //extract attributes from body
1✔
934
        searchParams, err := parseSearchParams(c)
1✔
935
        if err != nil {
2✔
936
                rest.RenderError(c, http.StatusBadRequest, err)
1✔
937
                return
1✔
938
        }
1✔
939

940
        // query the database
941
        devs, totalCount, err := i.App.SearchDevices(ctx, *searchParams)
1✔
942
        if err != nil {
2✔
943
                if strings.Contains(err.Error(), "BadValue") {
2✔
944
                        rest.RenderError(c, http.StatusBadRequest, err)
1✔
945
                } else {
2✔
946
                        rest.RenderError(c,
1✔
947
                                http.StatusInternalServerError,
1✔
948
                                errors.New("internal error"),
1✔
949
                        )
1✔
950
                }
1✔
951
                return
1✔
952
        }
953

954
        // the response writer will ensure the header name is in Kebab-Pascal-Case
955
        c.Writer.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
1✔
956
        c.JSON(http.StatusOK, devs)
1✔
957
}
958

959
func (i *InternalAPI) InternalFiltersSearchHandler(c *gin.Context) {
2✔
960
        ctx := c.Request.Context()
2✔
961

2✔
962
        tenantId := c.Param("tenant_id")
2✔
963
        if tenantId != "" {
4✔
964
                ctx = getTenantContext(ctx, tenantId)
2✔
965
        }
2✔
966

967
        //extract attributes from body
968
        searchParams, err := parseSearchParams(c)
2✔
969
        if err != nil {
4✔
970
                rest.RenderError(c, http.StatusBadRequest, err)
2✔
971
                return
2✔
972
        }
2✔
973

974
        // query the database
975
        devs, totalCount, err := i.App.SearchDevices(ctx, *searchParams)
2✔
976
        if err != nil {
3✔
977
                if strings.Contains(err.Error(), "BadValue") {
2✔
978
                        rest.RenderError(c, http.StatusBadRequest, err)
1✔
979
                } else {
2✔
980
                        rest.RenderError(c,
1✔
981
                                http.StatusInternalServerError,
1✔
982
                                errors.New("internal error"),
1✔
983
                        )
1✔
984
                }
1✔
985
                return
1✔
986
        }
987

988
        // the response writer will ensure the header name is in Kebab-Pascal-Case
989
        c.Writer.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
2✔
990
        c.JSON(http.StatusOK, devs)
2✔
991
}
992

993
func getTenantContext(ctx context.Context, tenantId string) context.Context {
2✔
994
        if ctx == nil {
2✔
UNCOV
995
                ctx = context.Background()
×
UNCOV
996
        }
×
997
        if tenantId == "" {
4✔
998
                return ctx
2✔
999
        }
2✔
1000
        id := &identity.Identity{
2✔
1001
                Tenant: tenantId,
2✔
1002
        }
2✔
1003

2✔
1004
        ctx = identity.WithContext(ctx, id)
2✔
1005

2✔
1006
        return ctx
2✔
1007
}
1008

1009
func (i *InternalAPI) InternalDevicesStatusHandler(c *gin.Context) {
2✔
1010
        const (
2✔
1011
                StatusDecommissioned = "decommissioned"
2✔
1012
                StatusAccepted       = "accepted"
2✔
1013
                StatusRejected       = "rejected"
2✔
1014
                StatusPreauthorized  = "preauthorized"
2✔
1015
                StatusPending        = "pending"
2✔
1016
                StatusNoAuth         = "noauth"
2✔
1017
        )
2✔
1018
        var (
2✔
1019
                devices []model.DeviceUpdate
2✔
1020
                result  *model.UpdateResult
2✔
1021
        )
2✔
1022

2✔
1023
        ctx := c.Request.Context()
2✔
1024

2✔
1025
        tenantID := c.Param("tenant_id")
2✔
1026
        ctx = getTenantContext(ctx, tenantID)
2✔
1027

2✔
1028
        status := c.Param("status")
2✔
1029

2✔
1030
        err := c.ShouldBindJSON(&devices)
2✔
1031
        if err != nil {
4✔
1032
                rest.RenderError(c,
2✔
1033
                        http.StatusBadRequest,
2✔
1034
                        errors.Wrap(err, "cant parse devices"),
2✔
1035
                )
2✔
1036
                return
2✔
1037
        }
2✔
1038

1039
        switch status {
2✔
1040
        case StatusAccepted, StatusPreauthorized,
1041
                StatusPending, StatusRejected,
1042
                StatusNoAuth:
2✔
1043
                // Update statuses
2✔
1044
                attrs := model.DeviceAttributes{{
2✔
1045
                        Name:  "status",
2✔
1046
                        Scope: model.AttrScopeIdentity,
2✔
1047
                        Value: status,
2✔
1048
                }}
2✔
1049
                result, err = i.App.UpsertDevicesStatuses(ctx, devices, attrs)
2✔
1050
        case StatusDecommissioned:
1✔
1051
                // Delete Inventory
1✔
1052
                result, err = i.App.DeleteDevices(ctx, getIdsFromDevices(devices))
1✔
1053
        default:
1✔
1054
                // Unrecognized status
1✔
1055
                rest.RenderError(c,
1✔
1056
                        http.StatusNotFound,
1✔
1057
                        errors.Errorf("unrecognized status: %s", status),
1✔
1058
                )
1✔
1059
                return
1✔
1060
        }
1061
        if err == store.ErrWriteConflict {
3✔
1062
                rest.RenderError(c,
1✔
1063
                        http.StatusConflict,
1✔
1064
                        err,
1✔
1065
                )
1✔
1066
                return
1✔
1067
        } else if err != nil {
4✔
1068
                rest.RenderError(c,
1✔
1069
                        http.StatusInternalServerError,
1✔
1070
                        errors.New("internal error"),
1✔
1071
                )
1✔
1072
                return
1✔
1073
        }
1✔
1074

1075
        c.JSON(http.StatusOK, result)
2✔
1076
}
1077

1078
func (i *InternalAPI) GetDeviceGroupsInternalHandler(c *gin.Context) {
2✔
1079
        ctx := c.Request.Context()
2✔
1080

2✔
1081
        tenantId := c.Param("tenant_id")
2✔
1082
        ctx = getTenantContext(ctx, tenantId)
2✔
1083

2✔
1084
        deviceID := c.Param("device_id")
2✔
1085
        group, err := i.App.GetDeviceGroup(ctx, model.DeviceID(deviceID))
2✔
1086
        if err != nil {
4✔
1087
                if err == store.ErrDevNotFound {
4✔
1088
                        rest.RenderError(c,
2✔
1089
                                http.StatusNotFound,
2✔
1090
                                store.ErrDevNotFound,
2✔
1091
                        )
2✔
1092
                } else {
3✔
1093
                        rest.RenderError(c,
1✔
1094
                                http.StatusInternalServerError,
1✔
1095
                                errors.New("internal error"),
1✔
1096
                        )
1✔
1097
                }
1✔
1098
                return
2✔
1099
        }
1100

1101
        res := model.DeviceGroups{}
2✔
1102
        if group != "" {
3✔
1103
                res.Groups = append(res.Groups, string(group))
1✔
1104
        }
1✔
1105

1106
        c.JSON(http.StatusOK, res)
2✔
1107
}
1108

1109
func (i *InternalAPI) ReindexDeviceDataHandler(c *gin.Context) {
2✔
1110
        ctx := c.Request.Context()
2✔
1111
        tenantId := c.Param("tenant_id")
2✔
1112
        ctx = getTenantContext(ctx, tenantId)
2✔
1113

2✔
1114
        deviceId := c.Param("device_id")
2✔
1115
        if len(deviceId) < 1 {
3✔
1116
                rest.RenderError(c,
1✔
1117
                        http.StatusBadRequest,
1✔
1118
                        errors.New("device id cannot be empty"),
1✔
1119
                )
1✔
1120
                return
1✔
1121
        }
1✔
1122

1123
        serviceName, err := utils.ParseQueryParmStr(c.Request, "service", false, nil)
2✔
1124
        // inventory service accepts only reindex requests from devicemonitor
2✔
1125
        if err != nil || serviceName != "devicemonitor" {
4✔
1126
                rest.RenderError(c,
2✔
1127
                        http.StatusBadRequest,
2✔
1128
                        errors.New("unsupported service"),
2✔
1129
                )
2✔
1130
                return
2✔
1131
        }
2✔
1132

1133
        // check devicemonitor alerts
1134
        alertsCount, err := i.App.CheckAlerts(ctx, deviceId)
1✔
1135
        if err != nil {
2✔
1136
                rest.RenderError(c,
1✔
1137
                        http.StatusInternalServerError,
1✔
1138
                        errors.New("internal error"),
1✔
1139
                )
1✔
1140
                return
1✔
1141
        }
1✔
1142

1143
        alertsPresent := false
1✔
1144
        if alertsCount > 0 {
2✔
1145
                alertsPresent = true
1✔
1146
        }
1✔
1147
        attrs := model.DeviceAttributes{
1✔
1148
                model.DeviceAttribute{
1✔
1149
                        Name:  model.AttrNameNumberOfAlerts,
1✔
1150
                        Scope: model.AttrScopeMonitor,
1✔
1151
                        Value: alertsCount,
1✔
1152
                },
1✔
1153
                model.DeviceAttribute{
1✔
1154
                        Name:  model.AttrNameAlerts,
1✔
1155
                        Scope: model.AttrScopeMonitor,
1✔
1156
                        Value: alertsPresent,
1✔
1157
                },
1✔
1158
        }
1✔
1159

1✔
1160
        // upsert monitor attributes
1✔
1161
        err = i.App.UpsertAttributes(ctx, model.DeviceID(deviceId), attrs)
1✔
1162
        cause := errors.Cause(err)
1✔
1163
        switch cause {
1✔
UNCOV
1164
        case store.ErrNoAttrName:
×
NEW
UNCOV
1165
                rest.RenderError(c, http.StatusBadRequest, cause)
×
UNCOV
1166
                return
×
1167
        }
1168
        if err != nil {
2✔
1169
                rest.RenderError(c,
1✔
1170
                        http.StatusInternalServerError,
1✔
1171
                        errors.New("internal error"),
1✔
1172
                )
1✔
1173
                return
1✔
1174
        }
1✔
1175

1176
        c.Status(http.StatusOK)
1✔
1177
}
1178

1179
func getIdsFromDevices(devices []model.DeviceUpdate) []model.DeviceID {
1✔
1180
        ids := make([]model.DeviceID, len(devices))
1✔
1181
        for i, dev := range devices {
2✔
1182
                ids[i] = dev.Id
1✔
1183
        }
1✔
1184
        return ids
1✔
1185
}
1186

1187
func parseSearchParams(c *gin.Context) (*model.SearchParams, error) {
2✔
1188
        var searchParams model.SearchParams
2✔
1189

2✔
1190
        if err := c.ShouldBindJSON(&searchParams); err != nil {
4✔
1191
                return nil, errors.Wrap(err, "failed to decode request body")
2✔
1192
        }
2✔
1193

1194
        if searchParams.Page < 1 {
3✔
1195
                searchParams.Page = utils.PageDefault
1✔
1196
        }
1✔
1197
        if searchParams.PerPage < 1 {
3✔
1198
                searchParams.PerPage = utils.PerPageDefault
1✔
1199
        }
1✔
1200

1201
        if err := searchParams.Validate(); err != nil {
3✔
1202
                return nil, err
1✔
1203
        }
1✔
1204

1205
        return &searchParams, nil
2✔
1206
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc