From ecacf2d726f7f122ca95a2a241cec3ee1345c0fe Mon Sep 17 00:00:00 2001 From: David Ashby Date: Sat, 26 Jun 2021 16:48:49 -0400 Subject: [PATCH] more tests and standardize struct field casing --- README.md | 4 ++- client/client.go | 33 ++++++++++++-------- client/client_test.go | 71 +++++++++++++++++++++++++++++++++++++++++-- client/response.go | 10 +++--- 4 files changed, 97 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 2568042..af1af1f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ # go-openlibrary -A simple Go wrapper around the OpenLibrary.org Books API: . +[![Go Reference](https://pkg.go.dev/badge/git.yetaga.in/alazyreader/go-openlibrary.svg)](https://pkg.go.dev/git.yetaga.in/alazyreader/go-openlibrary) + +A simple Go wrapper around the OpenLibrary.org **Books** API: . diff --git a/client/client.go b/client/client.go index b90adba..226ef5c 100644 --- a/client/client.go +++ b/client/client.go @@ -12,7 +12,16 @@ var ( defaultAPI = "https://openlibrary.org/api/books" ) -// Client +var ( + // FormatError marks errors with provided book identifers + FormatError = fmt.Errorf("identifier in wrong format") + // NetworkError marks errors with the connection to the OpenLibrary API + NetworkError = fmt.Errorf("network error") + // NotFoundError marks an empty response from the OpenLibrary API + NotFoundError = fmt.Errorf("book not found") +) + +// Client provides OpenLibrary Books API query methods type Client struct { httpClient http.Client apipath string @@ -33,9 +42,9 @@ func (c *Client) SetAPIPath(p string) { // GetByISBN fetches details for a book based on its ISBN10 or ISBN13. func (c *Client) GetByISBN(k string) (*BookDetails, error) { if k == "" { - return nil, fmt.Errorf("ISBN cannot be empty") + return nil, fmt.Errorf("%w: ISBN cannot be empty", FormatError) } else if len(k) != 10 && len(k) != 13 { - return nil, fmt.Errorf("ISBN must be either 10 or 13 digits") + return nil, fmt.Errorf("%w: ISBN must be either 10 or 13 digits", FormatError) } key := "ISBN:" + k return c.fetch(key) @@ -44,7 +53,7 @@ func (c *Client) GetByISBN(k string) (*BookDetails, error) { // GetByOCLC fetches details for a book based on its WorldCat OCLC number. func (c *Client) GetByOCLC(k string) (*BookDetails, error) { if k == "" { - return nil, fmt.Errorf("OCLC cannot be empty") + return nil, fmt.Errorf("%w: OCLC cannot be empty", FormatError) } key := "OCLC:" + k return c.fetch(key) @@ -53,7 +62,7 @@ func (c *Client) GetByOCLC(k string) (*BookDetails, error) { // GetByLCCN fetches details for a book based on its Library of Congress Control Number. func (c *Client) GetByLCCN(k string) (*BookDetails, error) { if k == "" { - return nil, fmt.Errorf("LCCN cannot be empty") + return nil, fmt.Errorf("%w: LCCN cannot be empty", FormatError) } key := "LCCN:" + k return c.fetch(key) @@ -62,7 +71,7 @@ func (c *Client) GetByLCCN(k string) (*BookDetails, error) { // GetByOLID fetches details for a book based on its OpenLibrary ID number. func (c *Client) GetByOLID(k string) (*BookDetails, error) { if k == "" { - return nil, fmt.Errorf("OLID cannot be empty") + return nil, fmt.Errorf("%w: OLID cannot be empty", FormatError) } key := "OLID:" + k return c.fetch(key) @@ -82,7 +91,7 @@ func (c *Client) fetch(key string) (*BookDetails, error) { } u, err := url.Parse(h) if err != nil { - return nil, err + return nil, fmt.Errorf("%w: %v", NetworkError, err) } u.RawQuery = url.Values{ "bibkeys": []string{key}, @@ -92,26 +101,26 @@ func (c *Client) fetch(key string) (*BookDetails, error) { resp, err := c.httpClient.Get(u.String()) if err != nil { - return nil, err + return nil, fmt.Errorf("%w: %v", NetworkError, err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("received non-200 status code: %d", resp.StatusCode) + return nil, fmt.Errorf("%w: received non-200 status code: %d", NetworkError, resp.StatusCode) } var result response b, err := io.ReadAll(resp.Body) if err != nil { - return nil, err + return nil, fmt.Errorf("%w: %v", NetworkError, err) } if err = json.Unmarshal(b, &result); err != nil { - return nil, err + return nil, fmt.Errorf("%w: %v", NetworkError, err) } details, ok := result[key] if !ok { - return nil, fmt.Errorf("book not found") + return nil, NotFoundError } return &details, nil diff --git a/client/client_test.go b/client/client_test.go index 32aabf4..64391bb 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -1,6 +1,7 @@ package client import ( + "errors" "fmt" "net/http" "net/http/httptest" @@ -9,7 +10,10 @@ import ( func testServer(expectedkey string) *httptest.Server { return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Query().Get("bibkeys") == expectedkey { + if r.URL.Query().Get("bibkeys") == "SPECIAL:MISSING" { + w.WriteHeader(http.StatusOK) + w.Write([]byte("{}")) + } else if r.URL.Query().Get("bibkeys") == expectedkey { w.WriteHeader(http.StatusOK) w.Write([]byte(fmt.Sprintf(exampleResponse, expectedkey))) } else { @@ -34,6 +38,7 @@ func TestFetch(t *testing.T) { t.Fail() } } + func TestFetchFailure(t *testing.T) { r := testServer("ISBN:1234567890") defer r.Close() @@ -41,12 +46,72 @@ func TestFetchFailure(t *testing.T) { c := Client{} c.SetAPIPath(r.URL) _, err := c.GetByISBN("9780980200447") - if err == nil { - t.Log("expected error, received nil") + if err == nil || !errors.Is(err, NetworkError) { + t.Logf("expected error, received nil: %v", err) t.Fail() } } +func TestBookNotFound(t *testing.T) { + r := testServer("") + defer r.Close() + + c := Client{} + c.SetAPIPath(r.URL) + _, err := c.GetByRawKey("SPECIAL:MISSING") + if err == nil || !errors.Is(err, NotFoundError) { + t.Logf("expected error, received nil: %v", err) + t.Fail() + } +} + +func TestKeyRejections(t *testing.T) { + c := Client{} + cases := map[string][]string{ + "ISBN": {"", "1", "12", "123", "1234", "12345", "123456", "1234567", "12345678", "123456789", "12345678901", "123456789012", "12345678901234"}, + "OCLC": {""}, + "LCCN": {""}, + "OLID": {""}, + } + for id, ts := range cases { + switch id { + case "ISBN": + for i := range ts { + _, err := c.GetByISBN(ts[i]) + if !errors.Is(err, FormatError) { + t.Logf("%s received %v, expected %v", id, err, FormatError) + t.Fail() + } + } + case "OCLC": + for i := range ts { + _, err := c.GetByOCLC(ts[i]) + if !errors.Is(err, FormatError) { + t.Logf("%s received %v, expected %v", id, err, FormatError) + t.Fail() + } + } + case "LCCN": + for i := range ts { + _, err := c.GetByLCCN(ts[i]) + if !errors.Is(err, FormatError) { + t.Logf("%s received %v, expected %v", id, err, FormatError) + t.Fail() + } + } + case "OLID": + for i := range ts { + _, err := c.GetByOLID(ts[i]) + if !errors.Is(err, FormatError) { + t.Logf("%s received %v, expected %v", id, err, FormatError) + t.Fail() + } + } + } + } +} + +// example comes from the official API docs var exampleResponse = `{ "%s": { "publishers": [ diff --git a/client/response.go b/client/response.go index f944da6..feabd3f 100644 --- a/client/response.go +++ b/client/response.go @@ -44,11 +44,11 @@ type Identifiers struct { Amazon []string `json:"amazon"` Goodreads []string `json:"goodreads"` Google []string `json:"google"` - Isbn10 []string `json:"isbn_10"` - Isbn13 []string `json:"isbn_13"` - Lccn []string `json:"lccn"` - Librarything []string `json:"librarything"` - Oclc []string `json:"oclc"` + ISBN10 []string `json:"isbn_10"` + ISBN13 []string `json:"isbn_13"` + LCCN []string `json:"lccn"` + LibraryThing []string `json:"librarything"` + OCLC []string `json:"oclc"` ProjectGutenberg []string `json:"project_gutenberg"` }