diff --git a/api/v2.0/swagger.yaml b/api/v2.0/swagger.yaml index 606fd4638..75f37686f 100644 --- a/api/v2.0/swagger.yaml +++ b/api/v2.0/swagger.yaml @@ -18,21 +18,52 @@ securityDefinitions: security: - basicAuth: [] paths: - /projects/{project_id}/repositories/{repository_id}/artifacts/{artifact_id}: + /projects/{project_name}/repositories/{repository_name}/artifacts/{digest}: get: - summary: Read artifact by id - description: endpoint returns artifact by id + summary: Get the specific artifact + description: Get the artifact specified by the digest under the project and repository tags: - artifact - operationId: readArtifact + operationId: getArtifact parameters: - $ref: '#/parameters/requestId' - - $ref: '#/parameters/projectId' - - $ref: '#/parameters/repositoryId' - - $ref: '#/parameters/artifactId' + - $ref: '#/parameters/projectName' + - $ref: '#/parameters/repositoryName' + - $ref: '#/parameters/digest' + - name: with_tag + in: query + description: Specify whether the tags are inclued inside the returning artifacts + type: boolean + required: false + default: true + - name: with_label + in: query + description: Specify whether the labels are inclued inside the returning artifacts + type: boolean + required: false + default: false + - name: with_scan_overview + in: query + description: Specify whether the scan overview is inclued inside the returning artifacts + type: boolean + required: false + default: false + # should be in tag level + - name: with_signatrue + in: query + description: Specify whether the signature is inclued inside the returning artifacts + type: boolean + required: false + default: false + - name: with_immutable_status + in: query + description: Specify whether the immutable status is inclued inside the tags of the returning artifacts. Only works when setting "with_tag=true" + type: boolean + required: false + default: false responses: '200': - description: OK + description: Success schema: $ref: '#/definitions/Artifact' '401': @@ -42,59 +73,78 @@ paths: '500': $ref: '#/responses/500' delete: - summary: Delete artifact by id - description: endpoint to delete the artifact by id + summary: Delete the specific artifact + description: Delete the artifact specified by the digest under the project and repository tags: - artifact operationId: deleteArtifact parameters: - $ref: '#/parameters/requestId' - - $ref: '#/parameters/projectId' - - $ref: '#/parameters/repositoryId' - - $ref: '#/parameters/artifactId' + - $ref: '#/parameters/projectName' + - $ref: '#/parameters/repositoryName' + - $ref: '#/parameters/digest' responses: '200': - description: Artifact is deleted successfully. + description: Success '401': $ref: '#/responses/401' '403': $ref: '#/responses/403' '500': $ref: '#/responses/500' - /projects/{project_id}/repositories/{repository_id}/artifacts: + /projects/{project_name}/repositories/{repository_name}/artifacts: get: - summary: List artifacts of the repository - description: endpoint returns all artifacts of the repository. + summary: List artifacts + description: List artifacts under the specific project and repository. tags: - artifact operationId: listArtifacts parameters: - $ref: '#/parameters/requestId' - - $ref: '#/parameters/projectId' - - $ref: '#/parameters/repositoryId' + - $ref: '#/parameters/projectName' + - $ref: '#/parameters/repositoryName' - $ref: '#/parameters/page' - $ref: '#/parameters/pageSize' - - name: label + - name: type in: query - description: Response for artifact include label info when it's true + description: Query the artifacts by type. Valid values can be "IMAGE", "CHART", etc. + type: string + required: false + - name: with_tag + in: query + description: Specify whether the tags are inclued inside the returning artifacts + type: boolean + required: false + default: true + - name: with_label + in: query + description: Specify whether the labels are inclued inside the returning artifacts type: boolean required: false default: false - - name: signature + - name: with_scan_overview in: query - description: Response for artifact include signature info when it's true + description: Specify whether the scan overview is inclued inside the returning artifacts type: boolean required: false default: false - - name: vulnerability + # should be in tag level + - name: with_signatrue in: query - description: Response for artifact include vulnerability info when it's true + description: Specify whether the signature is inclued inside the returning artifacts type: boolean required: false default: false + - name: with_immutable_status + in: query + description: Specify whether the immutable status is inclued inside the tags of the returning artifacts. Only works when setting "with_tag=true" + type: boolean + required: false + default: false + # TODO add other query string: type, .... responses: '200': - description: OK + description: Success headers: X-Total-Count: description: The total count of artifacts @@ -115,70 +165,67 @@ paths: parameters: requestId: name: X-Request-Id - description: A unique id for the request + description: An unique ID for the request in: header type: string required: false minLength: 1 - projectId: - name: project_id + projectName: + name: project_name in: path - description: The id of the project + description: The name of the project required: true - type: integer - format: int64 - repositoryId: - name: repository_id + type: string + repositoryName: + name: repository_name in: path - description: The id of the repository + description: The name of the repository required: true - type: integer - format: int64 - artifactId: - name: artifact_id + type: string + digest: + name: digest in: path - description: The id of the artifact + description: The digest of the artifact required: true - type: integer - format: int64 + type: string page: name: page in: query type: integer - format: int32 + format: int64 required: false - description: 'The page number, default is 1.' + description: The page number default: 1 pageSize: name: page_size in: query type: integer - format: int32 + format: int64 required: false - description: 'The size of per page, default is 10, maximum is 100.' + description: The size of per page default: 10 responses: '401': - description: UnauthorizedError + description: Unauthorized headers: X-Request-Id: - description: The request id this is a response to + description: The ID of the corresponding request for the response type: string schema: $ref: '#/definitions/Error' '403': - description: ForbiddenError + description: Forbidden headers: X-Request-Id: - description: The request id this is a response to + description: The ID of the corresponding request for the response type: string schema: $ref: '#/definitions/Error' '500': - description: InternalServerError + description: Internal server error headers: X-Request-Id: - description: The request id this is a response to + description: The ID of the corresponding request for the response type: string schema: $ref: '#/definitions/Error' @@ -204,40 +251,132 @@ definitions: id: type: integer format: int64 - description: The id of the artifact + description: The ID of the artifact type: type: string - description: The type of the artifact, eg image, chart, etc - repository: - $ref: '#/definitions/Repository' - description: The repository of the artifact - tags: - description: The list of tags that attached to the artifact + description: The type of the artifact, e.g. image, chart, etc media_type: type: string - description: The specific media type for the artifact + description: The media type of the artifact + manifest_media_type: + type: string + description: The manifest media type of the artifact + project_id: + type: integer + format: int64 + description: The ID of the project that the artifact belongs to + repository_id: + type: integer + format: int64 + description: The ID of the repository that the artifact belongs to digest: type: string description: The digest of the artifact size: - type: string + type: integer format: int64 description: The size of the artifact - upload_time: + push_time: type: string format: date-time - description: The upload time for the artifact - labels: - description: The list of labels that attached to the artifact - signature: - description: The signature attached to the artifact - Repository: + description: The push time of the artifact + pull_time: + type: string + format: date-time + description: The latest pull time of the artifact + extra_attrs: + $ref: '#/definitions/ExtraAttrs' + annotations: + $ref: '#/definitions/Annotations' + references: + type: array + items: + $ref: '#/definitions/Reference' + tags: + type: array + items: + $ref: '#/definitions/Tag' + sub_resource_links: + $ref: '#/definitions/SubResourceLinks' + Tag: type: object properties: id: type: integer format: int64 - description: The id of the repository + description: The ID of the tag + repository_id: + type: integer + format: int64 + description: The ID of the repository that the tag belongs to + artifact_id: + type: integer + format: int64 + description: The ID of the artifact that the tag attached to name: type: string - description: The name of repository. + description: The name of the tag + push_time: + type: string + format: date-time + description: The push time of the tag + pull_time: + type: string + format: date-time + description: The latest pull time of the tag + ExtraAttrs: + type: object + additionalProperties: + type: object + Annotations: + type: object + additionalProperties: + type: string + SubResourceLinks: + type: object + additionalProperties: + type: array + items: + $ref: '#/definitions/ResourceLink' + ResourceLink: + type: object + properties: + href: + type: string + description: The link of the resource + absolute: + type: boolean + description: Determine whether the link is an absolute URL or not + Reference: + type: object + properties: + parent_id: + type: integer + format: int64 + description: The parent ID of the reference + child_id: + type: integer + format: int64 + description: The child ID of the reference + platform: + $ref: '#/definitions/Platform' + Platform: + type: object + properties: + architecture: + type: string + description: The architecture that the artifact applys to + os: + type: string + description: The OS that the artifact applys to + os.version: + type: string + description: The version of the OS that the artifact applys to + os.features: + type: array + description: The features of the OS that the artifact applys to + items: + type: string + variant: + type: string + description: The variant of the CPU diff --git a/src/api/artifact/controller.go b/src/api/artifact/controller.go index d93ddcfbe..392cf8a82 100644 --- a/src/api/artifact/controller.go +++ b/src/api/artifact/controller.go @@ -296,7 +296,13 @@ func (c *controller) assembleArtifact(ctx context.Context, art *artifact.Artifac log.Errorf("failed to list tag of artifact %d: %v", artifact.ID, err) } } - // TODO populate other properties: scan, signature etc. + if option.WithLabel { + // TODO populate label + } + if option.WithScanOverview { + // TODO populate scan overview + } + // TODO populate signature on artifact or label level? return artifact } @@ -308,6 +314,9 @@ func (c *controller) assembleTag(ctx context.Context, tag *tm.Tag, option *TagOp if option == nil { return t } - // TODO populate label, signature, immutable status for tag + if option.WithImmutableStatus { + // TODO populate immutable status + } + // TODO populate signature on tag level? return t } diff --git a/src/api/artifact/controller_test.go b/src/api/artifact/controller_test.go index 9d70cb626..8a53dd77a 100644 --- a/src/api/artifact/controller_test.go +++ b/src/api/artifact/controller_test.go @@ -68,7 +68,6 @@ func (c *controllerTestSuite) TestAssembleTag() { PullTime: time.Now(), } option := &TagOption{ - WithLabel: true, WithImmutableStatus: true, } @@ -85,11 +84,12 @@ func (c *controllerTestSuite) TestAssembleArtifact() { option := &Option{ WithTag: true, TagOption: &TagOption{ - WithLabel: false, + WithImmutableStatus: false, }, - WithScanResult: true, - WithSignature: true, + WithLabel: false, + WithScanOverview: true, + WithSignature: true, } tg := &tag.Tag{ ID: 1, @@ -209,9 +209,9 @@ func (c *controllerTestSuite) TestEnsure() { func (c *controllerTestSuite) TestList() { query := &q.Query{} option := &Option{ - WithTag: true, - WithScanResult: true, - WithSignature: true, + WithTag: true, + WithScanOverview: true, + WithSignature: true, } c.artMgr.On("List").Return(1, []*artifact.Artifact{ { diff --git a/src/api/artifact/model.go b/src/api/artifact/model.go index 2a5e829e0..0f457d72f 100644 --- a/src/api/artifact/model.go +++ b/src/api/artifact/model.go @@ -15,8 +15,10 @@ package artifact import ( + "github.com/go-openapi/strfmt" "github.com/goharbor/harbor/src/pkg/artifact" "github.com/goharbor/harbor/src/pkg/tag/model/tag" + "github.com/goharbor/harbor/src/server/v2.0/models" ) // Artifact is the overall view of artifact @@ -28,6 +30,62 @@ type Artifact struct { // TODO add other attrs: signature, scan result, etc } +// ToSwagger converts the artifact to the swagger model +func (a *Artifact) ToSwagger() *models.Artifact { + art := &models.Artifact{ + ID: a.ID, + Type: a.Type, + MediaType: a.MediaType, + ManifestMediaType: a.ManifestMediaType, + ProjectID: a.ProjectID, + RepositoryID: a.RepositoryID, + Digest: a.Digest, + Size: a.Size, + PullTime: strfmt.DateTime(a.PullTime), + PushTime: strfmt.DateTime(a.PushTime), + ExtraAttrs: a.ExtraAttrs, + Annotations: a.Annotations, + } + for _, reference := range a.References { + ref := &models.Reference{ + ChildID: reference.ChildID, + ParentID: reference.ParentID, + } + if reference.Platform != nil { + ref.Platform = &models.Platform{ + Architecture: reference.Platform.Architecture, + Os: reference.Platform.OS, + OsFeatures: reference.Platform.OSFeatures, + OsVersion: reference.Platform.OSVersion, + Variant: reference.Platform.Variant, + } + } + art.References = append(art.References, ref) + } + for _, tag := range a.Tags { + art.Tags = append(art.Tags, &models.Tag{ + ArtifactID: tag.ArtifactID, + ID: tag.ID, + Name: tag.Name, + PullTime: strfmt.DateTime(tag.PullTime), + PushTime: strfmt.DateTime(tag.PushTime), + RepositoryID: tag.RepositoryID, + }) + } + for resource, links := range a.SubResourceLinks { + for _, link := range links { + art.SubResourceLinks[resource] = []models.ResourceLink{} + if link != nil { + art.SubResourceLinks[resource] = append(art.SubResourceLinks[resource], models.ResourceLink{ + Absolute: link.Absolute, + Href: link.HREF, + }) + } + } + } + return art +} + // Tag is the overall view of tag type Tag struct { tag.Tag @@ -48,15 +106,16 @@ type ResourceLink struct { // Option is used to specify the properties returned when listing/getting artifacts type Option struct { - WithTag bool - TagOption *TagOption // only works when WithTag is set to true - WithScanResult bool - WithSignature bool // TODO move it to TagOption? + WithTag bool + TagOption *TagOption // only works when WithTag is set to true + WithLabel bool + WithScanOverview bool + // TODO move it to TagOption? + WithSignature bool } // TagOption is used to specify the properties returned when listing/getting tags type TagOption struct { - WithLabel bool WithImmutableStatus bool } diff --git a/src/pkg/artifact/model.go b/src/pkg/artifact/model.go index 7a5a75d72..65679b37b 100644 --- a/src/pkg/artifact/model.go +++ b/src/pkg/artifact/model.go @@ -111,7 +111,8 @@ func (r *Reference) From(ref *dao.ArtifactReference) { r.ParentID = ref.ParentID r.ChildID = ref.ChildID if len(ref.Platform) > 0 { - if err := json.Unmarshal([]byte(ref.Platform), r); err != nil { + r.Platform = &v1.Platform{} + if err := json.Unmarshal([]byte(ref.Platform), r.Platform); err != nil { log.Errorf("failed to unmarshal the platform of reference: %v", err) } } diff --git a/src/pkg/repository/manager.go b/src/pkg/repository/manager.go index 8cc595f8a..78e8e1455 100644 --- a/src/pkg/repository/manager.go +++ b/src/pkg/repository/manager.go @@ -17,6 +17,7 @@ package repository import ( "context" "github.com/goharbor/harbor/src/common/models" + ierror "github.com/goharbor/harbor/src/internal/error" "github.com/goharbor/harbor/src/pkg/q" "github.com/goharbor/harbor/src/pkg/repository/dao" ) @@ -30,6 +31,8 @@ type Manager interface { List(ctx context.Context, query *q.Query) (total int64, repositories []*models.RepoRecord, err error) // Get the repository specified by ID Get(ctx context.Context, id int64) (repository *models.RepoRecord, err error) + // GetByName gets the repository specified by name + GetByName(ctx context.Context, name string) (repository *models.RepoRecord, err error) // Create a repository Create(ctx context.Context, repository *models.RepoRecord) (id int64, err error) // Delete the repository specified by ID @@ -65,6 +68,22 @@ func (m *manager) Get(ctx context.Context, id int64) (*models.RepoRecord, error) return m.dao.Get(ctx, id) } +func (m *manager) GetByName(ctx context.Context, name string) (repository *models.RepoRecord, err error) { + _, repositories, err := m.List(ctx, &q.Query{ + Keywords: map[string]interface{}{ + "Name": name, + }, + }) + if err != nil { + return nil, err + } + if len(repositories) == 0 { + return nil, ierror.New(nil).WithCode(ierror.NotFoundCode). + WithMessage("repository %s not found", name) + } + return repositories[0], nil +} + func (m *manager) Create(ctx context.Context, repository *models.RepoRecord) (int64, error) { return m.dao.Create(ctx, repository) } diff --git a/src/pkg/repository/manager_test.go b/src/pkg/repository/manager_test.go index afc60e1ba..0ef82a7f6 100644 --- a/src/pkg/repository/manager_test.go +++ b/src/pkg/repository/manager_test.go @@ -95,6 +95,21 @@ func (m *managerTestSuite) TestGet() { m.Equal(repository.RepositoryID, repo.RepositoryID) } +func (m *managerTestSuite) TestGetByName() { + repository := &models.RepoRecord{ + RepositoryID: 1, + ProjectID: 1, + Name: "library/hello-world", + } + m.dao.On("Count", mock.Anything).Return(1, nil) + m.dao.On("List", mock.Anything).Return([]*models.RepoRecord{repository}, nil) + repo, err := m.mgr.GetByName(nil, "library/hello-world") + m.Require().Nil(err) + m.dao.AssertExpectations(m.T()) + m.Require().NotNil(repo) + m.Equal(repository.RepositoryID, repo.RepositoryID) +} + func (m *managerTestSuite) TestCreate() { m.dao.On("Create", mock.Anything).Return(1, nil) id, err := m.mgr.Create(nil, &models.RepoRecord{ diff --git a/src/server/v2.0/handler/artifact.go b/src/server/v2.0/handler/artifact.go index 7476aac09..7153515fc 100644 --- a/src/server/v2.0/handler/artifact.go +++ b/src/server/v2.0/handler/artifact.go @@ -16,32 +16,98 @@ package handler import ( "context" - + "fmt" "github.com/go-openapi/runtime/middleware" + "github.com/goharbor/harbor/src/api/artifact" + "github.com/goharbor/harbor/src/pkg/project" + "github.com/goharbor/harbor/src/pkg/q" + "github.com/goharbor/harbor/src/pkg/repository" + "github.com/goharbor/harbor/src/server/v2.0/models" operation "github.com/goharbor/harbor/src/server/v2.0/restapi/operations/artifact" ) -// ArtifactAPI the api implemention of artifacts -type ArtifactAPI struct { - BaseAPI +func newArtifactAPI() *artifactAPI { + return &artifactAPI{ + artCtl: artifact.Ctl, + proMgr: project.Mgr, + repoMgr: repository.Mgr, + } } -// DeleteArtifact ... -func (api *ArtifactAPI) DeleteArtifact(ctx context.Context, params operation.DeleteArtifactParams) middleware.Responder { +type artifactAPI struct { + BaseAPI + artCtl artifact.Controller + proMgr project.Manager + repoMgr repository.Manager +} + +// TODO do auth in a separate middleware + +func (a *artifactAPI) ListArtifacts(ctx context.Context, params operation.ListArtifactsParams) middleware.Responder { + // set query + query := &q.Query{ + Keywords: map[string]interface{}{}, + } + if params.Type != nil { + query.Keywords["Type"] = *(params.Type) + } + if params.Page != nil { + query.PageNumber = *(params.Page) + } + if params.PageSize != nil { + query.PageSize = *(params.PageSize) + } + repository, err := a.repoMgr.GetByName(ctx, fmt.Sprintf("%s/%s", params.ProjectName, params.RepositoryName)) + if err != nil { + return a.SendError(ctx, err) + } + query.Keywords["RepositoryID"] = repository.RepositoryID + + // set option + option := &artifact.Option{ + WithTag: true, // return the tag by default + } + if params.WithTag != nil { + option.WithTag = *(params.WithTag) + } + if option.WithTag { + if params.WithImmutableStatus != nil { + option.TagOption = &artifact.TagOption{ + WithImmutableStatus: *(params.WithImmutableStatus), + } + } + } + if params.WithLabel != nil { + option.WithLabel = *(params.WithLabel) + } + if params.WithScanOverview != nil { + option.WithScanOverview = *(params.WithScanOverview) + } + if params.WithSignatrue != nil { + option.WithSignature = *(params.WithSignatrue) + } + + // list artifacts according to the query and option + total, arts, err := a.artCtl.List(ctx, query, option) + if err != nil { + return a.SendError(ctx, err) + } + + var artifacts []*models.Artifact + for _, art := range arts { + artifacts = append(artifacts, art.ToSwagger()) + } + + // TODO add link header + return operation.NewListArtifactsOK().WithXTotalCount(total).WithLink("").WithPayload(artifacts) +} + +func (a *artifactAPI) GetArtifact(ctx context.Context, params operation.GetArtifactParams) middleware.Responder { + // TODO implement + return operation.NewGetArtifactOK() +} + +func (a *artifactAPI) DeleteArtifact(ctx context.Context, params operation.DeleteArtifactParams) middleware.Responder { + // TODO implement return operation.NewDeleteArtifactOK() } - -// ListArtifacts ... -func (api *ArtifactAPI) ListArtifacts(ctx context.Context, params operation.ListArtifactsParams) middleware.Responder { - return operation.NewListArtifactsOK() -} - -// ReadArtifact ... -func (api *ArtifactAPI) ReadArtifact(ctx context.Context, params operation.ReadArtifactParams) middleware.Responder { - return operation.NewReadArtifactOK() -} - -// NewArtifactAPI returns API of artifacts -func NewArtifactAPI() *ArtifactAPI { - return &ArtifactAPI{} -} diff --git a/src/server/v2.0/handler/base.go b/src/server/v2.0/handler/base.go index c8f3683ec..773e8baf3 100644 --- a/src/server/v2.0/handler/base.go +++ b/src/server/v2.0/handler/base.go @@ -14,6 +14,8 @@ package handler +// TODO move this file out of v2.0 folder as this is common for all versions of API + import ( "context" diff --git a/src/server/v2.0/handler/handler.go b/src/server/v2.0/handler/handler.go index 0fe57b178..d6f354eee 100644 --- a/src/server/v2.0/handler/handler.go +++ b/src/server/v2.0/handler/handler.go @@ -30,7 +30,7 @@ import ( // New returns http handler for API V2.0 func New() http.Handler { h, api, err := restapi.HandlerAPI(restapi.Config{ - ArtifactAPI: NewArtifactAPI(), + ArtifactAPI: newArtifactAPI(), }) if err != nil { log.Fatal(err) diff --git a/src/testing/artifact_manager.go b/src/testing/artifact_manager.go index 6a07c8973..aeab562ff 100644 --- a/src/testing/artifact_manager.go +++ b/src/testing/artifact_manager.go @@ -30,13 +30,21 @@ type FakeArtifactManager struct { // List ... func (f *FakeArtifactManager) List(ctx context.Context, query *q.Query) (int64, []*artifact.Artifact, error) { args := f.Called() - return int64(args.Int(0)), args.Get(1).([]*artifact.Artifact), args.Error(2) + var artifacts []*artifact.Artifact + if args.Get(1) != nil { + artifacts = args.Get(1).([]*artifact.Artifact) + } + return int64(args.Int(0)), artifacts, args.Error(2) } // Get ... func (f *FakeArtifactManager) Get(ctx context.Context, id int64) (*artifact.Artifact, error) { args := f.Called() - return args.Get(0).(*artifact.Artifact), args.Error(1) + var art *artifact.Artifact + if args.Get(0) != nil { + art = args.Get(0).(*artifact.Artifact) + } + return art, args.Error(1) } // Create ... diff --git a/src/testing/repository_manager.go b/src/testing/repository_manager.go index 160fdc3a3..f6994dd94 100644 --- a/src/testing/repository_manager.go +++ b/src/testing/repository_manager.go @@ -29,13 +29,31 @@ type FakeRepositoryManager struct { // List ... func (f *FakeRepositoryManager) List(ctx context.Context, query *q.Query) (int64, []*models.RepoRecord, error) { args := f.Called() - return int64(args.Int(0)), args.Get(1).([]*models.RepoRecord), args.Error(2) + var repositories []*models.RepoRecord + if args.Get(1) != nil { + repositories = args.Get(1).([]*models.RepoRecord) + } + return int64(args.Int(0)), repositories, args.Error(2) } // Get ... func (f *FakeRepositoryManager) Get(ctx context.Context, id int64) (*models.RepoRecord, error) { args := f.Called() - return args.Get(0).(*models.RepoRecord), args.Error(1) + var repository *models.RepoRecord + if args.Get(0) != nil { + repository = args.Get(0).(*models.RepoRecord) + } + return repository, args.Error(1) +} + +// GetByName ... +func (f *FakeRepositoryManager) GetByName(ctx context.Context, name string) (*models.RepoRecord, error) { + args := f.Called() + var repository *models.RepoRecord + if args.Get(0) != nil { + repository = args.Get(0).(*models.RepoRecord) + } + return repository, args.Error(1) } // Delete ... diff --git a/src/testing/tag_manager.go b/src/testing/tag_manager.go index d09345ca9..7c664eb86 100644 --- a/src/testing/tag_manager.go +++ b/src/testing/tag_manager.go @@ -29,13 +29,21 @@ type FakeTagManager struct { // List ... func (f *FakeTagManager) List(ctx context.Context, query *q.Query) (int64, []*tag.Tag, error) { args := f.Called() - return int64(args.Int(0)), args.Get(1).([]*tag.Tag), args.Error(2) + var tags []*tag.Tag + if args.Get(1) != nil { + tags = args.Get(1).([]*tag.Tag) + } + return int64(args.Int(0)), tags, args.Error(2) } // Get ... func (f *FakeTagManager) Get(ctx context.Context, id int64) (*tag.Tag, error) { args := f.Called() - return args.Get(0).(*tag.Tag), args.Error(1) + var tg *tag.Tag + if args.Get(0) != nil { + tg = args.Get(0).(*tag.Tag) + } + return tg, args.Error(1) } // Create ...