Skip to content

Commit

Permalink
feat(api/user): add endpoint for viewing builds sent by current user
Browse files Browse the repository at this point in the history
  • Loading branch information
ecrupper committed Oct 17, 2023
1 parent d90fdb7 commit 6c320fd
Show file tree
Hide file tree
Showing 12 changed files with 608 additions and 1 deletion.
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

0 comments on commit 6c320fd

Please sign in to comment.