Skip to content

Commit

Permalink
Merge pull request #11 from davemachado/feature/random-accept-search-…
Browse files Browse the repository at this point in the history
…request

Allow /random endpoint to filter by search request
  • Loading branch information
davemachado committed Feb 4, 2019
2 parents f2d959f + 29e35b0 commit a5d59ff
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 47 deletions.
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,17 @@ Parameter | Type | Data Type | Description | Required
*List a single entry selected at random*

### Parameters
None
Parameter | Type | Data Type | Description | Required
| --- | --- | --- | --- | --- |
| title | query | string | name of entry (matches via substring - i.e. "at" would return "cat" and "atlas") | No |
| description | query | string | description of entry (matches via substring) | No |
| auth | query | string | auth type of entry (can only be values matching in project or null) | No |
| https | query | bool | return entries that support HTTPS or not | No |
| cors | query | string | CORS support for entry ("yes", "no", or "unknown") | No |
| category | query | string | return entries of a specific category | No |

#### Example
/random?auth=null

## **GET** /categories

Expand Down
46 changes: 18 additions & 28 deletions handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import (
"io"
"math/rand"
"net/http"

"github.com/gorilla/schema"
)

type (
Expand All @@ -26,13 +24,13 @@ type (
}
// Entry describes a single API reference.
Entry struct {
API string
Description string
Auth string
HTTPS bool
Cors string
Link string
Category string
API string `json:"API"`
Description string `json:"Description"`
Auth string `json:"Auth"`
HTTPS bool `json:"HTTPS"`
Cors string `json:"Cors"`
Link string `json:"Link"`
Category string `json:"Category"`
}
)

Expand All @@ -44,23 +42,10 @@ func getEntriesHandler() http.Handler {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
var err error
searchReq := new(SearchRequest)
// Only check query parameters if the request's Body is not nil
if req.Body != nil {
// Decode incoming search request off the query parameters map.
err = schema.NewDecoder().Decode(searchReq, req.URL.Query())
if err != nil {
http.Error(w, "server failed to parse request: "+err.Error(), http.StatusBadRequest)
return
}
defer req.Body.Close()
}
var results []Entry
for _, e := range apiList.Entries {
if checkEntryMatches(e, searchReq) {
results = append(results, e)
}
results, err := processSearchRequestToMatchingEntries(req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
Expand Down Expand Up @@ -99,11 +84,16 @@ func getRandomHandler() http.Handler {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
results, err := processSearchRequestToMatchingEntries(req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
err := json.NewEncoder(w).Encode(Entries{
err = json.NewEncoder(w).Encode(Entries{
Count: 1,
Entries: []Entry{apiList.Entries[rand.Intn(len(apiList.Entries))]},
Entries: []Entry{results[rand.Intn(len(results))]},
})
if err != nil {
http.Error(w, "server failed to encode response object: "+err.Error(), http.StatusInternalServerError)
Expand Down
70 changes: 53 additions & 17 deletions handlers_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,40 @@
package main

import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"reflect"
"testing"
)

func mockEntry() []Entry {
return []Entry{
{
API: "title",
Description: "description",
Auth: "apiKey",
HTTPS: false,
Cors: "Cors",
Link: "link",
Category: "category",
},
}
}

func assertResponseValid(t *testing.T, body *bytes.Buffer, expected []Entry) {
var resp Entries
if err := json.NewDecoder(body).Decode(&resp); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(resp.Entries, expected) {
t.Fatalf("handler returned wrong entry: got %v want %v",
resp.Entries, expected)
}

}

func TestHealthCheckHandler(t *testing.T) {
req, err := http.NewRequest("GET", "/health", nil)
if err != nil {
Expand All @@ -30,12 +59,26 @@ func TestGetCategoriesHandler(t *testing.T) {
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := healthCheckHandler()
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
testCases := []struct {
categories []string
expectedBody string
}{
{[]string{}, "[]\n"},
{[]string{"cat1"}, "[\"cat1\"]\n"},
{[]string{"cat1", "cat2", "cat3"}, "[\"cat1\",\"cat2\",\"cat3\"]\n"},
}
for _, tc := range testCases {
categories = tc.categories
rr := httptest.NewRecorder()
handler := getCategoriesHandler()
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
if rr.Body.String() != tc.expectedBody {
t.Errorf("handler returned wrong body: got %q want %q", rr.Body, tc.expectedBody)
}
}
}

Expand All @@ -44,38 +87,31 @@ func TestGetRandomHandler(t *testing.T) {
if err != nil {
t.Fatal(err)
}
apiList.Entries = []Entry{
{
API: "title",
Description: "description",
Auth: "apiKey",
HTTPS: false,
Cors: "Cors",
Link: "link",
Category: "category",
},
}
apiList.Entries = mockEntry()
rr := httptest.NewRecorder()
handler := getRandomHandler()
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
assertResponseValid(t, rr.Body, apiList.Entries)
}

func TestGetEntriesHandler(t *testing.T) {
req, err := http.NewRequest("GET", "/api", nil)
if err != nil {
t.Fatal(err)
}
apiList.Entries = mockEntry()
rr := httptest.NewRecorder()
handler := getEntriesHandler()
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
assertResponseValid(t, rr.Body, apiList.Entries)
}

func TestGetEntriesWithBadMethod(t *testing.T) {
Expand Down
21 changes: 21 additions & 0 deletions util.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ package main

import (
"encoding/json"
"fmt"
"net/http"
"os"
"strconv"
"strings"

"github.com/gorilla/schema"
)

// getList initializes an Entries struct filled from the public-apis project
Expand Down Expand Up @@ -54,3 +58,20 @@ func checkEntryMatches(entry Entry, request *SearchRequest) bool {
}
return false
}

// processSearchRequestToMatchingEntries decodes the request body into a SearchRequest struct that can
// be processed in a call to checkEntryMatches to return all matching entries.
func processSearchRequestToMatchingEntries(req *http.Request) ([]Entry, error) {
searchReq := new(SearchRequest)
// Decode incoming search request off the query parameters map.
if err := schema.NewDecoder().Decode(searchReq, req.URL.Query()); err != nil {
return nil, fmt.Errorf("server failed to parse request: %v", err)
}
var results []Entry
for _, e := range apiList.Entries {
if checkEntryMatches(e, searchReq) {
results = append(results, e)
}
}
return results, nil
}
61 changes: 60 additions & 1 deletion util_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package main

import "testing"
import (
"net/http"
"net/url"
"reflect"
"testing"
)

func TestGetCategories(t *testing.T) {
actual := parseCategories([]Entry{
Expand Down Expand Up @@ -76,3 +81,57 @@ func TestCheckEntryMatches(t *testing.T) {
})
}
}

func TestProcessSearchRequestToMatchingEntries(t *testing.T) {
apiList.Entries = []Entry{
Entry{
API: "examplesAsAService",
Description: "provide classic examples",
Auth: "apiKey",
HTTPS: true,
Cors: "Unknown",
Link: "http:https://www.example.com",
Category: "Development",
},
Entry{
API: "examplesAsAServiceToo",
Description: "provide classic examples",
Auth: "",
HTTPS: true,
Cors: "Yes",
Link: "http:https://www.example.com",
Category: "Development",
},
}
testCases := []struct {
name string
query string
expected []Entry
}{
{"null auth", "?auth=null", []Entry{apiList.Entries[1]}},
{"apiKey auth", "?auth=apiKey", []Entry{apiList.Entries[0]}},
{"multi-key query", "?auth=null&description=example", []Entry{apiList.Entries[1]}},
{"multi-key query full match", "?category=development&description=example", apiList.Entries},
{"fully-matching description", "?description=example", apiList.Entries},
{"unkwown cors", "?cors=unknown", []Entry{apiList.Entries[0]}},
{"yes cors", "?cors=yes", []Entry{apiList.Entries[1]}},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
u, err := url.Parse(tc.query)
if err != nil {
t.Fatal(err)
}
req := &http.Request{URL: u}
actual, err := processSearchRequestToMatchingEntries(req)
if err != nil {
t.Error(err)
}
if !reflect.DeepEqual(actual, tc.expected) {
t.Errorf("unexpected matched entries:\nreceived %+v\nexpected %+v\n", actual, tc.expected)
}
})
}

}

0 comments on commit a5d59ff

Please sign in to comment.