Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api/user): add endpoint for viewing builds sent by current user #989

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
243 changes: 243 additions & 0 deletions api/build/list_sender.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
// SPDX-License-Identifier: Apache-2.0

package build

import (
"fmt"
"net/http"
"strconv"
"time"

"github.com/gin-gonic/gin"
"github.com/go-vela/server/api"
"github.com/go-vela/server/database"
"github.com/go-vela/server/router/middleware/user"
"github.com/go-vela/server/util"
"github.com/go-vela/types/constants"
"github.com/go-vela/types/library"
"github.com/sirupsen/logrus"
)

// swagger:operation GET /api/v1/user/builds builds ListBuildsForSender
//
// Get builds from the configured backend
//
// ---
// produces:
// - application/json
// parameters:
// - in: query
// name: event
// description: Filter by build event
// type: string
// enum:
// - comment
// - deployment
// - pull_request
// - push
// - schedule
// - tag
// - in: query
// name: commit
// description: Filter builds based on the commit hash
// type: string
// - in: query
// name: branch
// description: Filter builds by branch
// type: string
// - in: query
// name: status
// description: Filter by build status
// type: string
// enum:
// - canceled
// - error
// - failure
// - killed
// - pending
// - running
// - success
// - in: query
// name: page
// description: The page of results to retrieve
// type: integer
// default: 1
// - in: query
// name: per_page
// description: How many results per page to return
// type: integer
// maximum: 100
// default: 10
// - in: query
// name: before
// description: filter builds created before a certain time
// type: integer
// default: 1
// - in: query
// name: after
// description: filter builds created after a certain time
// type: integer
// default: 0
// security:
// - ApiKeyAuth: []
// responses:
// '200':
// description: Successfully retrieved the builds
// schema:
// type: array
// items:
// "$ref": "#/definitions/Build"
// headers:
// X-Total-Count:
// description: Total number of results
// type: integer
// Link:
// description: see https://tools.ietf.org/html/rfc5988
// type: string
// '400':
// description: Unable to retrieve the list of builds
// schema:
// "$ref": "#/definitions/Error"
// '500':
// description: Unable to retrieve the list of builds
// schema:
// "$ref": "#/definitions/Error"

// ListBuildsForSender represents the API handler to capture a
// list of builds for a sender from the configured backend.
func ListBuildsForSender(c *gin.Context) {
// variables that will hold the build list, build list filters and total count
var (
filters = map[string]interface{}{}
b []*library.Build
t int64
)

// capture middleware values
u := user.Retrieve(c)
ctx := c.Request.Context()

// update engine logger with API metadata
//
// https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields
logrus.WithFields(logrus.Fields{
"user": u.GetName(),
}).Infof("listing builds for sender %s", u.GetName())

// capture the branch name parameter
branch := c.Query("branch")
// capture the event type parameter
event := c.Query("event")
// capture the status type parameter
status := c.Query("status")
// capture the commit hash parameter
commit := c.Query("commit")

// check if branch filter was provided
if len(branch) > 0 {
// add branch to filters map
filters["branch"] = branch
}
// check if event filter was provided
if len(event) > 0 {
// verify the event provided is a valid event type
if event != constants.EventComment && event != constants.EventDeploy &&
event != constants.EventPush && event != constants.EventPull &&
event != constants.EventTag && event != constants.EventSchedule {
retErr := fmt.Errorf("unable to process event %s: invalid event type provided", event)

util.HandleError(c, http.StatusBadRequest, retErr)

return
}

// add event to filters map
filters["event"] = event
}
// check if status filter was provided
if len(status) > 0 {
// verify the status provided is a valid status type
if status != constants.StatusCanceled && status != constants.StatusError &&
status != constants.StatusFailure && status != constants.StatusKilled &&
status != constants.StatusPending && status != constants.StatusRunning &&
status != constants.StatusSuccess {
retErr := fmt.Errorf("unable to process status %s: invalid status type provided", status)

util.HandleError(c, http.StatusBadRequest, retErr)

return
}

// add status to filters map
filters["status"] = status
}

// check if commit hash filter was provided
if len(commit) > 0 {
// add commit to filters map
filters["commit"] = commit
}

// capture page query parameter if present
page, err := strconv.Atoi(c.DefaultQuery("page", "1"))
if err != nil {
retErr := fmt.Errorf("unable to convert page query parameter for sender %s: %w", u.GetName(), err)

util.HandleError(c, http.StatusBadRequest, retErr)

return
}

// capture per_page query parameter if present
perPage, err := strconv.Atoi(c.DefaultQuery("per_page", "10"))
if err != nil {
retErr := fmt.Errorf("unable to convert per_page query parameter for sender %s: %w", u.GetName(), err)

util.HandleError(c, http.StatusBadRequest, retErr)

return
}

// ensure per_page isn't above or below allowed values
perPage = util.MaxInt(1, util.MinInt(100, perPage))

// capture before query parameter if present, default to now
before, err := strconv.ParseInt(c.DefaultQuery("before", strconv.FormatInt(time.Now().UTC().Unix(), 10)), 10, 64)
if err != nil {
retErr := fmt.Errorf("unable to convert before query parameter for sender %s: %w", u.GetName(), err)

util.HandleError(c, http.StatusBadRequest, retErr)

return
}

// capture after query parameter if present, default to 0
after, err := strconv.ParseInt(c.DefaultQuery("after", "0"), 10, 64)
if err != nil {
retErr := fmt.Errorf("unable to convert after query parameter for sender %s: %w", u.GetName(), err)

util.HandleError(c, http.StatusBadRequest, retErr)

return
}

b, t, err = database.FromContext(c).ListBuildsForSender(ctx, u.GetName(), filters, before, after, page, perPage)
if err != nil {
retErr := fmt.Errorf("unable to list builds for sender %s: %w", u.GetName(), err)

util.HandleError(c, http.StatusInternalServerError, retErr)

return
}

// create pagination object
pagination := api.Pagination{
Page: page,
PerPage: perPage,
Total: t,
}
// set pagination headers
pagination.SetHeaderLink(c)

c.JSON(http.StatusOK, b)
}
2 changes: 2 additions & 0 deletions database/build/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func TestBuild_New(t *testing.T) {
_mock.ExpectExec(CreateRepoIDIndex).WillReturnResult(sqlmock.NewResult(1, 1))
_mock.ExpectExec(CreateSourceIndex).WillReturnResult(sqlmock.NewResult(1, 1))
_mock.ExpectExec(CreateStatusIndex).WillReturnResult(sqlmock.NewResult(1, 1))
_mock.ExpectExec(CreateSenderIndex).WillReturnResult(sqlmock.NewResult(1, 1))

_config := &gorm.Config{SkipDefaultTransaction: true}

Expand Down Expand Up @@ -129,6 +130,7 @@ func testPostgres(t *testing.T) (*engine, sqlmock.Sqlmock) {
_mock.ExpectExec(CreateRepoIDIndex).WillReturnResult(sqlmock.NewResult(1, 1))
_mock.ExpectExec(CreateSourceIndex).WillReturnResult(sqlmock.NewResult(1, 1))
_mock.ExpectExec(CreateStatusIndex).WillReturnResult(sqlmock.NewResult(1, 1))
_mock.ExpectExec(CreateSenderIndex).WillReturnResult(sqlmock.NewResult(1, 1))

// create the new mock Postgres database client
//
Expand Down
30 changes: 30 additions & 0 deletions database/build/count_sender.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// SPDX-License-Identifier: Apache-2.0

package build

import (
"context"

"github.com/go-vela/types/constants"
"github.com/sirupsen/logrus"
)

// CountBuildsForSender gets the count of builds by sender from the database.
func (e *engine) CountBuildsForSender(ctx context.Context, sender string, filters map[string]interface{}) (int64, error) {
e.logger.WithFields(logrus.Fields{
"sender": sender,
}).Tracef("getting count of builds for sender %s from the database", sender)

// variable to store query results
var b int64

// send query to the database and store result in variable
err := e.client.
Table(constants.TableBuild).
Where("sender = ?", sender).
Where(filters).
Count(&b).
Error

return b, err
}
96 changes: 96 additions & 0 deletions database/build/count_sender_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// SPDX-License-Identifier: Apache-2.0

package build

import (
"context"
"reflect"
"testing"

"github.com/DATA-DOG/go-sqlmock"
)

func TestBuild_Engine_CountBuildsForSender(t *testing.T) {
// setup types
_buildOne := testBuild()
_buildOne.SetID(1)
_buildOne.SetRepoID(1)
_buildOne.SetNumber(1)
_buildOne.SetDeployPayload(nil)
_buildOne.SetSender("octocat")

_buildTwo := testBuild()
_buildTwo.SetID(2)
_buildTwo.SetRepoID(1)
_buildTwo.SetNumber(2)
_buildTwo.SetDeployPayload(nil)
_buildTwo.SetSender("octokitty")

_postgres, _mock := testPostgres(t)
defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }()

// create expected result in mock
_rows := sqlmock.NewRows([]string{"count"}).AddRow(1)

// ensure the mock expects the query
_mock.ExpectQuery(`SELECT count(*) FROM "builds" WHERE sender = $1`).WithArgs("octocat").WillReturnRows(_rows)

_sqlite := testSqlite(t)
defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }()

_, err := _sqlite.CreateBuild(context.TODO(), _buildOne)
if err != nil {
t.Errorf("unable to create test build for sqlite: %v", err)
}

_, err = _sqlite.CreateBuild(context.TODO(), _buildTwo)
if err != nil {
t.Errorf("unable to create test build for sqlite: %v", err)
}

// setup tests
tests := []struct {
failure bool
name string
database *engine
want int64
}{
{
failure: false,
name: "postgres",
database: _postgres,
want: 1,
},
{
failure: false,
name: "sqlite3",
database: _sqlite,
want: 1,
},
}

filters := map[string]interface{}{}

// run tests
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := test.database.CountBuildsForSender(context.TODO(), "octocat", filters)

if test.failure {
if err == nil {
t.Errorf("CountBuildsForSender for %s should have returned err", test.name)
}

return
}

if err != nil {
t.Errorf("CountBuildsForSender for %s returned err: %v", test.name, err)
}

if !reflect.DeepEqual(got, test.want) {
t.Errorf("CountBuildsForSender for %s is %v, want %v", test.name, got, test.want)
}
})
}
}
Loading