Skip to content
This repository has been archived by the owner on Dec 25, 2024. It is now read-only.

Commit

Permalink
feat(joke): added tests for JokeDEV
Browse files Browse the repository at this point in the history
  • Loading branch information
Wittano committed Mar 22, 2024
1 parent 0a4aac9 commit e751615
Show file tree
Hide file tree
Showing 2 changed files with 176 additions and 19 deletions.
65 changes: 46 additions & 19 deletions pkgs/joke/jokedev.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import (
"time"
)

const devServiceName = "jokedev"
const (
rateLimitRemainingHeaderName = "RateLimit-Remaining"
jokeDevAPIUrlTemplate = "https://v2.jokeapi.dev/joke/%s?type=%s"
)

type jokeMapper interface {
Joke() Joke
Expand All @@ -26,21 +29,21 @@ type jokeApiFlags struct {
}

type jokeApiSingleResponse struct {
Error bool `json:"error"`
CategoryRes string `json:"category"`
Type string `json:"type"`
Flags jokeApiFlags `json:"flags"`
Id int `json:"id"`
Safe bool `json:"safe"`
Lang string `json:"lang"`
Content string `json:"joke"`
Error bool `json:"error"`
Category string `json:"category"`
Type string `json:"type"`
Flags jokeApiFlags `json:"flags"`
Id int `json:"id"`
Safe bool `json:"safe"`
Lang string `json:"lang"`
Content string `json:"joke"`
}

func (j jokeApiSingleResponse) Joke() Joke {
return Joke{
Answer: j.Content,
Type: Single,
Category: Category(j.CategoryRes),
Category: Category(j.Category),
}
}

Expand Down Expand Up @@ -99,7 +102,7 @@ func (d *DevService) Get(ctx context.Context, search SearchParameters) (Joke, er
search.Category = Any
}

jokeDevApiURL := fmt.Sprintf("https://v2.jokeapi.dev/joke/%s?type=%s", search.Category, search.Category)
jokeDevApiURL := fmt.Sprintf(jokeDevAPIUrlTemplate, search.Category, search.Type)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, jokeDevApiURL, nil)
if err != nil {
return Joke{}, err
Expand All @@ -112,19 +115,22 @@ func (d *DevService) Get(ctx context.Context, search SearchParameters) (Joke, er
defer res.Body.Close()

// Check if daily limit exceeded
if res.StatusCode == http.StatusTooManyRequests || res.Header["RateLimit-Remaining"][0] == "0" {
resetTime, err := time.Parse("Sun, 06 Nov 1994 08:49:37 GMT", res.Header["RateLimit-Reset"][0])
if err != nil {
return Joke{}, err
}
isLimitExceeded := len(res.Header[rateLimitRemainingHeaderName]) > 0 && res.Header[rateLimitRemainingHeaderName][0] == "0"

if res.StatusCode == http.StatusTooManyRequests || isLimitExceeded {
const rateLimitReset = "RateLimit-Reset"
resetTime := prepareResetTime(res.Header[rateLimitReset])
d.active = false

go unlockService(d.globalCtx, &d.active, resetTime)

return Joke{}, DevServiceLimitExceededErr
}

if res.StatusCode >= 400 {
return Joke{}, errors.New("jokedev: client or server side error")
}

resBody, err := io.ReadAll(res.Body)
if err != nil {
return Joke{}, err
Expand All @@ -133,15 +139,36 @@ func (d *DevService) Get(ctx context.Context, search SearchParameters) (Joke, er
var jokeMapper jokeMapper
switch search.Type {
case Single:
jokeMapper = jokeApiSingleResponse{}
singleRes := jokeApiSingleResponse{}

err = json.Unmarshal(resBody, &singleRes)

jokeMapper = singleRes
case TwoPart:
jokeMapper = jokeApiTwoPartResponse{}
twoPartRes := &jokeApiTwoPartResponse{}

err = json.Unmarshal(resBody, &twoPartRes)

jokeMapper = twoPartRes
}

err = json.Unmarshal(resBody, &jokeMapper)
if err != nil {
return Joke{}, err
}

return jokeMapper.Joke(), nil
}

func prepareResetTime(rateLimitReset []string) (resetTime time.Time) {
var err error
if len(rateLimitReset) > 0 {
resetTime, err = time.Parse("Sun, 06 Nov 1994 08:49:37 GMT", rateLimitReset[0])
if err != nil {
resetTime = time.Now().Add(24 * time.Hour)
}
} else {
resetTime = time.Now().Add(24 * time.Hour)
}

return resetTime
}
130 changes: 130 additions & 0 deletions pkgs/joke/jokedev_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package joke

import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/jarcoal/httpmock"
"net/http"
"os"
"strconv"
"testing"
)

var testSingleJokeDev = jokeApiSingleResponse{
Error: false,
Category: "Any",
Type: "single",
Flags: jokeApiFlags{},
Id: 0,
Safe: false,
Lang: "",
Content: "testContent",
}

var testJokeDevUrl = fmt.Sprintf(jokeDevAPIUrlTemplate, testJokeSearch.Category, testJokeSearch.Type)

func TestDevService_Get(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()

response, err := json.Marshal(testSingleJokeDev)
if err != nil {
t.Fatal(err)
}

httpmock.RegisterResponder("GET",
testJokeDevUrl,
httpmock.NewBytesResponder(http.StatusOK, response))

os.Setenv(humorAPIKey, "123")

ctx := context.Background()
service := NewJokeDevService(ctx)

joke, err := service.Get(ctx, testJokeSearch)
if err != nil {
t.Fatal(err)
}

if joke.Answer != testSingleJokeDev.Content {
t.Fatalf("Invalid joke response. Expected: '%s', Result: '%s'", testSingleJokeDev.Content, joke.Answer)
}

if joke.Category != Category(testSingleJokeDev.Category) {
t.Fatalf("Invalid category. Expected: '%s', Result: '%s'", testJokeSearch, joke.Category)
}
}

func TestDevService_GetButApiReturnInvalidStatus(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()

badResponses := []int{http.StatusTooManyRequests, http.StatusPaymentRequired, http.StatusBadRequest, http.StatusForbidden, http.StatusInternalServerError}

httpmock.RegisterResponder("GET",
testJokeDevUrl,
httpmock.NewStringResponder(http.StatusOK, ""))

os.Setenv(humorAPIKey, "123")

for _, status := range badResponses {
t.Run("API responses status "+strconv.Itoa(status), func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

service := NewJokeDevService(ctx)

if _, err := service.Get(ctx, testJokeSearch); err == nil {
t.Fatal("service didn't handle correct a bad/invalid http status")
}
})
}
}

func TestDevService_GetButApiLimitWasExceeded(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()

httpmock.RegisterResponder("GET",
testJokeDevUrl,
httpmock.NewStringResponder(http.StatusTooManyRequests, "").HeaderAdd(http.Header{xAPIQuotaLeftHeaderName: []string{"0"}}))

os.Setenv(humorAPIKey, "123")

ctx := context.Background()
service := NewJokeDevService(ctx)

if _, err := service.Get(ctx, testJokeSearch); !errors.Is(err, DevServiceLimitExceededErr) {
t.Fatal(err)
}
}

func TestDevService_Active(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()

httpmock.RegisterResponder("GET",
testJokeDevUrl,
httpmock.NewStringResponder(http.StatusTooManyRequests, "").HeaderAdd(http.Header{rateLimitRemainingHeaderName: []string{"0"}}))

os.Setenv(humorAPIKey, "123")

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

service := NewJokeDevService(ctx)

if _, err := service.Get(ctx, testJokeSearch); !errors.Is(err, DevServiceLimitExceededErr) {
t.Fatal(err)
}

if _, err := service.Get(ctx, testJokeSearch); !errors.Is(err, DevServiceLimitExceededErr) {
t.Fatal(err)
}

if httpmock.GetTotalCallCount() != 1 {
t.Fatalf("service call DevService after got information about limitation exceeded. Call API %d times", httpmock.GetTotalCallCount())
}
}

0 comments on commit e751615

Please sign in to comment.