From 1966d770ebbcceaf4ddc90fcaae107904589c14f Mon Sep 17 00:00:00 2001 From: Maciej Wereski Date: Fri, 29 Sep 2017 11:14:22 +0200 Subject: [PATCH] HTTP API: Implement listing and filtering requests Change-Id: I27f8d037978ef0f0f60ea5c7ee406f8459a90796 Signed-off-by: Maciej Wereski --- requests/requests.go | 12 ++- requests/requests_test.go | 7 ++ server/api/v1/api_test.go | 3 + server/api/v1/handlers.go | 21 ++++- server/api/v1/handlers_test.go | 96 ++++++++++++++++++++-- .../v1/testdata/filter-reqs-bad-filter-POST.json | 1 + server/api/v1/testdata/filter-reqs-empty-POST.json | 1 + .../v1/testdata/filter-reqs-empty-json-POST.json | 1 + .../testdata/filter-reqs-malformed-json-POST.json | 1 + server/api/v1/testdata/filter-reqs-nil-POST.json | 1 + .../v1/testdata/filter-reqs-nomatch-all-POST.json | 1 + .../v1/testdata/filter-reqs-valid-filter-POST.json | 1 + server/api/v1/testdata/list-reqs-all-GET.json | 2 +- server/api/v1/testdata/list-reqs-filter-POST.json | 1 - 14 files changed, 137 insertions(+), 12 deletions(-) create mode 100644 server/api/v1/testdata/filter-reqs-bad-filter-POST.json create mode 100644 server/api/v1/testdata/filter-reqs-empty-POST.json create mode 100644 server/api/v1/testdata/filter-reqs-empty-json-POST.json create mode 100644 server/api/v1/testdata/filter-reqs-malformed-json-POST.json create mode 100644 server/api/v1/testdata/filter-reqs-nil-POST.json create mode 100644 server/api/v1/testdata/filter-reqs-nomatch-all-POST.json create mode 100644 server/api/v1/testdata/filter-reqs-valid-filter-POST.json delete mode 100644 server/api/v1/testdata/list-reqs-filter-POST.json diff --git a/requests/requests.go b/requests/requests.go index c343f22..952c55a 100644 --- a/requests/requests.go +++ b/requests/requests.go @@ -18,6 +18,8 @@ package requests import ( + "reflect" + "sort" "sync" "time" @@ -257,16 +259,20 @@ func (reqs *ReqsCollection) GetRequestInfo(reqID ReqID) (ReqInfo, error) { } // ListRequests is part of implementation of Requests interface. It returns slice -// of ReqInfo that matches ListFilter. +// of ReqInfo that matches ListFilter. Returned slice is sorted by request ids. func (reqs *ReqsCollection) ListRequests(filter ListFilter) ([]ReqInfo, error) { reqs.mutex.RLock() - defer reqs.mutex.RUnlock() res := make([]ReqInfo, 0, len(reqs.requests)) for _, req := range reqs.requests { - if filter == nil || filter.Match(req) { + if filter == nil || reflect.ValueOf(filter).IsNil() || + filter.Match(req) { res = append(res, *req) } } + reqs.mutex.RUnlock() + // TODO(mwereski): HTTP backend needs this to be sorted. This isn't best + // place to do it, rethink that when DB backend is implemented. + sort.Slice(res, func(i, j int) bool { return res[i].ID < res[j].ID }) return res, nil } diff --git a/requests/requests_test.go b/requests/requests_test.go index 2226ffb..3dcb708 100644 --- a/requests/requests_test.go +++ b/requests/requests_test.go @@ -471,9 +471,16 @@ func TestListRequests(t *testing.T) { } // Nil filter should return all requests. + + // Nil interface. resp, err := rqueue.ListRequests(nil) assert.Nil(err) checkReqs(reqs, resp) + var flt *reqFilter + // Concrete type is nil but interface isn't nil. + resp, err = rqueue.ListRequests(flt) + assert.Nil(err) + checkReqs(reqs, resp) } func TestAcquireWorker(t *testing.T) { diff --git a/server/api/v1/api_test.go b/server/api/v1/api_test.go index f532063..854e10a 100644 --- a/server/api/v1/api_test.go +++ b/server/api/v1/api_test.go @@ -38,6 +38,9 @@ import ( const ( contentTypeJSON = "application/json" invalidID = "test" + dateLayout = "2006-01-02" + past = "1683-09-12" + future = "2222-12-31" validReqJSON = `{ "ID":1, "State":"WAITING", diff --git a/server/api/v1/handlers.go b/server/api/v1/handlers.go index 29cf5b2..80aa825 100644 --- a/server/api/v1/handlers.go +++ b/server/api/v1/handlers.go @@ -20,6 +20,7 @@ package v1 import ( "encoding/json" + "io" "net/http" . "git.tizen.org/tools/boruta" @@ -86,7 +87,25 @@ func (api *API) getRequestInfoHandler(r *http.Request, ps map[string]string) res // listRequestsHandler parses HTTP request for listing Boruta requests and calls // ListRequests(). func (api *API) listRequestsHandler(r *http.Request, ps map[string]string) responseData { - return newServerError(ErrNotImplemented, "list requests") + var filter *RequestFilter + defer r.Body.Close() + + if r.Method == http.MethodPost { + filter = new(RequestFilter) + if err := json.NewDecoder(r.Body).Decode(filter); err != nil { + if err != io.EOF { + return newServerError(err) + } + filter = nil + } + } + + reqs, err := api.reqs.ListRequests(filter) + if err != nil { + return newServerError(err) + } + + return reqs } // acquireWorkerHandler parses HTTP request for acquiring worker for Boruta diff --git a/server/api/v1/handlers_test.go b/server/api/v1/handlers_test.go index 5b0811f..9d8a556 100644 --- a/server/api/v1/handlers_test.go +++ b/server/api/v1/handlers_test.go @@ -17,9 +17,11 @@ package v1 import ( "encoding/json" + "errors" "fmt" "net/http" "testing" + "time" . "git.tizen.org/tools/boruta" "git.tizen.org/tools/boruta/requests" @@ -159,23 +161,105 @@ func TestListRequestsHandler(t *testing.T) { assert, m, api := initTest(t) defer m.finish() + deadline, err := time.Parse(dateLayout, future) + assert.Nil(err) + validAfter, err := time.Parse(dateLayout, past) + assert.Nil(err) + reqs := []ReqInfo{ + {ID: 1, Priority: (HiPrio + LoPrio) / 2, State: WAIT, + Deadline: deadline, ValidAfter: validAfter}, + {ID: 2, Priority: (HiPrio+LoPrio)/2 + 1, State: WAIT, + Deadline: deadline, ValidAfter: validAfter}, + {ID: 3, Priority: (HiPrio + LoPrio) / 2, State: CANCEL, + Deadline: deadline, ValidAfter: validAfter}, + {ID: 4, Priority: (HiPrio+LoPrio)/2 + 1, State: CANCEL, + Deadline: deadline, ValidAfter: validAfter}, + } + + methods := []string{http.MethodPost} + prefix := "filter-reqs-" + filterPath := "/api/v1/reqs/list" + malformedJSONTest := testFromTempl(malformedJSONTestTempl, prefix, filterPath, methods...) + + validFilter := NewRequestFilter("WAIT", "") + m.rq.EXPECT().ListRequests(validFilter).Return(reqs[:2], nil) + + emptyFilter := NewRequestFilter("", "") + m.rq.EXPECT().ListRequests(emptyFilter).Return(reqs, nil).Times(2) + m.rq.EXPECT().ListRequests(nil).Return(reqs, nil).Times(3) + + missingFilter := NewRequestFilter("INVALID", "") + m.rq.EXPECT().ListRequests(missingFilter).Return([]ReqInfo{}, nil) + + // Currently ListRequests doesn't return any error hence the meaningless values. + badFilter := NewRequestFilter("FAIL", "-1") + m.rq.EXPECT().ListRequests(badFilter).Return([]ReqInfo{}, errors.New("foo bar: pizza failed")) + tests := []requestTest{ + // Valid filter - list some requests. + { + name: prefix + "valid-filter", + path: filterPath, + methods: methods, + json: string(jsonMustMarshal(validFilter)), + contentType: contentTypeJSON, + status: http.StatusOK, + }, + // List all requests. { name: "list-reqs-all", path: "/api/v1/reqs/", methods: []string{http.MethodGet, http.MethodHead}, json: ``, contentType: contentTypeJSON, - status: http.StatusNotImplemented, + status: http.StatusOK, }, + // Empty body - list all requests. { - name: "list-reqs-filter", - path: "/api/v1/reqs/list", - methods: []string{http.MethodPost}, - json: ``, + name: prefix + "empty-json", + path: filterPath, + methods: methods, + json: "", contentType: contentTypeJSON, - status: http.StatusNotImplemented, + status: http.StatusOK, }, + // Nil filter - list all requests (same as emptyFilter). + { + name: prefix + "nil", + path: filterPath, + methods: methods, + json: string(jsonMustMarshal(nil)), + contentType: contentTypeJSON, + status: http.StatusOK, + }, + // Empty filter - list all requests. + { + name: prefix + "empty", + path: filterPath, + methods: methods, + json: string(jsonMustMarshal(emptyFilter)), + contentType: contentTypeJSON, + status: http.StatusOK, + }, + // No matches + { + name: prefix + "nomatch-all", + path: filterPath, + methods: methods, + json: string(jsonMustMarshal(missingFilter)), + contentType: contentTypeJSON, + status: http.StatusOK, + }, + // Error + { + name: prefix + "bad-filter", + path: filterPath, + methods: methods, + json: string(jsonMustMarshal(badFilter)), + contentType: contentTypeJSON, + status: http.StatusBadRequest, + }, + malformedJSONTest, } runTests(assert, api, tests) diff --git a/server/api/v1/testdata/filter-reqs-bad-filter-POST.json b/server/api/v1/testdata/filter-reqs-bad-filter-POST.json new file mode 100644 index 0000000..f6dd9fd --- /dev/null +++ b/server/api/v1/testdata/filter-reqs-bad-filter-POST.json @@ -0,0 +1 @@ +{"error":"invalid request: foo bar: pizza failed"} \ No newline at end of file diff --git a/server/api/v1/testdata/filter-reqs-empty-POST.json b/server/api/v1/testdata/filter-reqs-empty-POST.json new file mode 100644 index 0000000..1879046 --- /dev/null +++ b/server/api/v1/testdata/filter-reqs-empty-POST.json @@ -0,0 +1 @@ +[{"ID":1,"Priority":8,"Owner":{"Groups":null},"Deadline":"2222-12-31T00:00:00Z","ValidAfter":"1683-09-12T00:00:00Z","State":"WAITING","Job":null,"Caps":null},{"ID":2,"Priority":9,"Owner":{"Groups":null},"Deadline":"2222-12-31T00:00:00Z","ValidAfter":"1683-09-12T00:00:00Z","State":"WAITING","Job":null,"Caps":null},{"ID":3,"Priority":8,"Owner":{"Groups":null},"Deadline":"2222-12-31T00:00:00Z","ValidAfter":"1683-09-12T00:00:00Z","State":"CANCELLED","Job":null,"Caps":null},{"ID":4,"Priority":9,"Owner":{"Groups":null},"Deadline":"2222-12-31T00:00:00Z","ValidAfter":"1683-09-12T00:00:00Z","State":"CANCELLED","Job":null,"Caps":null}] \ No newline at end of file diff --git a/server/api/v1/testdata/filter-reqs-empty-json-POST.json b/server/api/v1/testdata/filter-reqs-empty-json-POST.json new file mode 100644 index 0000000..1879046 --- /dev/null +++ b/server/api/v1/testdata/filter-reqs-empty-json-POST.json @@ -0,0 +1 @@ +[{"ID":1,"Priority":8,"Owner":{"Groups":null},"Deadline":"2222-12-31T00:00:00Z","ValidAfter":"1683-09-12T00:00:00Z","State":"WAITING","Job":null,"Caps":null},{"ID":2,"Priority":9,"Owner":{"Groups":null},"Deadline":"2222-12-31T00:00:00Z","ValidAfter":"1683-09-12T00:00:00Z","State":"WAITING","Job":null,"Caps":null},{"ID":3,"Priority":8,"Owner":{"Groups":null},"Deadline":"2222-12-31T00:00:00Z","ValidAfter":"1683-09-12T00:00:00Z","State":"CANCELLED","Job":null,"Caps":null},{"ID":4,"Priority":9,"Owner":{"Groups":null},"Deadline":"2222-12-31T00:00:00Z","ValidAfter":"1683-09-12T00:00:00Z","State":"CANCELLED","Job":null,"Caps":null}] \ No newline at end of file diff --git a/server/api/v1/testdata/filter-reqs-malformed-json-POST.json b/server/api/v1/testdata/filter-reqs-malformed-json-POST.json new file mode 100644 index 0000000..c59dde1 --- /dev/null +++ b/server/api/v1/testdata/filter-reqs-malformed-json-POST.json @@ -0,0 +1 @@ +{"error":"invalid request: unexpected EOF"} \ No newline at end of file diff --git a/server/api/v1/testdata/filter-reqs-nil-POST.json b/server/api/v1/testdata/filter-reqs-nil-POST.json new file mode 100644 index 0000000..1879046 --- /dev/null +++ b/server/api/v1/testdata/filter-reqs-nil-POST.json @@ -0,0 +1 @@ +[{"ID":1,"Priority":8,"Owner":{"Groups":null},"Deadline":"2222-12-31T00:00:00Z","ValidAfter":"1683-09-12T00:00:00Z","State":"WAITING","Job":null,"Caps":null},{"ID":2,"Priority":9,"Owner":{"Groups":null},"Deadline":"2222-12-31T00:00:00Z","ValidAfter":"1683-09-12T00:00:00Z","State":"WAITING","Job":null,"Caps":null},{"ID":3,"Priority":8,"Owner":{"Groups":null},"Deadline":"2222-12-31T00:00:00Z","ValidAfter":"1683-09-12T00:00:00Z","State":"CANCELLED","Job":null,"Caps":null},{"ID":4,"Priority":9,"Owner":{"Groups":null},"Deadline":"2222-12-31T00:00:00Z","ValidAfter":"1683-09-12T00:00:00Z","State":"CANCELLED","Job":null,"Caps":null}] \ No newline at end of file diff --git a/server/api/v1/testdata/filter-reqs-nomatch-all-POST.json b/server/api/v1/testdata/filter-reqs-nomatch-all-POST.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/server/api/v1/testdata/filter-reqs-nomatch-all-POST.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/server/api/v1/testdata/filter-reqs-valid-filter-POST.json b/server/api/v1/testdata/filter-reqs-valid-filter-POST.json new file mode 100644 index 0000000..a168045 --- /dev/null +++ b/server/api/v1/testdata/filter-reqs-valid-filter-POST.json @@ -0,0 +1 @@ +[{"ID":1,"Priority":8,"Owner":{"Groups":null},"Deadline":"2222-12-31T00:00:00Z","ValidAfter":"1683-09-12T00:00:00Z","State":"WAITING","Job":null,"Caps":null},{"ID":2,"Priority":9,"Owner":{"Groups":null},"Deadline":"2222-12-31T00:00:00Z","ValidAfter":"1683-09-12T00:00:00Z","State":"WAITING","Job":null,"Caps":null}] \ No newline at end of file diff --git a/server/api/v1/testdata/list-reqs-all-GET.json b/server/api/v1/testdata/list-reqs-all-GET.json index ed5be09..1879046 100644 --- a/server/api/v1/testdata/list-reqs-all-GET.json +++ b/server/api/v1/testdata/list-reqs-all-GET.json @@ -1 +1 @@ -{"error":"not implemented yet: list requests"} \ No newline at end of file +[{"ID":1,"Priority":8,"Owner":{"Groups":null},"Deadline":"2222-12-31T00:00:00Z","ValidAfter":"1683-09-12T00:00:00Z","State":"WAITING","Job":null,"Caps":null},{"ID":2,"Priority":9,"Owner":{"Groups":null},"Deadline":"2222-12-31T00:00:00Z","ValidAfter":"1683-09-12T00:00:00Z","State":"WAITING","Job":null,"Caps":null},{"ID":3,"Priority":8,"Owner":{"Groups":null},"Deadline":"2222-12-31T00:00:00Z","ValidAfter":"1683-09-12T00:00:00Z","State":"CANCELLED","Job":null,"Caps":null},{"ID":4,"Priority":9,"Owner":{"Groups":null},"Deadline":"2222-12-31T00:00:00Z","ValidAfter":"1683-09-12T00:00:00Z","State":"CANCELLED","Job":null,"Caps":null}] \ No newline at end of file diff --git a/server/api/v1/testdata/list-reqs-filter-POST.json b/server/api/v1/testdata/list-reqs-filter-POST.json deleted file mode 100644 index ed5be09..0000000 --- a/server/api/v1/testdata/list-reqs-filter-POST.json +++ /dev/null @@ -1 +0,0 @@ -{"error":"not implemented yet: list requests"} \ No newline at end of file -- 2.7.4