mirror of
https://github.com/goharbor/harbor
synced 2025-04-19 20:44:33 +00:00

Fix the legacy scheduled job issue for GC/scan all Fixes #13968 Signed-off-by: Wenkai Yin <yinw@vmware.com>
544 lines
17 KiB
Go
544 lines
17 KiB
Go
// Copyright Project Harbor Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package scan
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/goharbor/harbor/src/common"
|
|
"github.com/goharbor/harbor/src/common/rbac"
|
|
"github.com/goharbor/harbor/src/controller/artifact"
|
|
"github.com/goharbor/harbor/src/controller/robot"
|
|
"github.com/goharbor/harbor/src/core/config"
|
|
"github.com/goharbor/harbor/src/lib/orm"
|
|
"github.com/goharbor/harbor/src/lib/q"
|
|
"github.com/goharbor/harbor/src/pkg/permission/types"
|
|
"github.com/goharbor/harbor/src/pkg/robot/model"
|
|
sca "github.com/goharbor/harbor/src/pkg/scan"
|
|
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
|
|
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
|
|
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
|
"github.com/goharbor/harbor/src/pkg/scan/vuln"
|
|
"github.com/goharbor/harbor/src/pkg/task"
|
|
artifacttesting "github.com/goharbor/harbor/src/testing/controller/artifact"
|
|
robottesting "github.com/goharbor/harbor/src/testing/controller/robot"
|
|
scannertesting "github.com/goharbor/harbor/src/testing/controller/scanner"
|
|
ormtesting "github.com/goharbor/harbor/src/testing/lib/orm"
|
|
"github.com/goharbor/harbor/src/testing/mock"
|
|
postprocessorstesting "github.com/goharbor/harbor/src/testing/pkg/scan/postprocessors"
|
|
reporttesting "github.com/goharbor/harbor/src/testing/pkg/scan/report"
|
|
tasktesting "github.com/goharbor/harbor/src/testing/pkg/task"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/stretchr/testify/suite"
|
|
)
|
|
|
|
// ControllerTestSuite is the test suite for scan controller.
|
|
type ControllerTestSuite struct {
|
|
suite.Suite
|
|
|
|
artifactCtl *artifacttesting.Controller
|
|
originalArtifactCtl artifact.Controller
|
|
|
|
registration *scanner.Registration
|
|
artifact *artifact.Artifact
|
|
rawReport string
|
|
|
|
execMgr *tasktesting.ExecutionManager
|
|
taskMgr *tasktesting.Manager
|
|
reportMgr *reporttesting.Manager
|
|
ar artifact.Controller
|
|
c Controller
|
|
reportConverter *postprocessorstesting.ScanReportV1ToV2Converter
|
|
}
|
|
|
|
// TestController is the entry point of ControllerTestSuite.
|
|
func TestController(t *testing.T) {
|
|
suite.Run(t, new(ControllerTestSuite))
|
|
}
|
|
|
|
// SetupSuite ...
|
|
func (suite *ControllerTestSuite) SetupSuite() {
|
|
suite.originalArtifactCtl = artifact.Ctl
|
|
suite.artifactCtl = &artifacttesting.Controller{}
|
|
artifact.Ctl = suite.artifactCtl
|
|
|
|
suite.artifact = &artifact.Artifact{}
|
|
suite.artifact.Type = "IMAGE"
|
|
suite.artifact.ProjectID = 1
|
|
suite.artifact.RepositoryName = "library/photon"
|
|
suite.artifact.Digest = "digest-code"
|
|
suite.artifact.ManifestMediaType = v1.MimeTypeDockerArtifact
|
|
|
|
m := &v1.ScannerAdapterMetadata{
|
|
Scanner: &v1.Scanner{
|
|
Name: "Trivy",
|
|
Vendor: "Harbor",
|
|
Version: "0.1.0",
|
|
},
|
|
Capabilities: []*v1.ScannerCapability{{
|
|
ConsumesMimeTypes: []string{
|
|
v1.MimeTypeOCIArtifact,
|
|
v1.MimeTypeDockerArtifact,
|
|
},
|
|
ProducesMimeTypes: []string{
|
|
v1.MimeTypeNativeReport,
|
|
},
|
|
}},
|
|
Properties: v1.ScannerProperties{
|
|
"extra": "testing",
|
|
},
|
|
}
|
|
|
|
suite.registration = &scanner.Registration{
|
|
ID: 1,
|
|
UUID: "uuid001",
|
|
Name: "Test-scan-controller",
|
|
URL: "http://testing.com:3128",
|
|
IsDefault: true,
|
|
Metadata: m,
|
|
}
|
|
|
|
sc := &scannertesting.Controller{}
|
|
sc.On("GetRegistrationByProject", mock.Anything, suite.artifact.ProjectID).Return(suite.registration, nil)
|
|
sc.On("Ping", suite.registration).Return(m, nil)
|
|
|
|
mgr := &reporttesting.Manager{}
|
|
mgr.On("Create", mock.Anything, &scan.Report{
|
|
Digest: "digest-code",
|
|
RegistrationUUID: "uuid001",
|
|
MimeType: "application/vnd.scanner.adapter.vuln.report.harbor+json; version=1.0",
|
|
}).Return("r-uuid", nil)
|
|
|
|
rp := vuln.Report{
|
|
GeneratedAt: time.Now().UTC().String(),
|
|
Scanner: &v1.Scanner{
|
|
Name: "Trivy",
|
|
Vendor: "Harbor",
|
|
Version: "0.1.0",
|
|
},
|
|
Severity: vuln.High,
|
|
Vulnerabilities: []*vuln.VulnerabilityItem{
|
|
{
|
|
ID: "2019-0980-0909",
|
|
Package: "dpkg",
|
|
Version: "0.9.1",
|
|
FixVersion: "0.9.2",
|
|
Severity: vuln.High,
|
|
Description: "mock one",
|
|
Links: []string{"https://vuln.com"},
|
|
},
|
|
},
|
|
}
|
|
|
|
jsonData, err := json.Marshal(rp)
|
|
require.NoError(suite.T(), err)
|
|
suite.rawReport = string(jsonData)
|
|
|
|
reports := []*scan.Report{
|
|
{
|
|
ID: 11,
|
|
UUID: "rp-uuid-001",
|
|
Digest: "digest-code",
|
|
RegistrationUUID: "uuid001",
|
|
MimeType: "application/vnd.scanner.adapter.vuln.report.harbor+json; version=1.0",
|
|
Status: "Success",
|
|
Report: suite.rawReport,
|
|
StartTime: time.Now(),
|
|
EndTime: time.Now().Add(2 * time.Second),
|
|
},
|
|
}
|
|
|
|
mgr.On("GetBy", mock.Anything, suite.artifact.Digest, suite.registration.UUID, []string{v1.MimeTypeNativeReport}).Return(reports, nil)
|
|
mgr.On("Get", mock.Anything, "rp-uuid-001").Return(reports[0], nil)
|
|
mgr.On("UpdateReportData", "rp-uuid-001", suite.rawReport, (int64)(10000)).Return(nil)
|
|
mgr.On("UpdateStatus", "the-uuid-123", "Success", (int64)(10000)).Return(nil)
|
|
suite.reportMgr = mgr
|
|
|
|
rc := &robottesting.Controller{}
|
|
|
|
rname := fmt.Sprintf("%s-%s", suite.registration.Name, "the-uuid-123")
|
|
|
|
conf := map[string]interface{}{
|
|
common.RobotTokenDuration: "30",
|
|
}
|
|
config.InitWithSettings(conf)
|
|
|
|
account := &robot.Robot{
|
|
Robot: model.Robot{
|
|
Name: rname,
|
|
Description: "for scan",
|
|
ProjectID: suite.artifact.ProjectID,
|
|
},
|
|
Level: robot.LEVELPROJECT,
|
|
Permissions: []*robot.Permission{
|
|
{
|
|
Kind: "project",
|
|
Namespace: "library",
|
|
Access: []*types.Policy{
|
|
{
|
|
Resource: "repository",
|
|
Action: rbac.ActionPull,
|
|
},
|
|
{
|
|
Resource: "repository",
|
|
Action: rbac.ActionScannerPull,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
rc.On("Create", mock.Anything, account).Return(int64(1), "robot-account", nil)
|
|
rc.On("Get", mock.Anything, int64(1), &robot.Option{
|
|
WithPermission: false,
|
|
}).Return(&robot.Robot{
|
|
Robot: model.Robot{
|
|
ID: 1,
|
|
Name: rname,
|
|
Secret: "robot-account",
|
|
Description: "for scan",
|
|
ProjectID: suite.artifact.ProjectID,
|
|
},
|
|
Level: "project",
|
|
}, nil)
|
|
|
|
// Set job parameters
|
|
req := &v1.ScanRequest{
|
|
Registry: &v1.Registry{
|
|
URL: "https://core.com",
|
|
},
|
|
Artifact: &v1.Artifact{
|
|
NamespaceID: suite.artifact.ProjectID,
|
|
Digest: suite.artifact.Digest,
|
|
Repository: suite.artifact.RepositoryName,
|
|
MimeType: suite.artifact.ManifestMediaType,
|
|
},
|
|
}
|
|
|
|
rJSON, err := req.ToJSON()
|
|
require.NoError(suite.T(), err)
|
|
|
|
regJSON, err := suite.registration.ToJSON()
|
|
require.NoError(suite.T(), err)
|
|
|
|
id, _, _ := rc.Create(context.TODO(), account)
|
|
rb, _ := rc.Get(context.TODO(), id, &robot.Option{WithPermission: false})
|
|
robotJSON, err := rb.ToJSON()
|
|
require.NoError(suite.T(), err)
|
|
|
|
params := make(map[string]interface{})
|
|
params[sca.JobParamRegistration] = regJSON
|
|
params[sca.JobParameterRequest] = rJSON
|
|
params[sca.JobParameterMimes] = []string{v1.MimeTypeNativeReport}
|
|
params[sca.JobParameterAuthType] = "Basic"
|
|
params[sca.JobParameterRobot] = robotJSON
|
|
|
|
suite.ar = &artifacttesting.Controller{}
|
|
|
|
suite.execMgr = &tasktesting.ExecutionManager{}
|
|
|
|
suite.taskMgr = &tasktesting.Manager{}
|
|
|
|
suite.c = &basicController{
|
|
manager: mgr,
|
|
ar: suite.ar,
|
|
sc: sc,
|
|
rc: rc,
|
|
uuid: func() (string, error) {
|
|
return "the-uuid-123", nil
|
|
},
|
|
config: func(cfg string) (string, error) {
|
|
switch cfg {
|
|
case configRegistryEndpoint:
|
|
return "https://core.com", nil
|
|
case configCoreInternalAddr:
|
|
return "http://core:8080", nil
|
|
}
|
|
|
|
return "", nil
|
|
},
|
|
|
|
cloneCtx: func(ctx context.Context) context.Context { return ctx },
|
|
makeCtx: func() context.Context { return context.TODO() },
|
|
|
|
execMgr: suite.execMgr,
|
|
taskMgr: suite.taskMgr,
|
|
reportConverter: &postprocessorstesting.ScanReportV1ToV2Converter{},
|
|
}
|
|
}
|
|
|
|
// TearDownSuite ...
|
|
func (suite *ControllerTestSuite) TearDownSuite() {
|
|
artifact.Ctl = suite.originalArtifactCtl
|
|
}
|
|
|
|
// TestScanControllerScan ...
|
|
func (suite *ControllerTestSuite) TestScanControllerScan() {
|
|
{
|
|
// artifact not provieded
|
|
suite.Require().Error(suite.c.Scan(context.TODO(), nil))
|
|
}
|
|
|
|
{
|
|
// success
|
|
mock.OnAnything(suite.ar, "Walk").Return(nil).Run(func(args mock.Arguments) {
|
|
walkFn := args.Get(2).(func(*artifact.Artifact) error)
|
|
walkFn(suite.artifact)
|
|
}).Once()
|
|
|
|
mock.OnAnything(suite.taskMgr, "List").Return([]*task.Task{
|
|
{ExtraAttrs: suite.makeExtraAttrs("rp-uuid-001"), Status: "Success"},
|
|
}, nil).Once()
|
|
|
|
mock.OnAnything(suite.reportMgr, "Delete").Return(nil).Once()
|
|
|
|
mock.OnAnything(suite.execMgr, "Create").Return(int64(1), nil).Once()
|
|
mock.OnAnything(suite.taskMgr, "Create").Return(int64(1), nil).Once()
|
|
|
|
ctx := orm.NewContext(nil, &ormtesting.FakeOrmer{})
|
|
|
|
suite.Require().NoError(suite.c.Scan(ctx, suite.artifact))
|
|
}
|
|
|
|
{
|
|
// delete old report failed
|
|
mock.OnAnything(suite.ar, "Walk").Return(nil).Run(func(args mock.Arguments) {
|
|
walkFn := args.Get(2).(func(*artifact.Artifact) error)
|
|
walkFn(suite.artifact)
|
|
}).Once()
|
|
|
|
mock.OnAnything(suite.taskMgr, "List").Return([]*task.Task{
|
|
{ExtraAttrs: suite.makeExtraAttrs("rp-uuid-001"), Status: "Success"},
|
|
}, nil).Once()
|
|
|
|
mock.OnAnything(suite.reportMgr, "Delete").Return(fmt.Errorf("delete failed")).Once()
|
|
|
|
suite.Require().Error(suite.c.Scan(context.TODO(), suite.artifact))
|
|
}
|
|
|
|
{
|
|
// a previous scan process is ongoing
|
|
mock.OnAnything(suite.ar, "Walk").Return(nil).Run(func(args mock.Arguments) {
|
|
walkFn := args.Get(2).(func(*artifact.Artifact) error)
|
|
walkFn(suite.artifact)
|
|
}).Once()
|
|
|
|
mock.OnAnything(suite.taskMgr, "List").Return([]*task.Task{
|
|
{ExtraAttrs: suite.makeExtraAttrs("rp-uuid-001"), Status: "Running"},
|
|
}, nil).Once()
|
|
|
|
suite.Require().Error(suite.c.Scan(context.TODO(), suite.artifact))
|
|
}
|
|
}
|
|
|
|
// TestScanControllerGetReport ...
|
|
func (suite *ControllerTestSuite) TestScanControllerGetReport() {
|
|
mock.OnAnything(suite.ar, "Walk").Return(nil).Run(func(args mock.Arguments) {
|
|
walkFn := args.Get(2).(func(*artifact.Artifact) error)
|
|
walkFn(suite.artifact)
|
|
}).Once()
|
|
|
|
mock.OnAnything(suite.taskMgr, "List").Return([]*task.Task{
|
|
{ExtraAttrs: suite.makeExtraAttrs("rp-uuid-001")},
|
|
}, nil).Once()
|
|
|
|
rep, err := suite.c.GetReport(context.TODO(), suite.artifact, []string{v1.MimeTypeNativeReport})
|
|
require.NoError(suite.T(), err)
|
|
assert.Equal(suite.T(), 1, len(rep))
|
|
}
|
|
|
|
// TestScanControllerGetSummary ...
|
|
func (suite *ControllerTestSuite) TestScanControllerGetSummary() {
|
|
mock.OnAnything(suite.ar, "Walk").Return(nil).Run(func(args mock.Arguments) {
|
|
walkFn := args.Get(2).(func(*artifact.Artifact) error)
|
|
walkFn(suite.artifact)
|
|
}).Once()
|
|
mock.OnAnything(suite.taskMgr, "List").Return(nil, nil).Once()
|
|
|
|
sum, err := suite.c.GetSummary(context.TODO(), suite.artifact, []string{v1.MimeTypeNativeReport})
|
|
require.NoError(suite.T(), err)
|
|
assert.Equal(suite.T(), 1, len(sum))
|
|
}
|
|
|
|
// TestScanControllerGetScanLog ...
|
|
func (suite *ControllerTestSuite) TestScanControllerGetScanLog() {
|
|
mock.OnAnything(suite.taskMgr, "List").Return([]*task.Task{
|
|
{
|
|
ID: 1,
|
|
ExtraAttrs: suite.makeExtraAttrs("rp-uuid-001"),
|
|
},
|
|
}, nil).Once()
|
|
|
|
mock.OnAnything(suite.taskMgr, "GetLog").Return([]byte("log"), nil).Once()
|
|
|
|
bytes, err := suite.c.GetScanLog(context.TODO(), "rp-uuid-001")
|
|
require.NoError(suite.T(), err)
|
|
assert.Condition(suite.T(), func() (success bool) {
|
|
success = len(bytes) > 0
|
|
return
|
|
})
|
|
}
|
|
|
|
func (suite *ControllerTestSuite) TestScanControllerGetMultiScanLog() {
|
|
kw1 := q.KeyWords{"extra_attrs.report:rp-uuid-001": "1"}
|
|
suite.taskMgr.On("List", context.TODO(), q.New(kw1)).Return([]*task.Task{
|
|
{
|
|
ID: 1,
|
|
ExtraAttrs: suite.makeExtraAttrs("rp-uuid-001"),
|
|
},
|
|
}, nil).Times(4)
|
|
|
|
kw2 := q.KeyWords{"extra_attrs.report:rp-uuid-002": "1"}
|
|
suite.taskMgr.On("List", context.TODO(), q.New(kw2)).Return([]*task.Task{
|
|
{
|
|
ID: 2,
|
|
ExtraAttrs: suite.makeExtraAttrs("rp-uuid-002"),
|
|
},
|
|
}, nil).Times(4)
|
|
|
|
{
|
|
// Both success
|
|
mock.OnAnything(suite.taskMgr, "GetLog").Return([]byte("log"), nil).Twice()
|
|
|
|
bytes, err := suite.c.GetScanLog(context.TODO(), base64.StdEncoding.EncodeToString([]byte("rp-uuid-001|rp-uuid-002")))
|
|
suite.Nil(err)
|
|
suite.NotEmpty(bytes)
|
|
suite.Contains(string(bytes), "Logs of report rp-uuid-001")
|
|
suite.Contains(string(bytes), "Logs of report rp-uuid-002")
|
|
}
|
|
|
|
{
|
|
// One successfully, one failed
|
|
suite.taskMgr.On("GetLog", context.TODO(), int64(1)).Return([]byte("log"), nil).Once()
|
|
suite.taskMgr.On("GetLog", context.TODO(), int64(2)).Return(nil, fmt.Errorf("failed")).Once()
|
|
|
|
bytes, err := suite.c.GetScanLog(context.TODO(), base64.StdEncoding.EncodeToString([]byte("rp-uuid-001|rp-uuid-002")))
|
|
suite.Nil(err)
|
|
suite.NotEmpty(bytes)
|
|
suite.NotContains(string(bytes), "Logs of report rp-uuid-001")
|
|
}
|
|
|
|
{
|
|
// Both failed
|
|
mock.OnAnything(suite.taskMgr, "GetLog").Return(nil, fmt.Errorf("failed")).Twice()
|
|
|
|
bytes, err := suite.c.GetScanLog(context.TODO(), base64.StdEncoding.EncodeToString([]byte("rp-uuid-001|rp-uuid-002")))
|
|
suite.Error(err)
|
|
suite.Empty(bytes)
|
|
}
|
|
|
|
{
|
|
// Both empty
|
|
mock.OnAnything(suite.taskMgr, "GetLog").Return(nil, nil).Twice()
|
|
|
|
bytes, err := suite.c.GetScanLog(context.TODO(), base64.StdEncoding.EncodeToString([]byte("rp-uuid-001|rp-uuid-002")))
|
|
suite.Nil(err)
|
|
suite.Empty(bytes)
|
|
}
|
|
}
|
|
|
|
func (suite *ControllerTestSuite) TestUpdateReport() {
|
|
{
|
|
// get report failed
|
|
suite.reportMgr.On("GetBy", context.TODO(), "digest", "ruuid", []string{"mime"}).Return(nil, fmt.Errorf("failed")).Once()
|
|
report := &sca.CheckInReport{Digest: "digest", RegistrationUUID: "ruuid", MimeType: "mime"}
|
|
suite.Error(suite.c.UpdateReport(context.TODO(), report))
|
|
}
|
|
|
|
{
|
|
// report not found
|
|
suite.reportMgr.On("GetBy", context.TODO(), "digest", "ruuid", []string{"mime"}).Return(nil, nil).Once()
|
|
report := &sca.CheckInReport{Digest: "digest", RegistrationUUID: "ruuid", MimeType: "mime"}
|
|
suite.Error(suite.c.UpdateReport(context.TODO(), report))
|
|
}
|
|
}
|
|
|
|
func (suite *ControllerTestSuite) TestScanAll() {
|
|
{
|
|
// no artifacts found when scan all
|
|
ctx := context.TODO()
|
|
|
|
executionID := int64(1)
|
|
|
|
suite.execMgr.On(
|
|
"Create", ctx, "SCAN_ALL", int64(0), "SCHEDULE",
|
|
).Return(executionID, nil).Once()
|
|
|
|
mock.OnAnything(suite.artifactCtl, "List").Return([]*artifact.Artifact{}, nil).Once()
|
|
|
|
suite.taskMgr.On("Count", ctx, q.New(q.KeyWords{"execution_id": executionID})).Return(int64(0), nil).Once()
|
|
|
|
mock.OnAnything(suite.execMgr, "UpdateExtraAttrs").Return(nil).Once()
|
|
|
|
suite.execMgr.On("MarkDone", ctx, executionID, mock.Anything).Return(nil).Once()
|
|
|
|
_, err := suite.c.ScanAll(ctx, "SCHEDULE", false)
|
|
suite.NoError(err)
|
|
}
|
|
|
|
{
|
|
// artifacts found, but scan it failed when scan all
|
|
ctx := orm.NewContext(nil, &ormtesting.FakeOrmer{})
|
|
|
|
executionID := int64(1)
|
|
|
|
suite.execMgr.On(
|
|
"Create", ctx, "SCAN_ALL", int64(0), "SCHEDULE",
|
|
).Return(executionID, nil).Once()
|
|
|
|
mock.OnAnything(suite.artifactCtl, "List").Return([]*artifact.Artifact{suite.artifact}, nil).Once()
|
|
mock.OnAnything(suite.ar, "Walk").Return(nil).Run(func(args mock.Arguments) {
|
|
walkFn := args.Get(2).(func(*artifact.Artifact) error)
|
|
walkFn(suite.artifact)
|
|
}).Once()
|
|
|
|
mock.OnAnything(suite.taskMgr, "List").Return(nil, nil).Once()
|
|
|
|
mock.OnAnything(suite.reportMgr, "Delete").Return(nil).Once()
|
|
mock.OnAnything(suite.reportMgr, "Create").Return("uuid", nil).Once()
|
|
mock.OnAnything(suite.taskMgr, "Create").Return(int64(0), fmt.Errorf("failed")).Once()
|
|
mock.OnAnything(suite.execMgr, "UpdateExtraAttrs").Return(nil).Once()
|
|
suite.execMgr.On("MarkError", ctx, executionID, mock.Anything).Return(nil).Once()
|
|
|
|
_, err := suite.c.ScanAll(ctx, "SCHEDULE", false)
|
|
suite.NoError(err)
|
|
}
|
|
}
|
|
|
|
func (suite *ControllerTestSuite) TestDeleteReports() {
|
|
suite.reportMgr.On("DeleteByDigests", context.TODO(), "digest").Return(nil).Once()
|
|
|
|
suite.NoError(suite.c.DeleteReports(context.TODO(), "digest"))
|
|
|
|
suite.reportMgr.On("DeleteByDigests", context.TODO(), "digest").Return(fmt.Errorf("failed")).Once()
|
|
|
|
suite.Error(suite.c.DeleteReports(context.TODO(), "digest"))
|
|
}
|
|
|
|
func (suite *ControllerTestSuite) makeExtraAttrs(reportUUIDs ...string) map[string]interface{} {
|
|
b, _ := json.Marshal(map[string]interface{}{reportUUIDsKey: reportUUIDs})
|
|
|
|
extraAttrs := map[string]interface{}{}
|
|
json.Unmarshal(b, &extraAttrs)
|
|
|
|
return extraAttrs
|
|
}
|