From 64152bfc08e22886c3333bd941218b4d57cef0a5 Mon Sep 17 00:00:00 2001 From: simon987 Date: Tue, 29 Jan 2019 18:16:40 -0500 Subject: [PATCH] added optional task unique field --- api/git.go | 3 +- api/main.go | 16 ++ api/project.go | 72 ++++++- api/task.go | 77 +++++-- api/worker.go | 83 +++++++- main/main.go | 3 + schema.sql | 8 +- storage/project.go | 38 ++-- storage/task.go | 21 +- storage/worker.go | 35 +++- test/api_git_test.go | 2 +- test/api_index_test.go | 2 +- test/api_log_test.go | 18 +- test/api_project_test.go | 146 ++++++++++++- test/api_task_bench_test.go | 4 +- test/api_task_test.go | 195 +++++++++++++++--- test/api_worker_test.go | 77 +++++-- test/common.go | 43 +++- test/schema.sql | 8 +- web/angular/src/app/api.service.ts | 9 + web/angular/src/app/app.component.html | 2 +- web/angular/src/app/app.module.ts | 13 +- .../create-project.component.html | 7 +- .../create-project.component.ts | 20 +- web/angular/src/app/messenger.service.ts | 22 ++ .../src/app/messenger/messenger-snack-bar.css | 0 .../app/messenger/messenger-snack-bar.html | 0 web/angular/src/app/messenger/messenger.ts | 6 + .../src/app/messenger/snack-bar.component.ts | 32 +++ web/angular/src/app/models/project.ts | 1 + .../project-dashboard.component.html | 2 + .../project-dashboard.component.ts | 24 ++- .../project-list/project-list.component.html | 1 + .../update-project.component.html | 18 +- .../update-project.component.ts | 25 ++- 35 files changed, 877 insertions(+), 156 deletions(-) create mode 100644 web/angular/src/app/messenger.service.ts create mode 100644 web/angular/src/app/messenger/messenger-snack-bar.css create mode 100644 web/angular/src/app/messenger/messenger-snack-bar.html create mode 100644 web/angular/src/app/messenger/messenger.ts create mode 100644 web/angular/src/app/messenger/snack-bar.component.ts diff --git a/api/git.go b/api/git.go index 64e1ce3..d74d6ad 100644 --- a/api/git.go +++ b/api/git.go @@ -72,7 +72,8 @@ func (api *WebAPI) ReceiveGitWebHook(r *Request) { version := getVersion(payload) project.Version = version - api.Database.UpdateProject(project) + err := api.Database.UpdateProject(project) + handleErr(err, r) } func signatureValid(r *Request) (matches bool) { diff --git a/api/main.go b/api/main.go index 4b6f6fe..edfc8a8 100644 --- a/api/main.go +++ b/api/main.go @@ -1,6 +1,7 @@ package api import ( + "fmt" "github.com/Sirupsen/logrus" "github.com/buaazp/fasthttprouter" "github.com/valyala/fasthttp" @@ -48,6 +49,7 @@ func New() *WebAPI { api.router.POST("/log/error", LogRequestMiddleware(LogError)) api.router.POST("/worker/create", LogRequestMiddleware(api.WorkerCreate)) + api.router.POST("/worker/update", LogRequestMiddleware(api.WorkerUpdate)) api.router.GET("/worker/get/:id", LogRequestMiddleware(api.WorkerGet)) api.router.POST("/access/grant", LogRequestMiddleware(api.WorkerGrantAccess)) @@ -55,6 +57,7 @@ func New() *WebAPI { api.router.POST("/project/create", LogRequestMiddleware(api.ProjectCreate)) api.router.GET("/project/get/:id", LogRequestMiddleware(api.ProjectGet)) + api.router.POST("/project/update/:id", LogRequestMiddleware(api.ProjectUpdate)) api.router.GET("/project/stats/:id", LogRequestMiddleware(api.ProjectGetStats)) api.router.GET("/project/stats", LogRequestMiddleware(api.ProjectGetAllStats)) @@ -67,6 +70,19 @@ func New() *WebAPI { api.router.POST("/logs", LogRequestMiddleware(api.GetLog)) + api.router.NotFound = func(ctx *fasthttp.RequestCtx) { + + if ctx.Request.Header.IsOptions() { + ctx.Response.Header.Add("Access-Control-Allow-Headers", "Content-Type") + ctx.Response.Header.Add("Access-Control-Allow-Methods", "GET, POST, OPTION") + ctx.Response.Header.Add("Access-Control-Allow-Origin", "*") + } else { + ctx.SetStatusCode(404) + _, _ = fmt.Fprintf(ctx, "Not found") + } + + } + return api } diff --git a/api/project.go b/api/project.go index 67b2426..8be3d58 100644 --- a/api/project.go +++ b/api/project.go @@ -16,6 +16,20 @@ type CreateProjectRequest struct { Public bool `json:"public"` } +type UpdateProjectRequest struct { + Name string `json:"name"` + CloneUrl string `json:"clone_url"` + GitRepo string `json:"git_repo"` + Priority int64 `json:"priority"` + Motd string `json:"motd"` + Public bool `json:"public"` +} + +type UpdateProjectResponse struct { + Ok bool `json:"ok"` + Message string `json:"message,omitempty"` +} + type CreateProjectResponse struct { Ok bool `json:"ok"` Id int64 `json:"id,omitempty"` @@ -86,10 +100,66 @@ func (api *WebAPI) ProjectCreate(r *Request) { } } +func (api *WebAPI) ProjectUpdate(r *Request) { + + id, err := strconv.ParseInt(r.Ctx.UserValue("id").(string), 10, 64) + handleErr(err, r) //todo handle invalid id + + updateReq := &UpdateProjectRequest{} + if r.GetJson(updateReq) { + + project := &storage.Project{ + Id: id, + Name: updateReq.Name, + CloneUrl: updateReq.CloneUrl, + GitRepo: updateReq.GitRepo, + Priority: updateReq.Priority, + Motd: updateReq.Motd, + Public: updateReq.Public, + } + + if isValidProject(project) { + err := api.Database.UpdateProject(project) + + if err != nil { + r.Json(CreateProjectResponse{ + Ok: false, + Message: err.Error(), + }, 500) + + logrus.WithError(err).WithFields(logrus.Fields{ + "project": project, + }).Warn("Error during project update") + } else { + r.OkJson(UpdateProjectResponse{ + Ok: true, + }) + + logrus.WithFields(logrus.Fields{ + "project": project, + }).Debug("Updated project") + } + + } else { + logrus.WithFields(logrus.Fields{ + "project": project, + }).Warn("Invalid project") + + r.Json(CreateProjectResponse{ + Ok: false, + Message: "Invalid project", + }, 400) + } + } +} + func isValidProject(project *storage.Project) bool { if len(project.Name) <= 0 { return false } + if project.Priority < 0 { + return false + } return true } @@ -97,7 +167,7 @@ func isValidProject(project *storage.Project) bool { func (api *WebAPI) ProjectGet(r *Request) { id, err := strconv.ParseInt(r.Ctx.UserValue("id").(string), 10, 64) - handleErr(err, r) + handleErr(err, r) //todo handle invalid id project := api.Database.GetProject(id) diff --git a/api/task.go b/api/task.go index da13892..b31f201 100644 --- a/api/task.go +++ b/api/task.go @@ -1,8 +1,13 @@ package api import ( + "bytes" + "crypto" + "crypto/hmac" + "encoding/hex" "errors" "github.com/Sirupsen/logrus" + "github.com/dchest/siphash" "github.com/google/uuid" "src/task_tracker/storage" "strconv" @@ -14,12 +19,13 @@ type CreateTaskRequest struct { Recipe string `json:"recipe"` Priority int64 `json:"priority"` MaxAssignTime int64 `json:"max_assign_time"` + Hash64 int64 `json:"hash_u64"` + UniqueString string `json:"unique_string"` } type ReleaseTaskRequest struct { - TaskId int64 `json:"task_id"` - Success bool `json:"success"` - WorkerId *uuid.UUID `json:"worker_id"` + TaskId int64 `json:"task_id"` + Success bool `json:"success"` } type ReleaseTaskResponse struct { @@ -51,8 +57,14 @@ func (api *WebAPI) TaskCreate(r *Request) { MaxAssignTime: createReq.MaxAssignTime, } - if isTaskValid(task) { - err := api.Database.SaveTask(task, createReq.Project) + if createReq.IsValid() && isTaskValid(task) { + + if createReq.UniqueString != "" { + //TODO: Load key from config + createReq.Hash64 = int64(siphash.Hash(1, 2, []byte(createReq.UniqueString))) + } + + err := api.Database.SaveTask(task, createReq.Project, createReq.Hash64) if err != nil { r.Json(CreateTaskResponse{ @@ -76,6 +88,10 @@ func (api *WebAPI) TaskCreate(r *Request) { } } +func (req *CreateTaskRequest) IsValid() bool { + return req.Hash64 == 0 || req.UniqueString == "" +} + func isTaskValid(task *storage.Task) bool { if task.MaxRetries < 0 { return false @@ -89,7 +105,7 @@ func isTaskValid(task *storage.Task) bool { func (api *WebAPI) TaskGetFromProject(r *Request) { - worker, err := api.workerFromQueryArgs(r) + worker, err := api.validateSignature(r) if err != nil { r.Json(GetTaskResponse{ Ok: false, @@ -121,7 +137,7 @@ func (api *WebAPI) TaskGetFromProject(r *Request) { func (api *WebAPI) TaskGet(r *Request) { - worker, err := api.workerFromQueryArgs(r) + worker, err := api.validateSignature(r) if err != nil { r.Json(GetTaskResponse{ Ok: false, @@ -138,9 +154,11 @@ func (api *WebAPI) TaskGet(r *Request) { }) } -func (api WebAPI) workerFromQueryArgs(r *Request) (*storage.Worker, error) { +func (api WebAPI) validateSignature(r *Request) (*storage.Worker, error) { + + widStr := string(r.Ctx.Request.Header.Peek("X-Worker-Id")) + signature := r.Ctx.Request.Header.Peek("X-Signature") - widStr := string(r.Ctx.QueryArgs().Peek("wid")) wid, err := uuid.Parse(widStr) if err != nil { logrus.WithError(err).WithFields(logrus.Fields{ @@ -155,20 +173,53 @@ func (api WebAPI) workerFromQueryArgs(r *Request) (*storage.Worker, error) { if worker == nil { logrus.WithError(err).WithFields(logrus.Fields{ "wid": widStr, - }).Warn("Can't parse wid") + }).Warn("Worker id does not match any valid worker") return nil, errors.New("worker id does not match any valid worker") } + var body []byte + if r.Ctx.Request.Header.IsGet() { + body = r.Ctx.Request.RequestURI() + } else { + body = r.Ctx.Request.Body() + } + + mac := hmac.New(crypto.SHA256.New, worker.Secret) + mac.Write(body) + + expectedMac := make([]byte, 64) + hex.Encode(expectedMac, mac.Sum(nil)) + matches := bytes.Compare(expectedMac, signature) == 0 + + logrus.WithFields(logrus.Fields{ + "expected": string(expectedMac), + "signature": string(signature), + "matches": matches, + }).Trace("Validating Worker signature") + + if !matches { + return nil, errors.New("invalid signature") + } + return worker, nil } func (api *WebAPI) TaskRelease(r *Request) { - req := ReleaseTaskRequest{} - if r.GetJson(req) { + worker, err := api.validateSignature(r) + if err != nil { + r.Json(GetTaskResponse{ + Ok: false, + Message: err.Error(), + }, 403) + return + } - res := api.Database.ReleaseTask(req.TaskId, req.WorkerId, req.Success) + var req ReleaseTaskRequest + if r.GetJson(&req) { + + res := api.Database.ReleaseTask(req.TaskId, &worker.Id, req.Success) response := ReleaseTaskResponse{ Ok: res, diff --git a/api/worker.go b/api/worker.go index 23c2671..8c5712f 100644 --- a/api/worker.go +++ b/api/worker.go @@ -3,17 +3,28 @@ package api import ( "github.com/Sirupsen/logrus" "github.com/google/uuid" + "math/rand" "src/task_tracker/storage" "time" ) type CreateWorkerRequest struct { + Alias string `json:"alias"` +} + +type UpdateWorkerRequest struct { + Alias string `json:"alias"` +} + +type UpdateWorkerResponse struct { + Ok bool `json:"ok"` + Message string `json:"message,omitempty"` } type CreateWorkerResponse struct { - Ok bool `json:"ok"` - Message string `json:"message,omitempty"` - WorkerId uuid.UUID `json:"id,omitempty"` + Ok bool `json:"ok"` + Message string `json:"message,omitempty"` + Worker *storage.Worker `json:"worker,omitempty"` } type GetWorkerResponse struct { @@ -55,13 +66,13 @@ func (api *WebAPI) WorkerCreate(r *Request) { return } - id, err := api.workerCreate(workerReq, getIdentity(r)) + worker, err := api.workerCreate(workerReq, getIdentity(r)) if err != nil { handleErr(err, r) } else { r.OkJson(CreateWorkerResponse{ - Ok: true, - WorkerId: id, + Ok: true, + Worker: worker, }) } } @@ -84,6 +95,9 @@ func (api *WebAPI) WorkerGet(r *Request) { worker := api.Database.GetWorker(id) if worker != nil { + + worker.Secret = nil + r.OkJson(GetWorkerResponse{ Ok: true, Worker: worker, @@ -136,22 +150,75 @@ func (api *WebAPI) WorkerRemoveAccess(r *Request) { } } -func (api *WebAPI) workerCreate(request *CreateWorkerRequest, identity *storage.Identity) (uuid.UUID, error) { +func (api *WebAPI) WorkerUpdate(r *Request) { + + worker, err := api.validateSignature(r) + if err != nil { + r.Json(GetTaskResponse{ + Ok: false, + Message: err.Error(), + }, 403) + return + } + + req := &UpdateWorkerRequest{} + if r.GetJson(req) { + + worker.Alias = req.Alias + + ok := api.Database.UpdateWorker(worker) + + if ok { + r.OkJson(UpdateWorkerResponse{ + Ok: true, + }) + } else { + r.OkJson(UpdateWorkerResponse{ + Ok: false, + Message: "Could not update worker", + }) + } + } +} + +func (api *WebAPI) workerCreate(request *CreateWorkerRequest, identity *storage.Identity) (*storage.Worker, error) { + + if request.Alias == "" { + request.Alias = "default_alias" + } worker := storage.Worker{ Id: uuid.New(), Created: time.Now().Unix(), Identity: identity, + Secret: makeSecret(), + Alias: request.Alias, } api.Database.SaveWorker(&worker) - return worker.Id, nil + return &worker, nil } func canCreateWorker(r *Request, cwr *CreateWorkerRequest, identity *storage.Identity) bool { + + if cwr.Alias == "unassigned" { + //Reserved alias + return false + } + return true } +func makeSecret() []byte { + + secret := make([]byte, 32) + for i := 0; i < 32; i++ { + secret[i] = byte(rand.Int31()) + } + + return secret +} + func getIdentity(r *Request) *storage.Identity { identity := storage.Identity{ diff --git a/main/main.go b/main/main.go index c529728..7f68007 100644 --- a/main/main.go +++ b/main/main.go @@ -1,9 +1,11 @@ package main import ( + "math/rand" "src/task_tracker/api" "src/task_tracker/config" "src/task_tracker/storage" + "time" ) func tmpDebugSetup() { @@ -15,6 +17,7 @@ func tmpDebugSetup() { func main() { + rand.Seed(time.Now().UTC().UnixNano()) config.SetupConfig() webApi := api.New() diff --git a/schema.sql b/schema.sql index 97c8f17..ff200ef 100755 --- a/schema.sql +++ b/schema.sql @@ -26,9 +26,10 @@ CREATE TABLE worker_identity CREATE TABLE worker ( id TEXT PRIMARY KEY, - alias TEXT DEFAULT NULL, + alias TEXT, created INTEGER, - identity INTEGER REFERENCES workerIdentity (id) + identity INTEGER REFERENCES worker_identity (id), + secret BYTEA ); CREATE TABLE project @@ -61,7 +62,8 @@ CREATE TABLE task status Status DEFAULT 'new', recipe TEXT, max_assign_time INTEGER DEFAULT 0, - assign_time INTEGER DEFAULT 0 + assign_time INTEGER DEFAULT 0, + hash64 BIGINT DEFAULT NULL UNIQUE ); CREATE TABLE log_entry diff --git a/storage/project.go b/storage/project.go index a85ee2c..1be93d7 100644 --- a/storage/project.go +++ b/storage/project.go @@ -3,7 +3,6 @@ package storage import ( "database/sql" "github.com/Sirupsen/logrus" - "github.com/google/uuid" "strings" ) @@ -19,8 +18,8 @@ type Project struct { } type AssignedTasks struct { - Assignee uuid.UUID `json:"assignee"` - TaskCount int64 `json:"task_count"` + Assignee string `json:"assignee"` + TaskCount int64 `json:"task_count"` } type ProjectStats struct { @@ -116,14 +115,16 @@ func (database *Database) GetProjectWithRepoName(repoName string) *Project { return project } -func (database *Database) UpdateProject(project *Project) { +func (database *Database) UpdateProject(project *Project) error { db := database.getDB() res, err := db.Exec(`UPDATE project SET (priority, name, clone_url, git_repo, version, motd, public) = ($1,$2,$3,$4,$5,$6,$7) WHERE id=$8`, project.Priority, project.Name, project.CloneUrl, project.GitRepo, project.Version, project.Motd, project.Public, project.Id) - handleErr(err) + if err != nil { + return err + } rowsAffected, _ := res.RowsAffected() @@ -132,7 +133,7 @@ func (database *Database) UpdateProject(project *Project) { "rowsAffected": rowsAffected, }).Trace("Database.updateProject UPDATE project") - return + return nil } func (database *Database) GetProjectStats(id int64) *ProjectStats { @@ -154,18 +155,27 @@ func (database *Database) GetProjectStats(id int64) *ProjectStats { logrus.WithError(err).WithFields(logrus.Fields{ "id": id, }).Trace("Get project stats: No task for this project") - return nil + } - //todo: only expose worker alias - rows, err := db.Query(`SELECT assignee, COUNT(*) FROM TASK - LEFT JOIN worker ON TASK.assignee = worker.id WHERE project=$1 GROUP BY assignee`, id) + rows, err := db.Query(`SELECT worker.alias, COUNT(*) as wc FROM TASK + LEFT JOIN worker ON TASK.assignee = worker.id WHERE project=$1 + GROUP BY worker.id ORDER BY wc LIMIT 10`, id) + + stats.Assignees = []*AssignedTasks{} for rows.Next() { assignee := AssignedTasks{} - err = rows.Scan(&assignee.Assignee, &assignee.TaskCount) + var assigneeAlias sql.NullString + err = rows.Scan(&assigneeAlias, &assignee.TaskCount) handleErr(err) + if assigneeAlias.Valid { + assignee.Assignee = assigneeAlias.String + } else { + assignee.Assignee = "unassigned" + } + stats.Assignees = append(stats.Assignees, &assignee) } } @@ -182,8 +192,8 @@ func (database Database) GetAllProjectsStats() *[]ProjectStats { SUM(CASE WHEN status='failed' THEN 1 ELSE 0 END) failedCount, SUM(CASE WHEN status='closed' THEN 1 ELSE 0 END) closedCount, p.* - FROM task INNER JOIN project p on task.project = p.id - GROUP BY p.id`) + FROM task RIGHT JOIN project p on task.project = p.id + GROUP BY p.id ORDER BY p.name`) handleErr(err) for rows.Next() { @@ -191,7 +201,7 @@ func (database Database) GetAllProjectsStats() *[]ProjectStats { stats := ProjectStats{} p := &Project{} err := rows.Scan(&stats.NewTaskCount, &stats.FailedTaskCount, &stats.ClosedTaskCount, - &p.Id, &p.Priority, &p.Motd, &p.Name, &p.CloneUrl, &p.GitRepo, &p.Version, &p.Public) + &p.Id, &p.Priority, &p.Name, &p.CloneUrl, &p.GitRepo, &p.Version, &p.Motd, &p.Public) handleErr(err) stats.Project = p diff --git a/storage/task.go b/storage/task.go index d677925..785bcde 100644 --- a/storage/task.go +++ b/storage/task.go @@ -2,6 +2,7 @@ package storage import ( "database/sql" + "fmt" "github.com/Sirupsen/logrus" "github.com/google/uuid" ) @@ -19,13 +20,14 @@ type Task struct { AssignTime int64 `json:"assign_time"` } -func (database *Database) SaveTask(task *Task, project int64) error { +func (database *Database) SaveTask(task *Task, project int64, hash64 int64) error { db := database.getDB() - res, err := db.Exec(` - INSERT INTO task (project, max_retries, recipe, priority, max_assign_time) - VALUES ($1,$2,$3,$4,$5)`, + //TODO: For some reason it refuses to insert the 64-bit value unless I do that... + res, err := db.Exec(fmt.Sprintf(` + INSERT INTO task (project, max_retries, recipe, priority, max_assign_time, hash64) + VALUES ($1,$2,$3,$4,$5,NULLIF(%d, 0))`, hash64), project, task.MaxRetries, task.Recipe, task.Priority, task.MaxAssignTime) if err != nil { logrus.WithError(err).WithFields(logrus.Fields{ @@ -57,7 +59,7 @@ func (database *Database) GetTask(worker *Worker) *Task { SELECT task.id FROM task INNER JOIN project p on task.project = p.id - WHERE assignee IS NULL + WHERE assignee IS NULL AND task.status='new' AND (p.public OR EXISTS ( SELECT 1 FROM worker_has_access_to_project a WHERE a.worker=$1 AND a.project=p.id )) @@ -88,7 +90,8 @@ func (database *Database) GetTask(worker *Worker) *Task { func getTaskById(id int64, db *sql.DB) *Task { row := db.QueryRow(` - SELECT * FROM task + SELECT task.id, task.priority, task.project, assignee, retries, max_retries, + status, recipe, max_assign_time, assign_time, project.* FROM task INNER JOIN project ON task.project = project.id WHERE task.id=$1`, id) task := scanTask(row) @@ -109,11 +112,11 @@ func (database Database) ReleaseTask(id int64, workerId *uuid.UUID, success bool var err error if success { res, err = db.Exec(`UPDATE task SET (status, assignee) = ('closed', NULL) - WHERE id=$2 AND task.assignee=$2`, id, workerId) + WHERE id=$1 AND task.assignee=$2`, id, workerId) } else { res, err = db.Exec(`UPDATE task SET (status, assignee, retries) = (CASE WHEN retries+1 >= max_retries THEN 'failed' ELSE 'new' END, NULL, retries+1) - WHERE id=$2 AND assignee=$2`, id, workerId) + WHERE id=$1 AND assignee=$2`, id, workerId) } handleErr(err) @@ -138,7 +141,7 @@ func (database *Database) GetTaskFromProject(worker *Worker, projectId int64) *T SELECT task.id FROM task INNER JOIN project p on task.project = p.id - WHERE assignee IS NULL AND p.id=$2 + WHERE assignee IS NULL AND p.id=$2 AND status='new' AND (p.public OR EXISTS ( SELECT 1 FROM worker_has_access_to_project a WHERE a.worker=$1 AND a.project=$2 )) diff --git a/storage/worker.go b/storage/worker.go index 7033734..155f551 100644 --- a/storage/worker.go +++ b/storage/worker.go @@ -16,6 +16,8 @@ type Worker struct { Id uuid.UUID `json:"id"` Created int64 `json:"created"` Identity *Identity `json:"identity"` + Alias string `json:"alias,omitempty"` + Secret []byte `json:"secret"` } func (database *Database) SaveWorker(worker *Worker) { @@ -24,8 +26,8 @@ func (database *Database) SaveWorker(worker *Worker) { identityId := getOrCreateIdentity(worker.Identity, db) - res, err := db.Exec("INSERT INTO worker (id, created, identity) VALUES ($1,$2,$3)", - worker.Id, worker.Created, identityId) + res, err := db.Exec("INSERT INTO worker (id, created, identity, secret, alias) VALUES ($1,$2,$3,$4,$5)", + worker.Id, worker.Created, identityId, worker.Secret, worker.Alias) handleErr(err) var rowsAffected, _ = res.RowsAffected() @@ -41,8 +43,8 @@ func (database *Database) GetWorker(id uuid.UUID) *Worker { worker := &Worker{} var identityId int64 - row := db.QueryRow("SELECT id, created, identity FROM worker WHERE id=$1", id) - err := row.Scan(&worker.Id, &worker.Created, &identityId) + row := db.QueryRow("SELECT id, created, identity, secret, alias FROM worker WHERE id=$1", id) + err := row.Scan(&worker.Id, &worker.Created, &identityId, &worker.Secret, &worker.Alias) if err != nil { logrus.WithFields(logrus.Fields{ "id": id, @@ -64,7 +66,7 @@ func getIdentity(id int64, db *sql.DB) (*Identity, error) { identity := &Identity{} - row := db.QueryRow("SELECT remote_addr, user_agent FROM workeridentity WHERE id=$1", id) + row := db.QueryRow("SELECT remote_addr, user_agent FROM worker_identity WHERE id=$1", id) err := row.Scan(&identity.RemoteAddr, &identity.UserAgent) if err != nil { @@ -80,7 +82,7 @@ func getIdentity(id int64, db *sql.DB) (*Identity, error) { func getOrCreateIdentity(identity *Identity, db *sql.DB) int64 { - res, err := db.Exec("INSERT INTO workeridentity (remote_addr, user_agent) VALUES ($1,$2) ON CONFLICT DO NOTHING", + res, err := db.Exec("INSERT INTO worker_identity (remote_addr, user_agent) VALUES ($1,$2) ON CONFLICT DO NOTHING", identity.RemoteAddr, identity.UserAgent) handleErr(err) @@ -89,7 +91,7 @@ func getOrCreateIdentity(identity *Identity, db *sql.DB) int64 { "rowsAffected": rowsAffected, }).Trace("Database.saveWorker INSERT workerIdentity") - row := db.QueryRow("SELECT (id) FROM workeridentity WHERE remote_addr=$1", identity.RemoteAddr) + row := db.QueryRow("SELECT (id) FROM worker_identity WHERE remote_addr=$1", identity.RemoteAddr) var rowId int64 err = row.Scan(&rowId) @@ -127,7 +129,7 @@ func (database *Database) GrantAccess(workerId *uuid.UUID, projectId int64) bool return rowsAffected == 1 } -func (database Database) RemoveAccess(workerId *uuid.UUID, projectId int64) bool { +func (database *Database) RemoveAccess(workerId *uuid.UUID, projectId int64) bool { db := database.getDB() res, err := db.Exec(`DELETE FROM worker_has_access_to_project WHERE worker=$1 AND project=$2`, @@ -144,3 +146,20 @@ func (database Database) RemoveAccess(workerId *uuid.UUID, projectId int64) bool return rowsAffected == 1 } + +func (database *Database) UpdateWorker(worker *Worker) bool { + + db := database.getDB() + res, err := db.Exec(`UPDATE worker SET alias=$1 WHERE id=$2`, + worker.Alias, worker.Id) + handleErr(err) + + rowsAffected, _ := res.RowsAffected() + + logrus.WithFields(logrus.Fields{ + "rowsAffected": rowsAffected, + "worker": worker, + }).Trace("Database.UpdateWorker UPDATE worker") + + return rowsAffected == 1 +} diff --git a/test/api_git_test.go b/test/api_git_test.go index 0051ae0..e8942ff 100644 --- a/test/api_git_test.go +++ b/test/api_git_test.go @@ -13,7 +13,7 @@ import ( func TestWebHookNoSignature(t *testing.T) { - r := Post("/git/receivehook", api.GitPayload{}) + r := Post("/git/receivehook", api.GitPayload{}, nil) if r.StatusCode != 403 { t.Error() diff --git a/test/api_index_test.go b/test/api_index_test.go index ba1d4ad..ac5a4d2 100644 --- a/test/api_index_test.go +++ b/test/api_index_test.go @@ -9,7 +9,7 @@ import ( func TestIndex(t *testing.T) { - r := Get("/") + r := Get("/", nil) body, _ := ioutil.ReadAll(r.Body) var info api.Info diff --git a/test/api_log_test.go b/test/api_log_test.go index 4498498..29f3aec 100644 --- a/test/api_log_test.go +++ b/test/api_log_test.go @@ -16,7 +16,7 @@ func TestTraceValid(t *testing.T) { Scope: "test", Message: "This is a test message", TimeStamp: time.Now().Unix(), - }) + }, nil) if r.StatusCode != 200 { t.Fail() @@ -27,7 +27,7 @@ func TestTraceInvalidScope(t *testing.T) { r := Post("/log/trace", api.LogRequest{ Message: "this is a test message", TimeStamp: time.Now().Unix(), - }) + }, nil) if r.StatusCode != 500 { t.Fail() @@ -37,7 +37,7 @@ func TestTraceInvalidScope(t *testing.T) { Scope: "", Message: "this is a test message", TimeStamp: time.Now().Unix(), - }) + }, nil) if r.StatusCode != 500 { t.Fail() @@ -52,7 +52,7 @@ func TestTraceInvalidMessage(t *testing.T) { Scope: "test", Message: "", TimeStamp: time.Now().Unix(), - }) + }, nil) if r.StatusCode != 500 { t.Fail() @@ -66,7 +66,7 @@ func TestTraceInvalidTime(t *testing.T) { r := Post("/log/trace", api.LogRequest{ Scope: "test", Message: "test", - }) + }, nil) if r.StatusCode != 500 { t.Fail() } @@ -81,7 +81,7 @@ func TestWarnValid(t *testing.T) { Scope: "test", Message: "test", TimeStamp: time.Now().Unix(), - }) + }, nil) if r.StatusCode != 200 { t.Fail() } @@ -93,7 +93,7 @@ func TestInfoValid(t *testing.T) { Scope: "test", Message: "test", TimeStamp: time.Now().Unix(), - }) + }, nil) if r.StatusCode != 200 { t.Fail() } @@ -105,7 +105,7 @@ func TestErrorValid(t *testing.T) { Scope: "test", Message: "test", TimeStamp: time.Now().Unix(), - }) + }, nil) if r.StatusCode != 200 { t.Fail() } @@ -171,7 +171,7 @@ func getLogs(since int64, level logrus.Level) *api.GetLogResponse { r := Post(fmt.Sprintf("/logs"), api.GetLogRequest{ Since: since, Level: level, - }) + }, nil) resp := &api.GetLogResponse{} data, _ := ioutil.ReadAll(r.Body) diff --git a/test/api_project_test.go b/test/api_project_test.go index a72f6b6..6ccbbcf 100644 --- a/test/api_project_test.go +++ b/test/api_project_test.go @@ -3,7 +3,6 @@ package test import ( "encoding/json" "fmt" - "github.com/google/uuid" "io/ioutil" "net/http" "src/task_tracker/api" @@ -132,28 +131,30 @@ func TestGetProjectStats(t *testing.T) { CloneUrl: "http://github.com/drone/test", GitRepo: "drone/test", Priority: 3, + Public: true, }) pid := r.Id + worker := genWid() createTask(api.CreateTaskRequest{ Priority: 1, Project: pid, MaxRetries: 0, Recipe: "{}", - }) + }, worker) createTask(api.CreateTaskRequest{ Priority: 2, Project: pid, MaxRetries: 0, Recipe: "{}", - }) + }, worker) createTask(api.CreateTaskRequest{ Priority: 3, Project: pid, MaxRetries: 0, Recipe: "{}", - }) + }, worker) stats := getProjectStats(pid) @@ -169,7 +170,7 @@ func TestGetProjectStats(t *testing.T) { t.Error() } - if stats.Stats.Assignees[0].Assignee != uuid.Nil { + if stats.Stats.Assignees[0].Assignee != "unassigned" { t.Error() } if stats.Stats.Assignees[0].TaskCount != 3 { @@ -189,19 +190,132 @@ func TestGetProjectStatsNotFound(t *testing.T) { }) s := getProjectStats(r.Id) - if s.Ok != false { + if s.Ok != true { t.Error() } - if len(s.Message) <= 0 { + if s.Stats == nil { t.Error() } } +func TestUpdateProjectValid(t *testing.T) { + + pid := createProject(api.CreateProjectRequest{ + Public: true, + Version: "versionA", + Motd: "MotdA", + Name: "NameA", + CloneUrl: "CloneUrlA", + GitRepo: "GitRepoA", + Priority: 1, + }).Id + + updateResp := updateProject(api.UpdateProjectRequest{ + Priority: 2, + GitRepo: "GitRepoB", + CloneUrl: "CloneUrlB", + Name: "NameB", + Motd: "MotdB", + Public: false, + }, pid) + + if updateResp.Ok != true { + t.Error() + } + + proj, _ := getProject(pid) + + if proj.Project.Public != false { + t.Error() + } + if proj.Project.Motd != "MotdB" { + t.Error() + } + if proj.Project.CloneUrl != "CloneUrlB" { + t.Error() + } + if proj.Project.GitRepo != "GitRepoB" { + t.Error() + } + if proj.Project.Priority != 2 { + t.Error() + } +} + +func TestUpdateProjectInvalid(t *testing.T) { + + pid := createProject(api.CreateProjectRequest{ + Public: true, + Version: "lllllllllllll", + Motd: "2wwwwwwwwwwwwwww", + Name: "aaaaaaaaaaaaaaaaaaaaaa", + CloneUrl: "333333333333333", + GitRepo: "llllllllllllllllllls", + Priority: 1, + }).Id + + updateResp := updateProject(api.UpdateProjectRequest{ + Priority: -1, + GitRepo: "GitRepo------", + CloneUrl: "CloneUrlB000000", + Name: "NameB-0", + Motd: "MotdB000000", + Public: false, + }, pid) + + if updateResp.Ok != false { + t.Error() + } + + if len(updateResp.Message) <= 0 { + t.Error() + } +} + +func TestUpdateProjectConstraintFail(t *testing.T) { + + pid := createProject(api.CreateProjectRequest{ + Public: true, + Version: "testUpdateProjectConstraintFail", + Motd: "testUpdateProjectConstraintFail", + Name: "testUpdateProjectConstraintFail", + CloneUrl: "testUpdateProjectConstraintFail", + GitRepo: "testUpdateProjectConstraintFail", + Priority: 1, + }).Id + + createProject(api.CreateProjectRequest{ + Public: true, + Version: "testUpdateProjectConstraintFail_d", + Motd: "testUpdateProjectConstraintFail_d", + Name: "testUpdateProjectConstraintFail_d", + CloneUrl: "testUpdateProjectConstraintFail_d", + GitRepo: "testUpdateProjectConstraintFail_d", + Priority: 1, + }) + + updateResp := updateProject(api.UpdateProjectRequest{ + Priority: 1, + GitRepo: "testUpdateProjectConstraintFail_d", + CloneUrl: "testUpdateProjectConstraintFail_d", + Name: "testUpdateProjectConstraintFail_d", + Motd: "testUpdateProjectConstraintFail_d", + }, pid) + + if updateResp.Ok != false { + t.Error() + } + + if len(updateResp.Message) <= 0 { + t.Error() + } +} + func createProject(req api.CreateProjectRequest) *api.CreateProjectResponse { - r := Post("/project/create", req) + r := Post("/project/create", req, nil) var resp api.CreateProjectResponse data, _ := ioutil.ReadAll(r.Body) @@ -213,7 +327,7 @@ func createProject(req api.CreateProjectRequest) *api.CreateProjectResponse { func getProject(id int64) (*api.GetProjectResponse, *http.Response) { - r := Get(fmt.Sprintf("/project/get/%d", id)) + r := Get(fmt.Sprintf("/project/get/%d", id), nil) var getResp api.GetProjectResponse data, _ := ioutil.ReadAll(r.Body) @@ -225,7 +339,7 @@ func getProject(id int64) (*api.GetProjectResponse, *http.Response) { func getProjectStats(id int64) *api.GetProjectStatsResponse { - r := Get(fmt.Sprintf("/project/stats/%d", id)) + r := Get(fmt.Sprintf("/project/stats/%d", id), nil) var getResp api.GetProjectStatsResponse data, _ := ioutil.ReadAll(r.Body) @@ -234,3 +348,15 @@ func getProjectStats(id int64) *api.GetProjectStatsResponse { return &getResp } + +func updateProject(request api.UpdateProjectRequest, pid int64) *api.UpdateProjectResponse { + + r := Post(fmt.Sprintf("/project/update/%d", pid), request, nil) + + var resp api.UpdateProjectResponse + data, _ := ioutil.ReadAll(r.Body) + err := json.Unmarshal(data, &resp) + handleErr(err) + + return &resp +} diff --git a/test/api_task_bench_test.go b/test/api_task_bench_test.go index 2d9b1a3..4b678cc 100644 --- a/test/api_task_bench_test.go +++ b/test/api_task_bench_test.go @@ -15,6 +15,8 @@ func BenchmarkCreateTask(b *testing.B) { CloneUrl: "http://localhost", }) + worker := genWid() + b.ResetTimer() for i := 0; i < b.N; i++ { createTask(api.CreateTaskRequest{ @@ -22,6 +24,6 @@ func BenchmarkCreateTask(b *testing.B) { Priority: 1, Recipe: "{}", MaxRetries: 1, - }) + }, worker) } } diff --git a/test/api_task_test.go b/test/api_task_test.go index c5e22ba..14e3361 100644 --- a/test/api_task_test.go +++ b/test/api_task_test.go @@ -6,6 +6,7 @@ import ( "github.com/google/uuid" "io/ioutil" "src/task_tracker/api" + "src/task_tracker/storage" "testing" ) @@ -18,11 +19,13 @@ func TestCreateTaskValid(t *testing.T) { CloneUrl: "http://github.com/test/test", }) + worker := genWid() + resp := createTask(api.CreateTaskRequest{ Project: 1, Recipe: "{}", MaxRetries: 3, - }) + }, worker) if resp.Ok != true { t.Fail() @@ -31,11 +34,13 @@ func TestCreateTaskValid(t *testing.T) { func TestCreateTaskInvalidProject(t *testing.T) { + worker := genWid() + resp := createTask(api.CreateTaskRequest{ Project: 123456, Recipe: "{}", MaxRetries: 3, - }) + }, worker) if resp.Ok != false { t.Error() @@ -62,7 +67,9 @@ func TestGetTaskInvalidWid(t *testing.T) { func TestGetTaskInvalidWorker(t *testing.T) { id := uuid.New() - resp := getTask(&id) + resp := getTask(&storage.Worker{ + Id: id, + }) if resp.Ok != false { t.Error() @@ -76,7 +83,9 @@ func TestGetTaskInvalidWorker(t *testing.T) { func TestGetTaskFromProjectInvalidWorker(t *testing.T) { id := uuid.New() - resp := getTaskFromProject(1, &id) + resp := getTaskFromProject(1, &storage.Worker{ + Id: id, + }) if resp.Ok != false { t.Error() @@ -89,10 +98,12 @@ func TestGetTaskFromProjectInvalidWorker(t *testing.T) { func TestCreateTaskInvalidRetries(t *testing.T) { + worker := genWid() + resp := createTask(api.CreateTaskRequest{ Project: 1, MaxRetries: -1, - }) + }, worker) if resp.Ok != false { t.Error() @@ -105,11 +116,13 @@ func TestCreateTaskInvalidRetries(t *testing.T) { func TestCreateTaskInvalidRecipe(t *testing.T) { + worker := genWid() + resp := createTask(api.CreateTaskRequest{ Project: 1, Recipe: "", MaxRetries: 3, - }) + }, worker) if resp.Ok != false { t.Error() @@ -132,12 +145,14 @@ func TestCreateGetTask(t *testing.T) { Public: true, }) + worker := genWid() + createTask(api.CreateTaskRequest{ Project: resp.Id, Recipe: "{\"url\":\"test\"}", MaxRetries: 3, Priority: 9999, - }) + }, worker) taskResp := getTaskFromProject(resp.Id, genWid()) @@ -194,26 +209,27 @@ func createTasks(prefix string) (int64, int64) { Priority: 999, Public: true, }) + worker := genWid() createTask(api.CreateTaskRequest{ Project: lowP.Id, Recipe: "low1", Priority: 0, - }) + }, worker) createTask(api.CreateTaskRequest{ Project: lowP.Id, Recipe: "low2", Priority: 1, - }) + }, worker) createTask(api.CreateTaskRequest{ Project: highP.Id, Recipe: "high1", Priority: 100, - }) + }, worker) createTask(api.CreateTaskRequest{ Project: highP.Id, Recipe: "high2", Priority: 101, - }) + }, worker) return lowP.Id, highP.Id } @@ -274,7 +290,7 @@ func TestTaskPriority(t *testing.T) { func TestTaskNoAccess(t *testing.T) { - wid := genWid() + worker := genWid() pid := createProject(api.CreateProjectRequest{ Name: "This is a private proj", @@ -292,16 +308,16 @@ func TestTaskNoAccess(t *testing.T) { MaxAssignTime: 10, MaxRetries: 2, Recipe: "---", - }) + }, worker) if createResp.Ok != true { t.Error() } - grantAccess(wid, pid) - removeAccess(wid, pid) + grantAccess(&worker.Id, pid) + removeAccess(&worker.Id, pid) - tResp := getTaskFromProject(pid, wid) + tResp := getTaskFromProject(pid, worker) if tResp.Ok != false { t.Error() @@ -316,7 +332,7 @@ func TestTaskNoAccess(t *testing.T) { func TestTaskHasAccess(t *testing.T) { - wid := genWid() + worker := genWid() pid := createProject(api.CreateProjectRequest{ Name: "This is a private proj1", @@ -334,15 +350,15 @@ func TestTaskHasAccess(t *testing.T) { MaxAssignTime: 10, MaxRetries: 2, Recipe: "---", - }) + }, worker) if createResp.Ok != true { t.Error() } - grantAccess(wid, pid) + grantAccess(&worker.Id, pid) - tResp := getTaskFromProject(pid, wid) + tResp := getTaskFromProject(pid, worker) if tResp.Ok != true { t.Error() @@ -354,16 +370,16 @@ func TestTaskHasAccess(t *testing.T) { func TestNoMoreTasks(t *testing.T) { - wid := genWid() + worker := genWid() for i := 0; i < 15; i++ { - getTask(wid) + getTask(worker) } } func TestReleaseTaskSuccess(t *testing.T) { - //wid := genWid() + worker := genWid() pid := createProject(api.CreateProjectRequest{ Priority: 0, @@ -372,6 +388,7 @@ func TestReleaseTaskSuccess(t *testing.T) { Version: "11111111111111111", Name: "testreleasetask", Motd: "", + Public: true, }).Id createTask(api.CreateTaskRequest{ @@ -379,13 +396,119 @@ func TestReleaseTaskSuccess(t *testing.T) { Project: pid, Recipe: "{}", MaxRetries: 3, - }) + }, worker) + task := getTaskFromProject(pid, worker).Task + + releaseResp := releaseTask(api.ReleaseTaskRequest{ + TaskId: task.Id, + Success: true, + }, worker) + + if releaseResp.Ok != true { + t.Error() + } + + otherTask := getTaskFromProject(pid, worker) + + //Shouldn't have more tasks available + if otherTask.Ok != false { + t.Error() + } } -func createTask(request api.CreateTaskRequest) *api.CreateTaskResponse { +func TestCreateIntCollision(t *testing.T) { - r := Post("/task/create", request) + pid := createProject(api.CreateProjectRequest{ + Priority: 1, + GitRepo: "testcreateintcollision", + CloneUrl: "testcreateintcollision", + Motd: "testcreateintcollision", + Public: true, + Name: "testcreateintcollision", + Version: "testcreateintcollision", + }).Id + + w := genWid() + + if createTask(api.CreateTaskRequest{ + Project: pid, + Hash64: 123, + Priority: 1, + Recipe: "{}", + }, w).Ok != true { + t.Error() + } + + resp := createTask(api.CreateTaskRequest{ + Project: pid, + Hash64: 123, + Priority: 1, + Recipe: "{}", + }, w) + + if resp.Ok != false { + t.Error() + } + + fmt.Println(resp.Message) + if len(resp.Message) <= 0 { + t.Error() + } +} + +func TestCreateStringCollision(t *testing.T) { + + pid := createProject(api.CreateProjectRequest{ + Priority: 1, + GitRepo: "testcreatestringcollision", + CloneUrl: "testcreatestringcollision", + Motd: "testcreatestringcollision", + Public: true, + Name: "testcreatestringcollision", + Version: "testcreatestringcollision", + }).Id + + w := genWid() + + if createTask(api.CreateTaskRequest{ + Project: pid, + UniqueString: "Hello, world", + Priority: 1, + Recipe: "{}", + }, w).Ok != true { + t.Error() + } + + resp := createTask(api.CreateTaskRequest{ + Project: pid, + UniqueString: "Hello, world", + Priority: 1, + Recipe: "{}", + }, w) + + if !createTask(api.CreateTaskRequest{ + Project: pid, + UniqueString: "This one should work", + Priority: 1, + Recipe: "{}", + }, w).Ok { + t.Error() + } + + if resp.Ok != false { + t.Error() + } + + fmt.Println(resp.Message) + if len(resp.Message) <= 0 { + t.Error() + } +} + +func createTask(request api.CreateTaskRequest, worker *storage.Worker) *api.CreateTaskResponse { + + r := Post("/task/create", request, worker) var resp api.CreateTaskResponse data, _ := ioutil.ReadAll(r.Body) @@ -395,9 +518,9 @@ func createTask(request api.CreateTaskRequest) *api.CreateTaskResponse { return &resp } -func getTask(wid *uuid.UUID) *api.GetTaskResponse { +func getTask(worker *storage.Worker) *api.GetTaskResponse { - r := Get(fmt.Sprintf("/task/get?wid=%s", wid)) + r := Get(fmt.Sprintf("/task/get"), worker) var resp api.GetTaskResponse data, _ := ioutil.ReadAll(r.Body) @@ -407,9 +530,9 @@ func getTask(wid *uuid.UUID) *api.GetTaskResponse { return &resp } -func getTaskFromProject(project int64, wid *uuid.UUID) *api.GetTaskResponse { +func getTaskFromProject(project int64, worker *storage.Worker) *api.GetTaskResponse { - r := Get(fmt.Sprintf("/task/get/%d?wid=%s", project, wid)) + r := Get(fmt.Sprintf("/task/get/%d", project), worker) var resp api.GetTaskResponse data, _ := ioutil.ReadAll(r.Body) @@ -418,3 +541,15 @@ func getTaskFromProject(project int64, wid *uuid.UUID) *api.GetTaskResponse { return &resp } + +func releaseTask(request api.ReleaseTaskRequest, worker *storage.Worker) *api.ReleaseTaskResponse { + + r := Post("/task/release", request, worker) + + var resp api.ReleaseTaskResponse + data, _ := ioutil.ReadAll(r.Body) + err := json.Unmarshal(data, &resp) + handleErr(err) + + return &resp +} diff --git a/test/api_worker_test.go b/test/api_worker_test.go index 57fa67c..9d1f2b4 100644 --- a/test/api_worker_test.go +++ b/test/api_worker_test.go @@ -7,12 +7,15 @@ import ( "io/ioutil" "net/http" "src/task_tracker/api" + "src/task_tracker/storage" "testing" ) func TestCreateGetWorker(t *testing.T) { - resp, r := createWorker(api.CreateWorkerRequest{}) + resp, r := createWorker(api.CreateWorkerRequest{ + Alias: "my_worker_alias", + }) if r.StatusCode != 200 { t.Error() @@ -22,12 +25,12 @@ func TestCreateGetWorker(t *testing.T) { t.Error() } - getResp, r := getWorker(resp.WorkerId.String()) + getResp, r := getWorker(resp.Worker.Id.String()) if r.StatusCode != 200 { t.Error() } - if resp.WorkerId != getResp.Worker.Id { + if resp.Worker.Id != getResp.Worker.Id { t.Error() } @@ -37,6 +40,9 @@ func TestCreateGetWorker(t *testing.T) { if len(getResp.Worker.Identity.UserAgent) <= 0 { t.Error() } + if resp.Worker.Alias != "my_worker_alias" { + t.Error() + } } func TestGetWorkerNotFound(t *testing.T) { @@ -70,7 +76,7 @@ func TestGrantAccessFailedProjectConstraint(t *testing.T) { wid := genWid() - resp := grantAccess(wid, 38274593) + resp := grantAccess(&wid.Id, 38274593) if resp.Ok != false { t.Error() @@ -82,9 +88,9 @@ func TestGrantAccessFailedProjectConstraint(t *testing.T) { func TestRemoveAccessFailedProjectConstraint(t *testing.T) { - wid := genWid() + worker := genWid() - resp := removeAccess(wid, 38274593) + resp := removeAccess(&worker.Id, 38274593) if resp.Ok != false { t.Error() @@ -138,8 +144,43 @@ func TestGrantAccessFailedWorkerConstraint(t *testing.T) { } } +func TestUpdateAliasValid(t *testing.T) { + + wid := genWid() + + updateResp := updateWorker(api.UpdateWorkerRequest{ + Alias: "new alias", + }, wid) + + if updateResp.Ok != true { + t.Error() + } + + w, _ := getWorker(wid.Id.String()) + + if w.Worker.Alias != "new alias" { + t.Error() + } +} + +func TestCreateWorkerAliasInvalid(t *testing.T) { + + resp, _ := createWorker(api.CreateWorkerRequest{ + Alias: "unassigned", //reserved alias + }) + + if resp.Ok != false { + t.Error() + } + + if len(resp.Message) <= 0 { + t.Error() + } + +} + func createWorker(req api.CreateWorkerRequest) (*api.CreateWorkerResponse, *http.Response) { - r := Post("/worker/create", req) + r := Post("/worker/create", req, nil) var resp *api.CreateWorkerResponse data, _ := ioutil.ReadAll(r.Body) @@ -151,7 +192,7 @@ func createWorker(req api.CreateWorkerRequest) (*api.CreateWorkerResponse, *http func getWorker(id string) (*api.GetWorkerResponse, *http.Response) { - r := Get(fmt.Sprintf("/worker/get/%s", id)) + r := Get(fmt.Sprintf("/worker/get/%s", id), nil) var resp *api.GetWorkerResponse data, _ := ioutil.ReadAll(r.Body) @@ -161,10 +202,10 @@ func getWorker(id string) (*api.GetWorkerResponse, *http.Response) { return resp, r } -func genWid() *uuid.UUID { +func genWid() *storage.Worker { resp, _ := createWorker(api.CreateWorkerRequest{}) - return &resp.WorkerId + return resp.Worker } func grantAccess(wid *uuid.UUID, project int64) *api.WorkerAccessResponse { @@ -172,7 +213,7 @@ func grantAccess(wid *uuid.UUID, project int64) *api.WorkerAccessResponse { r := Post("/access/grant", api.WorkerAccessRequest{ WorkerId: wid, ProjectId: project, - }) + }, nil) var resp *api.WorkerAccessResponse data, _ := ioutil.ReadAll(r.Body) @@ -187,7 +228,7 @@ func removeAccess(wid *uuid.UUID, project int64) *api.WorkerAccessResponse { r := Post("/access/remove", api.WorkerAccessRequest{ WorkerId: wid, ProjectId: project, - }) + }, nil) var resp *api.WorkerAccessResponse data, _ := ioutil.ReadAll(r.Body) @@ -196,3 +237,15 @@ func removeAccess(wid *uuid.UUID, project int64) *api.WorkerAccessResponse { return resp } + +func updateWorker(request api.UpdateWorkerRequest, w *storage.Worker) *api.UpdateWorkerResponse { + + r := Post("/worker/update", request, w) + + var resp *api.UpdateWorkerResponse + data, _ := ioutil.ReadAll(r.Body) + err := json.Unmarshal(data, &resp) + handleErr(err) + + return resp +} diff --git a/test/common.go b/test/common.go index c14d7c3..d3f5f5e 100644 --- a/test/common.go +++ b/test/common.go @@ -2,26 +2,61 @@ package test import ( "bytes" + "crypto" + "crypto/hmac" + "encoding/hex" "encoding/json" + "fmt" "io" "io/ioutil" "net/http" "src/task_tracker/config" + "src/task_tracker/storage" ) -func Post(path string, x interface{}) *http.Response { +func Post(path string, x interface{}, worker *storage.Worker) *http.Response { body, err := json.Marshal(x) buf := bytes.NewBuffer(body) - r, err := http.Post("http://"+config.Cfg.ServerAddr+path, "application/json", buf) + req, err := http.NewRequest("POST", "http://"+config.Cfg.ServerAddr+path, buf) + handleErr(err) + + if worker != nil { + mac := hmac.New(crypto.SHA256.New, worker.Secret) + mac.Write(body) + sig := hex.EncodeToString(mac.Sum(nil)) + + req.Header.Add("X-Worker-Id", worker.Id.String()) + req.Header.Add("X-Signature", sig) + } + + client := http.Client{} + r, err := client.Do(req) handleErr(err) return r } -func Get(path string) *http.Response { - r, err := http.Get("http://" + config.Cfg.ServerAddr + path) +func Get(path string, worker *storage.Worker) *http.Response { + + url := "http://" + config.Cfg.ServerAddr + path + req, err := http.NewRequest("GET", url, nil) + handleErr(err) + + if worker != nil { + + fmt.Println(worker.Secret) + mac := hmac.New(crypto.SHA256.New, worker.Secret) + mac.Write([]byte(path)) + sig := hex.EncodeToString(mac.Sum(nil)) + + req.Header.Add("X-Worker-Id", worker.Id.String()) + req.Header.Add("X-Signature", sig) + } + + client := http.Client{} + r, err := client.Do(req) handleErr(err) return r diff --git a/test/schema.sql b/test/schema.sql index 97c8f17..d108236 100755 --- a/test/schema.sql +++ b/test/schema.sql @@ -26,9 +26,10 @@ CREATE TABLE worker_identity CREATE TABLE worker ( id TEXT PRIMARY KEY, - alias TEXT DEFAULT NULL, + alias TEXT, created INTEGER, - identity INTEGER REFERENCES workerIdentity (id) + identity INTEGER REFERENCES worker_identity (id), + secret BYTEA ); CREATE TABLE project @@ -61,7 +62,8 @@ CREATE TABLE task status Status DEFAULT 'new', recipe TEXT, max_assign_time INTEGER DEFAULT 0, - assign_time INTEGER DEFAULT 0 + assign_time INTEGER DEFAULT 0, + hash64 BIGINT UNIQUE ); CREATE TABLE log_entry diff --git a/web/angular/src/app/api.service.ts b/web/angular/src/app/api.service.ts index 71b14aa..4ed0249 100755 --- a/web/angular/src/app/api.service.ts +++ b/web/angular/src/app/api.service.ts @@ -1,5 +1,6 @@ import {Injectable} from '@angular/core'; import {HttpClient} from "@angular/common/http"; +import {Project} from "./models/project"; @Injectable() export class ApiService { @@ -26,4 +27,12 @@ export class ApiService { getProject(id: number) { return this.http.get(this.url + "/project/get/" + id) } + + createProject(project: Project) { + return this.http.post(this.url + "/project/create", project) + } + + updateProject(project: Project) { + return this.http.post(this.url + "/project/update/" + project.id, project) + } } diff --git a/web/angular/src/app/app.component.html b/web/angular/src/app/app.component.html index a9228dc..0d02342 100755 --- a/web/angular/src/app/app.component.html +++ b/web/angular/src/app/app.component.html @@ -2,11 +2,11 @@ + diff --git a/web/angular/src/app/app.module.ts b/web/angular/src/app/app.module.ts index 64fdfd1..cd22cc0 100755 --- a/web/angular/src/app/app.module.ts +++ b/web/angular/src/app/app.module.ts @@ -20,18 +20,21 @@ import { MatPaginatorModule, MatSliderModule, MatSlideToggleModule, + MatSnackBarModule, MatSortModule, MatTableModule, MatToolbarModule, MatTreeModule } from "@angular/material"; import {ApiService} from "./api.service"; +import {MessengerService} from "./messenger.service"; import {HttpClientModule} from "@angular/common/http"; import {ProjectDashboardComponent} from './project-dashboard/project-dashboard.component'; import {ProjectListComponent} from './project-list/project-list.component'; import {CreateProjectComponent} from './create-project/create-project.component'; import {FormsModule, ReactiveFormsModule} from "@angular/forms"; import {UpdateProjectComponent} from './update-project/update-project.component'; +import {SnackBarComponent} from "./messenger/snack-bar.component"; @NgModule({ declarations: [ @@ -40,7 +43,8 @@ import {UpdateProjectComponent} from './update-project/update-project.component' ProjectDashboardComponent, ProjectListComponent, CreateProjectComponent, - UpdateProjectComponent + UpdateProjectComponent, + SnackBarComponent, ], imports: [ BrowserModule, @@ -65,12 +69,17 @@ import {UpdateProjectComponent} from './update-project/update-project.component' MatSliderModule, MatSlideToggleModule, MatCheckboxModule, - MatDividerModule + MatDividerModule, + MatSnackBarModule, ], exports: [], providers: [ ApiService, + MessengerService, + ], + entryComponents: [ + SnackBarComponent, ], bootstrap: [AppComponent] }) diff --git a/web/angular/src/app/create-project/create-project.component.html b/web/angular/src/app/create-project/create-project.component.html index 35323f9..9cc8711 100644 --- a/web/angular/src/app/create-project/create-project.component.html +++ b/web/angular/src/app/create-project/create-project.component.html @@ -3,7 +3,7 @@ -
+ Project name @@ -29,11 +29,10 @@ + +
- - - diff --git a/web/angular/src/app/create-project/create-project.component.ts b/web/angular/src/app/create-project/create-project.component.ts index 8a289e2..07e972e 100644 --- a/web/angular/src/app/create-project/create-project.component.ts +++ b/web/angular/src/app/create-project/create-project.component.ts @@ -1,5 +1,9 @@ import {Component, OnInit} from '@angular/core'; import {Project} from "../models/project"; +import {ApiService} from "../api.service"; +import {MessengerService} from "../messenger.service"; +import {Router} from "@angular/router"; + @Component({ selector: 'app-create-project', @@ -10,7 +14,9 @@ export class CreateProjectComponent implements OnInit { private project = new Project(); - constructor() { + constructor(private apiService: ApiService, + private messengerService: MessengerService, + private router: Router) { this.project.name = "test"; this.project.public = true; } @@ -18,4 +24,16 @@ export class CreateProjectComponent implements OnInit { ngOnInit() { } + onSubmit() { + this.apiService.createProject(this.project).subscribe( + data => { + this.router.navigateByUrl("/project/" + data["id"]); + }, + error => { + console.log(error.error.message); + this.messengerService.show(error.error.message); + } + ) + } + } diff --git a/web/angular/src/app/messenger.service.ts b/web/angular/src/app/messenger.service.ts new file mode 100644 index 0000000..a2316e6 --- /dev/null +++ b/web/angular/src/app/messenger.service.ts @@ -0,0 +1,22 @@ +import {Injectable} from '@angular/core'; +import {Subject} from "rxjs"; +import {MessengerState} from "./messenger/messenger"; + +@Injectable() +export class MessengerService { + + public messengerSubject = new Subject(); + + show(message: string) { + this.messengerSubject.next({ + message: message, + hidden: false, + }) + } + + hide() { + this.messengerSubject.next({ + hidden: true, + }) + } +} diff --git a/web/angular/src/app/messenger/messenger-snack-bar.css b/web/angular/src/app/messenger/messenger-snack-bar.css new file mode 100644 index 0000000..e69de29 diff --git a/web/angular/src/app/messenger/messenger-snack-bar.html b/web/angular/src/app/messenger/messenger-snack-bar.html new file mode 100644 index 0000000..e69de29 diff --git a/web/angular/src/app/messenger/messenger.ts b/web/angular/src/app/messenger/messenger.ts new file mode 100644 index 0000000..2dfbde1 --- /dev/null +++ b/web/angular/src/app/messenger/messenger.ts @@ -0,0 +1,6 @@ +export class MessengerState { + + hidden: boolean; + message?: string; + +} diff --git a/web/angular/src/app/messenger/snack-bar.component.ts b/web/angular/src/app/messenger/snack-bar.component.ts new file mode 100644 index 0000000..44afaee --- /dev/null +++ b/web/angular/src/app/messenger/snack-bar.component.ts @@ -0,0 +1,32 @@ +import {Component, OnInit} from '@angular/core'; +import {MessengerService} from "../messenger.service"; +import {MessengerState} from "./messenger"; +import {Subscription} from "rxjs"; +import {MatSnackBar, MatSnackBarConfig} from "@angular/material"; + +@Component({ + selector: 'messenger-snack-bar', + templateUrl: 'messenger-snack-bar.html', + styleUrls: ['messenger-snack-bar.css'], +}) +export class SnackBarComponent implements OnInit { + + private subscription: Subscription; + + constructor(private messengerService: MessengerService, private snackBar: MatSnackBar) { + + } + + ngOnInit() { + this.subscription = this.messengerService.messengerSubject + .subscribe((state: MessengerState) => { + if (state.hidden) { + this.snackBar.dismiss(); + } else { + this.snackBar.open(state.message, "Close", { + duration: 10 * 1000, + }) + } + }); + } +} diff --git a/web/angular/src/app/models/project.ts b/web/angular/src/app/models/project.ts index eb49c56..f74b5ef 100644 --- a/web/angular/src/app/models/project.ts +++ b/web/angular/src/app/models/project.ts @@ -1,5 +1,6 @@ export class Project { + public id: number; public priority: number; public motd: string; public name: string; diff --git a/web/angular/src/app/project-dashboard/project-dashboard.component.html b/web/angular/src/app/project-dashboard/project-dashboard.component.html index b692411..abfa326 100644 --- a/web/angular/src/app/project-dashboard/project-dashboard.component.html +++ b/web/angular/src/app/project-dashboard/project-dashboard.component.html @@ -37,6 +37,8 @@
{{projectStats | json}}
+ Update + diff --git a/web/angular/src/app/project-dashboard/project-dashboard.component.ts b/web/angular/src/app/project-dashboard/project-dashboard.component.ts index e82eeb8..65fb135 100644 --- a/web/angular/src/app/project-dashboard/project-dashboard.component.ts +++ b/web/angular/src/app/project-dashboard/project-dashboard.component.ts @@ -56,14 +56,14 @@ export class ProjectDashboardComponent implements OnInit { setupStatusPieChart() { let tooltip = d3.select("#stooltip"); - this.statusSvg = d3.select('#status') + this.statusSvg = d3.select("#status") .append('svg') .attr('width', this.pieWidth) .attr('height', this.pieHeight) .append("g") .attr("transform", "translate(" + this.pieRadius + "," + this.pieRadius + ")"); - this.statusPath = this.statusSvg.selectAll("path") + this.statusPath = this.statusSvg.selectAll() .data(this.pieFun(this.statusData)) .enter() .append('path') @@ -76,14 +76,14 @@ export class ProjectDashboardComponent implements OnInit { setupAssigneesPieChart() { let tooltip = d3.select("#atooltip"); - this.assigneesSvg = d3.select('#assignees') + this.assigneesSvg = d3.select("#assignees") .append('svg') .attr('width', this.pieWidth) .attr('height', this.pieHeight) .append("g") .attr("transform", "translate(" + this.pieRadius + "," + this.pieRadius + ")"); - this.assigneesPath = this.assigneesSvg.selectAll("path") + this.assigneesPath = this.assigneesSvg.selectAll() .data(this.pieFun(this.assigneesData)) .enter() .append('path') @@ -229,10 +229,10 @@ export class ProjectDashboardComponent implements OnInit { {label: "Failed", count: this.projectStats["failed_task_count"]}, {label: "Closed", count: this.projectStats["closed_task_count"]}, ]; - this.assigneesData = _.map(this.projectStats["assignees"], (assignedTasks) => { + this.assigneesData = _.map(this.projectStats["assignees"], assignedTask => { return { - label: assignedTasks["assignee"] == "00000000-0000-0000-0000-000000000000" ? "unassigned" : assignedTasks["assignee"], - count: assignedTasks["task_count"] + label: assignedTask["assignee"], + count: assignedTask["task_count"], } }); @@ -256,6 +256,16 @@ export class ProjectDashboardComponent implements OnInit { ]; this.assigneesData = [ {label: 'null', count: 0}, + {label: 'null', count: 0}, + {label: 'null', count: 0}, + {label: 'null', count: 0}, + {label: 'null', count: 0}, + {label: 'null', count: 0}, + {label: 'null', count: 0}, + {label: 'null', count: 0}, + {label: 'null', count: 0}, + {label: 'null', count: 0}, + {label: 'null', count: 0}, ]; this.setupStatusPieChart(); diff --git a/web/angular/src/app/project-list/project-list.component.html b/web/angular/src/app/project-list/project-list.component.html index aef2047..70e5669 100755 --- a/web/angular/src/app/project-list/project-list.component.html +++ b/web/angular/src/app/project-list/project-list.component.html @@ -12,6 +12,7 @@
{{stats.project | json}}
Dashboard + Update
diff --git a/web/angular/src/app/update-project/update-project.component.html b/web/angular/src/app/update-project/update-project.component.html index e48a77b..9c9076d 100644 --- a/web/angular/src/app/update-project/update-project.component.html +++ b/web/angular/src/app/update-project/update-project.component.html @@ -1,29 +1,29 @@ Update project - Changes are saved in real time -
- + + - - + + - + - - + Changes on the master branch will be tracked if webhooks are enabled - + +
diff --git a/web/angular/src/app/update-project/update-project.component.ts b/web/angular/src/app/update-project/update-project.component.ts index 35567e7..49135a9 100644 --- a/web/angular/src/app/update-project/update-project.component.ts +++ b/web/angular/src/app/update-project/update-project.component.ts @@ -1,7 +1,8 @@ import {Component, OnInit} from '@angular/core'; import {Project} from "../models/project"; import {ApiService} from "../api.service"; -import {ActivatedRoute} from "@angular/router"; +import {ActivatedRoute, Router} from "@angular/router"; +import {MessengerService} from "../messenger.service"; @Component({ selector: 'app-update-project', @@ -10,7 +11,10 @@ import {ActivatedRoute} from "@angular/router"; }) export class UpdateProjectComponent implements OnInit { - constructor(private apiService: ApiService, private route: ActivatedRoute) { + constructor(private apiService: ApiService, + private route: ActivatedRoute, + private messengerService: MessengerService, + private router: Router) { } private project: Project; @@ -26,15 +30,28 @@ export class UpdateProjectComponent implements OnInit { private getProject() { this.apiService.getProject(this.projectId).subscribe(data => { this.project = { + id: data["project"]["id"], name: data["project"]["name"], clone_url: data["project"]["clone_url"], git_repo: data["project"]["git_repo"], motd: data["project"]["motd"], priority: data["project"]["priority"], - version: data["project"]["version"] - + version: data["project"]["version"], + public: data["project"]["public"], } }) } + onSubmit() { + this.apiService.updateProject(this.project).subscribe( + data => { + this.router.navigateByUrl("/project/" + this.project.id); + }, + error => { + console.log(error.error.message); + this.messengerService.show(error.error.message); + } + ) + } + }