feat(http): add links to bucket service response structures

pull/10616/head
Michael Desa 2018-09-10 15:26:08 -04:00
parent 5a35d04386
commit d6098882f9
3 changed files with 653 additions and 13 deletions

View File

@ -4,11 +4,13 @@ import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"path"
"strings"
"github.com/influxdata/platform"
kerrors "github.com/influxdata/platform/kit/errors"
errors "github.com/influxdata/platform/kit/errors"
"github.com/julienschmidt/httprouter"
)
@ -33,6 +35,40 @@ func NewBucketHandler() *BucketHandler {
return h
}
type bucketResponse struct {
Links map[string]string `json:"links"`
platform.Bucket
}
func newBucketResponse(b *platform.Bucket) *bucketResponse {
return &bucketResponse{
Links: map[string]string{
"self": fmt.Sprintf("/v1/buckets/%s", b.ID),
"org": fmt.Sprintf("/v1/orgs/%s", b.OrganizationID),
},
Bucket: *b,
}
}
type bucketsResponse struct {
Links map[string]string `json:"links"`
Buckets []*bucketResponse `json:"buckets"`
}
func newBucketsResponse(opts platform.FindOptions, f platform.BucketFilter, bs []*platform.Bucket) *bucketsResponse {
rs := make([]*bucketResponse, 0, len(bs))
for _, b := range bs {
rs = append(rs, newBucketResponse(b))
}
return &bucketsResponse{
// TODO(desa): update links to include paging and filter information
Links: map[string]string{
"self": "/v1/buckets",
},
Buckets: rs,
}
}
// handlePostBucket is the HTTP handler for the POST /v1/buckets route.
func (h *BucketHandler) handlePostBucket(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
@ -48,7 +84,7 @@ func (h *BucketHandler) handlePostBucket(w http.ResponseWriter, r *http.Request)
return
}
if err := encodeResponse(ctx, w, http.StatusCreated, req.Bucket); err != nil {
if err := encodeResponse(ctx, w, http.StatusCreated, newBucketResponse(req.Bucket)); err != nil {
EncodeError(ctx, err, w)
return
}
@ -82,11 +118,15 @@ func (h *BucketHandler) handleGetBucket(w http.ResponseWriter, r *http.Request)
b, err := h.BucketService.FindBucketByID(ctx, req.BucketID)
if err != nil {
// TODO(desa): fix this when using real errors library
if strings.Contains(err.Error(), "not found") {
err = errors.New(err.Error(), errors.NotFound)
}
EncodeError(ctx, err, w)
return
}
if err := encodeResponse(ctx, w, http.StatusOK, b); err != nil {
if err := encodeResponse(ctx, w, http.StatusOK, newBucketResponse(b)); err != nil {
EncodeError(ctx, err, w)
return
}
@ -100,7 +140,7 @@ func decodeGetBucketRequest(ctx context.Context, r *http.Request) (*getBucketReq
params := httprouter.ParamsFromContext(ctx)
id := params.ByName("id")
if id == "" {
return nil, kerrors.InvalidDataf("url missing id")
return nil, errors.InvalidDataf("url missing id")
}
var i platform.ID
@ -125,11 +165,15 @@ func (h *BucketHandler) handleDeleteBucket(w http.ResponseWriter, r *http.Reques
}
if err := h.BucketService.DeleteBucket(ctx, req.BucketID); err != nil {
// TODO(desa): fix this when using real errors library
if strings.Contains(err.Error(), "not found") {
err = errors.New(err.Error(), errors.NotFound)
}
EncodeError(ctx, err, w)
return
}
w.WriteHeader(http.StatusAccepted)
w.WriteHeader(http.StatusNoContent)
}
type deleteBucketRequest struct {
@ -140,7 +184,7 @@ func decodeDeleteBucketRequest(ctx context.Context, r *http.Request) (*deleteBuc
params := httprouter.ParamsFromContext(ctx)
id := params.ByName("id")
if id == "" {
return nil, kerrors.InvalidDataf("url missing id")
return nil, errors.InvalidDataf("url missing id")
}
var i platform.ID
@ -164,13 +208,14 @@ func (h *BucketHandler) handleGetBuckets(w http.ResponseWriter, r *http.Request)
return
}
bs, _, err := h.BucketService.FindBuckets(ctx, req.filter)
opts := platform.FindOptions{}
bs, _, err := h.BucketService.FindBuckets(ctx, req.filter, opts)
if err != nil {
EncodeError(ctx, err, w)
return
}
if err := encodeResponse(ctx, w, http.StatusOK, bs); err != nil {
if err := encodeResponse(ctx, w, http.StatusOK, newBucketsResponse(opts, req.filter, bs)); err != nil {
EncodeError(ctx, err, w)
return
}
@ -221,11 +266,15 @@ func (h *BucketHandler) handlePatchBucket(w http.ResponseWriter, r *http.Request
b, err := h.BucketService.UpdateBucket(ctx, req.BucketID, req.Update)
if err != nil {
// TODO(desa): fix this when using real errors library
if strings.Contains(err.Error(), "not found") {
err = errors.New(err.Error(), errors.NotFound)
}
EncodeError(ctx, err, w)
return
}
if err := encodeResponse(ctx, w, http.StatusOK, b); err != nil {
if err := encodeResponse(ctx, w, http.StatusOK, newBucketResponse(b)); err != nil {
EncodeError(ctx, err, w)
return
}
@ -240,7 +289,7 @@ func decodePatchBucketRequest(ctx context.Context, r *http.Request) (*patchBucke
params := httprouter.ParamsFromContext(ctx)
id := params.ByName("id")
if id == "" {
return nil, kerrors.InvalidDataf("url missing id")
return nil, errors.InvalidDataf("url missing id")
}
var i platform.ID
@ -362,13 +411,18 @@ func (s *BucketService) FindBuckets(ctx context.Context, filter platform.BucketF
return nil, 0, err
}
var bs []*platform.Bucket
var bs bucketsResponse
if err := json.NewDecoder(resp.Body).Decode(&bs); err != nil {
return nil, 0, err
}
defer resp.Body.Close()
return bs, len(bs), nil
buckets := make([]*platform.Bucket, 0, len(bs.Buckets))
for _, b := range bs.Buckets {
buckets = append(buckets, &b.Bucket)
}
return buckets, len(buckets), nil
}
// CreateBucket creates a new bucket and sets b.ID with the new identifier.

586
http/bucket_test.go Normal file
View File

@ -0,0 +1,586 @@
package http
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/influxdata/platform"
"github.com/influxdata/platform/mock"
"github.com/julienschmidt/httprouter"
)
func TestService_handleGetBuckets(t *testing.T) {
type fields struct {
BucketService platform.BucketService
}
type args struct {
queryParams map[string][]string
}
type wants struct {
statusCode int
contentType string
body string
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "get all buckets",
fields: fields{
&mock.BucketService{
FindBucketsFn: func(ctx context.Context, filter platform.BucketFilter, opts ...platform.FindOptions) ([]*platform.Bucket, int, error) {
return []*platform.Bucket{
{
ID: platform.ID("0"),
Name: "hello",
OrganizationID: platform.ID("10"),
},
{
ID: platform.ID("2"),
Name: "example",
OrganizationID: platform.ID("20"),
},
}, 2, nil
},
},
},
args: args{},
wants: wants{
statusCode: http.StatusOK,
contentType: "application/json; charset=utf-8",
body: `
{
"links": {
"self": "/v1/buckets"
},
"buckets": [
{
"links": {
"org": "/v1/orgs/3130",
"self": "/v1/buckets/30"
},
"id": "30",
"organizationID": "3130",
"name": "hello",
"retentionPeriod": 0
},
{
"links": {
"org": "/v1/orgs/3230",
"self": "/v1/buckets/32"
},
"id": "32",
"organizationID": "3230",
"name": "example",
"retentionPeriod": 0
}
]
}
`,
},
},
{
name: "get all buckets when there are none",
fields: fields{
&mock.BucketService{
FindBucketsFn: func(ctx context.Context, filter platform.BucketFilter, opts ...platform.FindOptions) ([]*platform.Bucket, int, error) {
return []*platform.Bucket{}, 0, nil
},
},
},
args: args{},
wants: wants{
statusCode: http.StatusOK,
contentType: "application/json; charset=utf-8",
body: `
{
"links": {
"self": "/v1/buckets"
},
"buckets": []
}`,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := NewBucketHandler()
h.BucketService = tt.fields.BucketService
r := httptest.NewRequest("GET", "http://any.url", nil)
qp := r.URL.Query()
for k, vs := range tt.args.queryParams {
for _, v := range vs {
qp.Add(k, v)
}
}
r.URL.RawQuery = qp.Encode()
w := httptest.NewRecorder()
h.handleGetBuckets(w, r)
res := w.Result()
content := res.Header.Get("Content-Type")
body, _ := ioutil.ReadAll(res.Body)
if res.StatusCode != tt.wants.statusCode {
t.Errorf("%q. handleGetBuckets() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode)
}
if tt.wants.contentType != "" && content != tt.wants.contentType {
t.Errorf("%q. handleGetBuckets() = %v, want %v", tt.name, content, tt.wants.contentType)
}
if eq, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq {
t.Errorf("%q. handleGetBuckets() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wants.body)
}
})
}
}
func TestService_handleGetBucket(t *testing.T) {
type fields struct {
BucketService platform.BucketService
}
type args struct {
id string
}
type wants struct {
statusCode int
contentType string
body string
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "get a bucket by id",
fields: fields{
&mock.BucketService{
FindBucketByIDFn: func(ctx context.Context, id platform.ID) (*platform.Bucket, error) {
if bytes.Equal(id, mustParseID("020f755c3c082000")) {
return &platform.Bucket{
ID: mustParseID("020f755c3c082000"),
OrganizationID: mustParseID("020f755c3c082000"),
Name: "hello",
}, nil
}
return nil, fmt.Errorf("not found")
},
},
},
args: args{
id: "020f755c3c082000",
},
wants: wants{
statusCode: http.StatusOK,
contentType: "application/json; charset=utf-8",
body: `
{
"links": {
"org": "/v1/orgs/020f755c3c082000",
"self": "/v1/buckets/020f755c3c082000"
},
"id": "020f755c3c082000",
"organizationID": "020f755c3c082000",
"name": "hello",
"retentionPeriod": 0
}
`,
},
},
{
name: "not found",
fields: fields{
&mock.BucketService{
FindBucketByIDFn: func(ctx context.Context, id platform.ID) (*platform.Bucket, error) {
return nil, fmt.Errorf("bucket not found")
},
},
},
args: args{
id: "020f755c3c082000",
},
wants: wants{
statusCode: http.StatusNotFound,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := NewBucketHandler()
h.BucketService = tt.fields.BucketService
r := httptest.NewRequest("GET", "http://any.url", nil)
r = r.WithContext(context.WithValue(
context.TODO(),
httprouter.ParamsKey,
httprouter.Params{
{
Key: "id",
Value: tt.args.id,
},
}))
w := httptest.NewRecorder()
h.handleGetBucket(w, r)
res := w.Result()
content := res.Header.Get("Content-Type")
body, _ := ioutil.ReadAll(res.Body)
t.Logf(res.Header.Get("X-Influx-Error"))
if res.StatusCode != tt.wants.statusCode {
t.Errorf("%q. handleGetBucket() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode)
}
if tt.wants.contentType != "" && content != tt.wants.contentType {
t.Errorf("%q. handleGetBucket() = %v, want %v", tt.name, content, tt.wants.contentType)
}
if eq, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq {
t.Errorf("%q. handleGetBucket() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wants.body)
}
})
}
}
func TestService_handlePostBucket(t *testing.T) {
type fields struct {
BucketService platform.BucketService
}
type args struct {
bucket *platform.Bucket
}
type wants struct {
statusCode int
contentType string
body string
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "create a new bucket",
fields: fields{
&mock.BucketService{
CreateBucketFn: func(ctx context.Context, c *platform.Bucket) error {
c.ID = mustParseID("020f755c3c082000")
return nil
},
},
},
args: args{
bucket: &platform.Bucket{
Name: "hello",
OrganizationID: platform.ID("0"),
},
},
wants: wants{
statusCode: http.StatusCreated,
contentType: "application/json; charset=utf-8",
body: `
{
"links": {
"org": "/v1/orgs/30",
"self": "/v1/buckets/020f755c3c082000"
},
"id": "020f755c3c082000",
"organizationID": "30",
"name": "hello",
"retentionPeriod": 0
}
`,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := NewBucketHandler()
h.BucketService = tt.fields.BucketService
b, err := json.Marshal(tt.args.bucket)
if err != nil {
t.Fatalf("failed to unmarshal bucket: %v", err)
}
r := httptest.NewRequest("GET", "http://any.url", bytes.NewReader(b))
w := httptest.NewRecorder()
h.handlePostBucket(w, r)
res := w.Result()
content := res.Header.Get("Content-Type")
body, _ := ioutil.ReadAll(res.Body)
if res.StatusCode != tt.wants.statusCode {
t.Errorf("%q. handlePostBucket() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode)
}
if tt.wants.contentType != "" && content != tt.wants.contentType {
t.Errorf("%q. handlePostBucket() = %v, want %v", tt.name, content, tt.wants.contentType)
}
if eq, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq {
t.Errorf("%q. handlePostBucket() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wants.body)
}
})
}
}
func TestService_handleDeleteBucket(t *testing.T) {
type fields struct {
BucketService platform.BucketService
}
type args struct {
id string
}
type wants struct {
statusCode int
contentType string
body string
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "remove a bucket by id",
fields: fields{
&mock.BucketService{
DeleteBucketFn: func(ctx context.Context, id platform.ID) error {
if bytes.Equal(id, mustParseID("020f755c3c082000")) {
return nil
}
return fmt.Errorf("wrong id")
},
},
},
args: args{
id: "020f755c3c082000",
},
wants: wants{
statusCode: http.StatusNoContent,
},
},
{
name: "bucket not found",
fields: fields{
&mock.BucketService{
DeleteBucketFn: func(ctx context.Context, id platform.ID) error {
return fmt.Errorf("bucket not found")
},
},
},
args: args{
id: "020f755c3c082000",
},
wants: wants{
statusCode: http.StatusNotFound,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := NewBucketHandler()
h.BucketService = tt.fields.BucketService
r := httptest.NewRequest("GET", "http://any.url", nil)
r = r.WithContext(context.WithValue(
context.TODO(),
httprouter.ParamsKey,
httprouter.Params{
{
Key: "id",
Value: tt.args.id,
},
}))
w := httptest.NewRecorder()
h.handleDeleteBucket(w, r)
res := w.Result()
content := res.Header.Get("Content-Type")
body, _ := ioutil.ReadAll(res.Body)
if res.StatusCode != tt.wants.statusCode {
t.Errorf("%q. handleDeleteBucket() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode)
}
if tt.wants.contentType != "" && content != tt.wants.contentType {
t.Errorf("%q. handleDeleteBucket() = %v, want %v", tt.name, content, tt.wants.contentType)
}
if eq, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq {
t.Errorf("%q. handleDeleteBucket() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wants.body)
}
})
}
}
func TestService_handlePatchBucket(t *testing.T) {
type fields struct {
BucketService platform.BucketService
}
type args struct {
id string
name string
retention time.Duration
}
type wants struct {
statusCode int
contentType string
body string
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "update a bucket name and retenion",
fields: fields{
&mock.BucketService{
UpdateBucketFn: func(ctx context.Context, id platform.ID, upd platform.BucketUpdate) (*platform.Bucket, error) {
if bytes.Equal(id, mustParseID("020f755c3c082000")) {
d := &platform.Bucket{
ID: mustParseID("020f755c3c082000"),
Name: "hello",
OrganizationID: mustParseID("020f755c3c082000"),
}
if upd.Name != nil {
d.Name = *upd.Name
}
if upd.RetentionPeriod != nil {
d.RetentionPeriod = *upd.RetentionPeriod
}
return d, nil
}
return nil, fmt.Errorf("not found")
},
},
},
args: args{
id: "020f755c3c082000",
name: "example",
retention: 1234,
},
wants: wants{
statusCode: http.StatusOK,
contentType: "application/json; charset=utf-8",
body: `
{
"links": {
"org": "/v1/orgs/020f755c3c082000",
"self": "/v1/buckets/020f755c3c082000"
},
"id": "020f755c3c082000",
"organizationID": "020f755c3c082000",
"name": "example",
"retentionPeriod": 1234
}
`,
},
},
{
name: "bucket not found",
fields: fields{
&mock.BucketService{
UpdateBucketFn: func(ctx context.Context, id platform.ID, upd platform.BucketUpdate) (*platform.Bucket, error) {
return nil, fmt.Errorf("not found")
},
},
},
args: args{
id: "020f755c3c082000",
name: "hello",
},
wants: wants{
statusCode: http.StatusNotFound,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := NewBucketHandler()
h.BucketService = tt.fields.BucketService
upd := platform.BucketUpdate{}
if tt.args.name != "" {
upd.Name = &tt.args.name
}
if tt.args.retention != 0 {
upd.RetentionPeriod = &tt.args.retention
}
b, err := json.Marshal(upd)
if err != nil {
t.Fatalf("failed to unmarshal bucket update: %v", err)
}
r := httptest.NewRequest("GET", "http://any.url", bytes.NewReader(b))
r = r.WithContext(context.WithValue(
context.TODO(),
httprouter.ParamsKey,
httprouter.Params{
{
Key: "id",
Value: tt.args.id,
},
}))
w := httptest.NewRecorder()
h.handlePatchBucket(w, r)
res := w.Result()
content := res.Header.Get("Content-Type")
body, _ := ioutil.ReadAll(res.Body)
if res.StatusCode != tt.wants.statusCode {
t.Errorf("%q. handlePatchBucket() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode)
}
if tt.wants.contentType != "" && content != tt.wants.contentType {
t.Errorf("%q. handlePatchBucket() = %v, want %v", tt.name, content, tt.wants.contentType)
}
if eq, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq {
t.Errorf("%q. handlePatchBucket() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wants.body)
}
})
}
}

View File

@ -78,5 +78,5 @@ func (s *BucketService) UpdateBucket(ctx context.Context, id platform.ID, upd pl
// DeleteBucket removes a bucket by ID.
func (s *BucketService) DeleteBucket(ctx context.Context, id platform.ID) error {
return s.DeleteBucket(ctx, id)
return s.DeleteBucketFn(ctx, id)
}