├── LICENSE ├── README.md ├── couchbase.go ├── datastore.go ├── go.mod ├── go.sum ├── main.go └── main_test.go /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Jerry Zhao / codingsince1985 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Couchcache 2 | == 3 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/codingsince1985/couchcache)](https://pkg.go.dev/github.com/codingsince1985/couchcache) 4 | [![Go Report Card](https://goreportcard.com/badge/codingsince1985/couchcache)](https://goreportcard.com/report/codingsince1985/couchcache) 5 | 6 | A caching service developed in Go. It provides REST APIs to access key-value pairs stored in Couchbase. 7 | 8 | You may also consider [using couchcache as a mocked service](http://codingsince1985.blogspot.com.au/2015/05/use-caching-service-as-mocked.html) when doing TDD. 9 | 10 | To start couchcache 11 | -- 12 | Run couchcache with Couchbase server (host and port) and bucket (name and password) information 13 | 14 | `./couchcache --host=HOST --port=PORT --bucket=BUCKET --pass=PASS` 15 | #### Example 16 | `./couchcache --host=10.99.107.192 --port=8091 --bucket=cachebucket --pass=c@che1t` 17 | #### Default values 18 | ``` 19 | host: localhost 20 | port: 8091 21 | bucket: couchcache 22 | pass: password 23 | ``` 24 | Cache service endpoint 25 | -- 26 | `http://HOST:8080/key/KEY` 27 | #### Examples 28 | `http://10.99.107.190:8080/key/customer_555` 29 | 30 | `http://10.99.107.190:8080/key/the_service_i_want_to_mock-endpoint_a`, if you're mocking other service's endpoint 31 | 32 | ### To store a key-value pair 33 | * request 34 | * send `POST` request to endpoint with data in body 35 | * optionally set TTL by `?ttl=TTL_IN_SEC` 36 | * response 37 | * `HTTP 201 Created` if stored 38 | * `HTTP 400 Bad Request` if key or value is invalid 39 | 40 | ### To retrieve a key 41 | * request 42 | * send `GET` request to endpoint 43 | * response 44 | * `HTTP 200 OK` with data in body 45 | * `HTTP 404 Not Found` if key doesn't exist 46 | * `HTTP 400 Bad Request` if key is invalid 47 | 48 | ### To delete a key 49 | * request 50 | * send `DELETE` request to endpoint 51 | * response 52 | * `HTTP 204 No Content` if deleted 53 | * `HTTP 404 Not Found` is key doesn't exist 54 | * `HTTP 400 Bad Request` if key is invalid 55 | 56 | ### To append data for a key 57 | * request 58 | * send `PUT` request to endpoint with data in body 59 | * response 60 | * `HTTP 200 OK` if appended 61 | * `HTTP 404 Not Found` if key doesn't exist 62 | * `HTTP 400 Bad Request` if key or value is invalid 63 | 64 | Limitations 65 | -- 66 | * Max key length is 250 bytes 67 | * Max value size is 20 MB 68 | 69 | See [Couchbase Limits](https://docs.couchbase.com/server/current/learn/clusters-and-availability/size-limitations.html). 70 | 71 | License 72 | == 73 | couchcache is distributed under the terms of the MIT license. See LICENSE for details. 74 | -------------------------------------------------------------------------------- /couchbase.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "gopkg.in/couchbase/gocb.v1" 7 | "log" 8 | ) 9 | 10 | const ( 11 | maxTTLInSec = 60 * 60 * 24 * 30 12 | maxSizeInByte = 20 * 1024 * 1024 13 | maxKeyLength = 250 14 | ) 15 | 16 | type couchbaseDatastore gocb.Bucket 17 | 18 | func newDatastore() (ds *couchbaseDatastore, err error) { 19 | url, bucket, pass := parseFlag() 20 | 21 | if c, err := gocb.Connect(url); err == nil { 22 | if b, err := c.OpenBucket(bucket, pass); err == nil { 23 | return (*couchbaseDatastore)(b), nil 24 | } 25 | } 26 | return nil, err 27 | } 28 | 29 | func parseFlag() (string, string, string) { 30 | host := flag.String("host", "localhost", "host name (defaults to localhost)") 31 | port := flag.Int("port", 8091, "port number (defaults to 8091)") 32 | bucket := flag.String("bucket", "couchcache", "bucket name (defaults to couchcache)") 33 | pass := flag.String("pass", "password", "password (defaults to password)") 34 | 35 | flag.Parse() 36 | 37 | url := fmt.Sprintf("http://%s:%d", *host, *port) 38 | log.Println(url) 39 | return url, *bucket, *pass 40 | } 41 | 42 | func (ds *couchbaseDatastore) get(k string) []byte { 43 | var val []uint8 44 | if _, err := (*gocb.Bucket)(ds).Get(k, &val); err != nil { 45 | if err.Error() != "Key not found." { 46 | log.Println(err) 47 | } 48 | return nil 49 | } 50 | return []byte(val) 51 | } 52 | 53 | func (ds *couchbaseDatastore) set(k string, v []byte, ttl int) error { 54 | if ttl > maxTTLInSec { 55 | ttl = maxTTLInSec 56 | } else if ttl < 0 { 57 | ttl = 0 58 | } 59 | 60 | _, err := (*gocb.Bucket)(ds).Upsert(k, v, uint32(ttl)) 61 | return memdErrorToDatastoreError(err) 62 | 63 | } 64 | 65 | func (ds *couchbaseDatastore) delete(k string) error { 66 | if err := ds.validKey(k); err != nil { 67 | return errInvalidKey 68 | } 69 | 70 | _, err := (*gocb.Bucket)(ds).Remove(k, gocb.Cas(0)) 71 | return memdErrorToDatastoreError(err) 72 | } 73 | 74 | func (ds *couchbaseDatastore) append(k string, v []byte) error { 75 | if err := ds.validKey(k); err != nil { 76 | return errInvalidKey 77 | } 78 | 79 | if err := ds.validValue(v); err != nil { 80 | return err 81 | } 82 | 83 | _, err := (*gocb.Bucket)(ds).Append(k, string(v)) 84 | return memdErrorToDatastoreError(err) 85 | 86 | } 87 | 88 | func (ds *couchbaseDatastore) validKey(key string) error { 89 | if len(key) < 1 || len(key) > maxKeyLength { 90 | return errInvalidKey 91 | } 92 | return nil 93 | } 94 | 95 | func (ds *couchbaseDatastore) validValue(v []byte) error { 96 | if len(v) == 0 { 97 | log.Println("body is empty") 98 | return errEmptyBody 99 | } 100 | 101 | if len(v) > maxSizeInByte { 102 | log.Println("body is too large") 103 | return errOversizedBody 104 | } 105 | 106 | return nil 107 | } 108 | 109 | func memdErrorToDatastoreError(err error) error { 110 | if err == nil { 111 | return nil 112 | } 113 | 114 | log.Println(err.Error()) 115 | switch err.Error() { 116 | case "Key not found.": 117 | return errNotFound 118 | case "The document could not be stored.": 119 | return errNotFound 120 | case "Document value was too large.": 121 | return errOversizedBody 122 | default: 123 | log.Println(err) 124 | return err 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /datastore.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | type datastorer interface { 8 | get(k string) []byte 9 | set(k string, v []byte, ttl int) error 10 | delete(k string) error 11 | append(k string, v []byte) error 12 | validKey(k string) error 13 | validValue(v []byte) error 14 | } 15 | 16 | var ( 17 | errNotFound = errors.New("NOT_FOUND") 18 | errKeyExists = errors.New("KEY_EXISTS_ERROR") 19 | errOversizedBody = errors.New("OVERSIZED_BODY") 20 | errEmptyBody = errors.New("EMPTY_BODY") 21 | errInvalidKey = errors.New("INVALID_KEY") 22 | errInvalidBody = errors.New("INVALID_BODY") 23 | ) 24 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/codingsince1985/couchcache 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/gorilla/mux v1.8.0 7 | gopkg.in/couchbase/gocb.v1 v1.6.7 8 | ) 9 | 10 | require ( 11 | github.com/golang/snappy v0.0.4 // indirect 12 | github.com/google/uuid v1.3.0 // indirect 13 | github.com/opentracing/opentracing-go v1.2.0 // indirect 14 | github.com/pkg/errors v0.9.1 // indirect 15 | golang.org/x/net v0.4.0 // indirect 16 | golang.org/x/sync v0.1.0 // indirect 17 | golang.org/x/text v0.5.0 // indirect 18 | gopkg.in/couchbase/gocbcore.v7 v7.1.18 // indirect 19 | gopkg.in/couchbaselabs/gocbconnstr.v1 v1.0.4 // indirect 20 | gopkg.in/couchbaselabs/gojcbmock.v1 v1.0.4 // indirect 21 | gopkg.in/couchbaselabs/jsonx.v1 v1.0.1 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 4 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 5 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 6 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 7 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= 8 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 9 | github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= 10 | github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= 11 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 12 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 13 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 14 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 15 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 16 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 17 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 18 | golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= 19 | golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= 20 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= 21 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 22 | golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= 23 | golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 24 | gopkg.in/couchbase/gocb.v1 v1.6.7 h1:Za2KhMBdo00+CKg4C09QetVziU8/N4YmQNwaPQqZWPg= 25 | gopkg.in/couchbase/gocb.v1 v1.6.7/go.mod h1:Ri5Qok4ZKiwmPr75YxZ0uELQy45XJgUSzeUnK806gTY= 26 | gopkg.in/couchbase/gocbcore.v7 v7.1.18 h1:d4yfIXWdf/ZmyuJjwRVVlGT/yqx8ICy6fcT/ViaMZsI= 27 | gopkg.in/couchbase/gocbcore.v7 v7.1.18/go.mod h1:48d2Be0MxRtsyuvn+mWzqmoGUG9uA00ghopzOs148/E= 28 | gopkg.in/couchbaselabs/gocbconnstr.v1 v1.0.4 h1:VVVoIV/nSw1w9ZnTEOjmkeJVcAzaCyxEujKglarxz7U= 29 | gopkg.in/couchbaselabs/gocbconnstr.v1 v1.0.4/go.mod h1:ZjII0iKx4Veo6N6da+pEZu/ptNyKLg9QTVt7fFmR6sw= 30 | gopkg.in/couchbaselabs/gojcbmock.v1 v1.0.4 h1:r5WoWGyeTJQiNGsoWAsMJfz0JFF14xc2TJrYSs09VXk= 31 | gopkg.in/couchbaselabs/gojcbmock.v1 v1.0.4/go.mod h1:jl/gd/aQ2S8whKVSTnsPs6n7BPeaAuw9UglBD/OF7eo= 32 | gopkg.in/couchbaselabs/jsonx.v1 v1.0.1 h1:giDAdTGcyXUuY+uFCWeJ2foukiqMTYl4ORSxCi/ybcc= 33 | gopkg.in/couchbaselabs/jsonx.v1 v1.0.1/go.mod h1:oR201IRovxvLW/eISevH12/+MiKHtNQAKfcX8iWZvJY= 34 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/gorilla/mux" 5 | "io/ioutil" 6 | "log" 7 | "math" 8 | "net/http" 9 | "strconv" 10 | "time" 11 | ) 12 | 13 | var ds datastorer 14 | 15 | var timeout = time.Millisecond * 100 16 | 17 | func main() { 18 | if d, err := newDatastore(); err != nil { 19 | log.Fatalln(err) 20 | } else { 21 | ds = datastorer(d) 22 | } 23 | 24 | r := mux.NewRouter() 25 | kr := r.PathPrefix("/key/{key}").Subrouter() 26 | kr.Methods("GET").HandlerFunc(getHandler) 27 | kr.Methods("POST").HandlerFunc(postHandler) 28 | kr.Methods("DELETE").HandlerFunc(deleteHandler) 29 | kr.Methods("PUT").HandlerFunc(putHandler) 30 | 31 | if err := http.ListenAndServe(":8080", r); err != nil { 32 | log.Fatal("ListenAndServe: ", err) 33 | } 34 | } 35 | 36 | func getHandler(w http.ResponseWriter, r *http.Request) { 37 | t0 := time.Now().UnixNano() 38 | k := mux.Vars(r)["key"] 39 | 40 | if err := ds.validKey(k); err != nil { 41 | http.Error(w, k+": invalid key", http.StatusBadRequest) 42 | return 43 | } 44 | 45 | ch := make(chan []byte, 1) 46 | go func() { 47 | ch <- ds.get(k) 48 | }() 49 | 50 | select { 51 | case v := <-ch: 52 | if v != nil { 53 | log.Println("get ["+k+"] in", timeSpent(t0), "ms") 54 | w.Write(v) 55 | } else { 56 | log.Println(k + ": not found") 57 | http.Error(w, k+": not found", http.StatusNotFound) 58 | } 59 | case <-time.After(timeout): 60 | returnTimeout(w, k) 61 | } 62 | } 63 | 64 | func postHandler(w http.ResponseWriter, r *http.Request) { 65 | t0 := time.Now().UnixNano() 66 | k := mux.Vars(r)["key"] 67 | ttl, _ := strconv.Atoi(r.FormValue("ttl")) 68 | 69 | if v, err := ioutil.ReadAll(r.Body); err != nil { 70 | http.Error(w, k+": can't get value", http.StatusBadRequest) 71 | return 72 | } else { 73 | if err = ds.validKey(k); err == nil { 74 | if err = ds.validValue(v); err == nil { 75 | go func() { 76 | ds.set(k, v, ttl) 77 | }() 78 | 79 | log.Println("set ["+k+"] in", timeSpent(t0), "ms") 80 | w.WriteHeader(http.StatusCreated) 81 | return 82 | } 83 | } 84 | datastoreErrorToHTTPError(err, w) 85 | } 86 | } 87 | 88 | func deleteHandler(w http.ResponseWriter, r *http.Request) { 89 | t0 := time.Now().UnixNano() 90 | k := mux.Vars(r)["key"] 91 | 92 | if err := ds.delete(k); err == nil { 93 | log.Println("delete ["+k+"] in", timeSpent(t0), "ms") 94 | w.WriteHeader(http.StatusNoContent) 95 | } else { 96 | datastoreErrorToHTTPError(err, w) 97 | } 98 | } 99 | 100 | func putHandler(w http.ResponseWriter, r *http.Request) { 101 | t0 := time.Now().UnixNano() 102 | k := mux.Vars(r)["key"] 103 | 104 | if v, err := ioutil.ReadAll(r.Body); err != nil { 105 | http.Error(w, k+": can't get value", http.StatusBadRequest) 106 | return 107 | } else { 108 | if err = ds.append(k, v); err == nil { 109 | log.Println("append ["+k+"] in", timeSpent(t0), "ms") 110 | w.WriteHeader(http.StatusOK) 111 | } else { 112 | datastoreErrorToHTTPError(err, w) 113 | } 114 | } 115 | } 116 | 117 | func returnTimeout(w http.ResponseWriter, k string) { 118 | log.Println(k + ": timeout") 119 | http.Error(w, k+": timeout", http.StatusRequestTimeout) 120 | } 121 | 122 | func timeSpent(t0 int64) int64 { 123 | return int64(math.Floor(float64(time.Now().UnixNano()-t0)/1000000 + .5)) 124 | } 125 | 126 | func datastoreErrorToHTTPError(err error, w http.ResponseWriter) { 127 | switch err { 128 | case errNotFound: 129 | http.Error(w, "key not found", http.StatusNotFound) 130 | case errEmptyBody: 131 | http.Error(w, "empty value", http.StatusBadRequest) 132 | case errOversizedBody: 133 | http.Error(w, "oversized value", http.StatusBadRequest) 134 | case errInvalidKey: 135 | http.Error(w, "invalid key", http.StatusBadRequest) 136 | case errKeyExists: 137 | http.Error(w, "key exists", http.StatusBadRequest) 138 | default: 139 | http.Error(w, "cache server error", http.StatusInternalServerError) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main_test 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "net/http" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | const ( 12 | url = "http://localhost:8080/key/test_key" 13 | 14 | statusNotFound = "404 Not Found" 15 | statusBadRequest = "400 Bad Request" 16 | statusCreated = "201 Created" 17 | statusOK = "200 OK" 18 | statusNoContent = "204 No Content" 19 | 20 | get = "GET" 21 | post = "POST" 22 | put = "PUT" 23 | delete = "DELETE" 24 | ) 25 | 26 | func TestGetNotExistingKey(t *testing.T) { 27 | client := &http.Client{} 28 | req, err := http.NewRequest(get, url+"_temp", bytes.NewBufferString("")) 29 | resp, err := client.Do(req) 30 | defer resp.Body.Close() 31 | if err != nil || resp == nil || resp.Status != statusNotFound { 32 | t.Error("TestGetNotExistingKey() failed", err, resp) 33 | } 34 | } 35 | 36 | func TestStoreEmptyValue(t *testing.T) { 37 | client := &http.Client{} 38 | req, err := http.NewRequest(post, url, bytes.NewBufferString("")) 39 | resp, err := client.Do(req) 40 | defer resp.Body.Close() 41 | if err != nil || resp == nil || resp.Status != statusBadRequest { 42 | t.Error("TestStoreEmptyValue() failed", err, resp) 43 | } 44 | } 45 | 46 | func TestAppendEmptyValue(t *testing.T) { 47 | client := &http.Client{} 48 | req, err := http.NewRequest(post, url+"?ttl=1", bytes.NewBufferString("hello")) 49 | resp, err := client.Do(req) 50 | if err != nil || resp == nil || resp.Status != statusCreated { 51 | t.Error("TestAppendEmptyValue() failed", err, resp) 52 | } 53 | resp.Body.Close() 54 | 55 | req, err = http.NewRequest(put, url, bytes.NewBufferString("")) 56 | resp, err = client.Do(req) 57 | defer resp.Body.Close() 58 | if err != nil || resp == nil || resp.Status != statusBadRequest { 59 | t.Error("TestAppendEmptyValue() failed", err, resp) 60 | } 61 | } 62 | 63 | func TestAppendNotExistingKey(t *testing.T) { 64 | client := &http.Client{} 65 | req, err := http.NewRequest(put, url+"_temp", bytes.NewBufferString(" world")) 66 | resp, err := client.Do(req) 67 | defer resp.Body.Close() 68 | if err != nil || resp == nil || resp.Status != statusNotFound { 69 | t.Error("TestAppendNotExistingKey() failed", err, resp) 70 | } 71 | } 72 | 73 | func TestDeleteNotExistingKey(t *testing.T) { 74 | client := &http.Client{} 75 | req, err := http.NewRequest(delete, url+"_temp", bytes.NewBufferString("")) 76 | resp, err := client.Do(req) 77 | defer resp.Body.Close() 78 | if err != nil || resp == nil || resp.Status != statusNotFound { 79 | t.Error("TestDeleteNotExistingKey() failed", err, resp) 80 | } 81 | } 82 | 83 | func TestStoreAndGet(t *testing.T) { 84 | client := &http.Client{} 85 | req, err := http.NewRequest(post, url+"?ttl=1", bytes.NewBufferString("hello")) 86 | resp, err := client.Do(req) 87 | if err != nil || resp == nil || resp.Status != statusCreated { 88 | t.Error("TestStoreAndGet() failed", err, resp) 89 | } 90 | resp.Body.Close() 91 | 92 | req, err = http.NewRequest(get, url, bytes.NewBufferString("")) 93 | resp, err = client.Do(req) 94 | defer resp.Body.Close() 95 | if err != nil || resp == nil || resp.Status != statusOK { 96 | t.Error("TestStoreAndGet() failed", err, resp) 97 | } 98 | data, err := ioutil.ReadAll(resp.Body) 99 | if err != nil || data == nil || string(data) != "hello" { 100 | t.Error("TestStoreAndGet() failed", err, resp) 101 | } 102 | } 103 | 104 | func TestStoreAndDelete(t *testing.T) { 105 | client := &http.Client{} 106 | req, err := http.NewRequest(post, url+"?ttl=1", bytes.NewBufferString("hello")) 107 | resp, err := client.Do(req) 108 | if err != nil || resp == nil || resp.Status != statusCreated { 109 | t.Error("TestStoreAndDelete() failed", err, resp) 110 | } 111 | resp.Body.Close() 112 | 113 | req, err = http.NewRequest(delete, url, bytes.NewBufferString("")) 114 | resp, err = client.Do(req) 115 | if err != nil || resp == nil || resp.Status != statusNoContent { 116 | t.Error("TestStoreAndDelete() failed", err, resp) 117 | } 118 | resp.Body.Close() 119 | 120 | req, err = http.NewRequest(get, url, bytes.NewBufferString("")) 121 | resp, err = client.Do(req) 122 | defer resp.Body.Close() 123 | if err != nil || resp == nil || resp.Status != statusNotFound { 124 | t.Error("TestStoreAndDelete() failed", err, resp) 125 | } 126 | } 127 | 128 | func TestStoreAndAppend(t *testing.T) { 129 | client := &http.Client{} 130 | req, err := http.NewRequest(post, url+"?ttl=1", bytes.NewBufferString("hello")) 131 | resp, err := client.Do(req) 132 | if err != nil || resp == nil || resp.Status != statusCreated { 133 | t.Error("TestStoreAndAppend() failed", err, resp) 134 | } 135 | resp.Body.Close() 136 | 137 | req, err = http.NewRequest(put, url, bytes.NewBufferString(" world")) 138 | resp, err = client.Do(req) 139 | if err != nil || resp == nil || resp.Status != statusOK { 140 | t.Error("TestStoreAndAppend() failed", err, resp) 141 | } 142 | resp.Body.Close() 143 | 144 | req, err = http.NewRequest(get, url, bytes.NewBufferString("")) 145 | resp, err = client.Do(req) 146 | defer resp.Body.Close() 147 | if err != nil || resp == nil || resp.Status != statusOK { 148 | t.Error("TestStoreAndAppend() failed", err, resp) 149 | } 150 | data, err := ioutil.ReadAll(resp.Body) 151 | if err != nil || data == nil || string(data) != "hello world" { 152 | t.Error("TestStoreAndAppend() failed", err, resp) 153 | } 154 | } 155 | 156 | func TestTTL(t *testing.T) { 157 | client := &http.Client{} 158 | req, err := http.NewRequest(post, url+"?ttl=1", bytes.NewBufferString("hello")) 159 | resp, err := client.Do(req) 160 | if err != nil || resp == nil || resp.Status != statusCreated { 161 | t.Error("TestTTL() failed", err, resp) 162 | } 163 | resp.Body.Close() 164 | 165 | time.Sleep(2 * time.Second) 166 | 167 | req, err = http.NewRequest(get, url, bytes.NewBufferString("")) 168 | resp, err = client.Do(req) 169 | defer resp.Body.Close() 170 | if err != nil || resp == nil || resp.Status != statusNotFound { 171 | t.Error("TestTTL() failed", err, resp) 172 | } 173 | } 174 | --------------------------------------------------------------------------------