Skip to content

Commit

Permalink
Start working on having status codes settable by resolvers (#334)
Browse files Browse the repository at this point in the history
  • Loading branch information
pajlada authored Jul 30, 2022
1 parent 10a0131 commit b13b28e
Show file tree
Hide file tree
Showing 56 changed files with 1,489 additions and 546 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- Fix: SevenTV emotes now resolve correctly. (#281, #288, #307)
- Fix: YouTube videos are no longer resolved as channels. (#284)
- Fix: Default resolver no longer crashes when provided url is broken. (#310)
- Fix: JSON responses now always return the proper content type. (#334)
- Dev: Improve BetterTTV emote tests. (#282)
- Minor: BetterTTV cache key changed from plural to singular form. (#282)
- Dev: Improve Twitch.tv clip tests. (#283)
Expand Down
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ build:
test:
@go test ./... -tags test

cover:
@go test ./... -cover -tags test

cover_html:
@go test ./... -coverprofile=coverage.out -tags test && go tool cover -html=coverage.out

vtest:
@go test -v ./... -tags test

Expand Down
38 changes: 32 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Go web service that serves as a cache to APIs that each Chatterino client could

## Routes

### Resolve Twitch emote set

`twitchemotes/set/:setID`
Returns information about a given Twitch emote set. Example response:

Expand All @@ -20,33 +22,57 @@ Returns information about a given Twitch emote set. Example response:
}
```

### Resolve URL

`link_resolver/:url`
Resolves a url into a preview tooltip. Example response:
Resolves a url into a preview tooltip.
Route content type: `application/json`
Route HTTP Status Code is almost always `200` as long as we were able to generate information about the URL, even if the API we call returns 404 or 500.
If the given URL is not a valid url, the Route HTTP status code will be `400`.

#### Examples

`url` parameter: `https://example.com/page`

```json
{
"status": 200, // status code returned or inferred from the page
"thumbnail": "http://api.url/thumbnail/web.com%2Fimage.png", // proxied thumbnail url if there's an image
"message": "", // used to forward errors in case the website e.g. couldn't load
"tooltip": "<div>tooltip</div>", // HTML tooltip used in Chatterino
"link": "http://example.com/longer-page" // final url, after any redirects
}
```

`url` parameter: `https://example.com/error`

```json
{
"status": 200, // status code returned from the page
"thumbnail": "http://api.url/thumbnail/web.com%2Fimage.png", // proxied thumbnail url if there's an image
"message": "", // used to forward errors in case the website e.g. couldn't load
"tooltip": "<div>tooltip</div>", // HTML tooltip used in Chatterino
"link": "http://final.url.com/asd" // final url, after any redirects
"status": 404,
"message": "Page not found"
}
```

### API Uptime

`health/uptime`
Returns API service's uptime. Example response:

```
928h2m53.795354922s
```

### API Memory usage

`health/memory`
Returns information about memory usage. Example response:

```
Alloc=505 MiB, TotalAlloc=17418866 MiB, Sys=3070 MiB, NumGC=111245
```

### API Uptime and memory usage

`health/combined`
Returns both uptime and information about memory usage. Example response:

Expand Down
8 changes: 4 additions & 4 deletions internal/caches/twitchusernamecache/username_loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ type UsernameLoader struct {
helixClient *helix.Client
}

func (l *UsernameLoader) Load(ctx context.Context, twitchUserID string, req *http.Request) ([]byte, time.Duration, error) {
func (l *UsernameLoader) Load(ctx context.Context, twitchUserID string, req *http.Request) ([]byte, *int, *string, time.Duration, error) {
params := &helix.UsersParams{
IDs: []string{
twitchUserID,
Expand All @@ -23,14 +23,14 @@ func (l *UsernameLoader) Load(ctx context.Context, twitchUserID string, req *htt

response, err := l.helixClient.GetUsers(params)
if err != nil {
return nil, cache.NoSpecialDur, err
return nil, nil, nil, cache.NoSpecialDur, err
}

if len(response.Data.Users) != 1 {
return nil, cache.NoSpecialDur, errors.New("no user with this ID found")
return nil, nil, nil, cache.NoSpecialDur, errors.New("no user with this ID found")
}

user := response.Data.Users[0]

return []byte(user.Login), cache.NoSpecialDur, nil
return []byte(user.Login), nil, nil, cache.NoSpecialDur, nil
}
44 changes: 44 additions & 0 deletions internal/migration/2_cache_http_status_code.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//go:build !test || migrationtest

package migration

import (
"context"

"github.com/jackc/pgx/v4"
)

func init() {
// The version of this migration
const migrationVersion = 2

Register(
migrationVersion,
func(ctx context.Context, tx pgx.Tx) error {
// The Up action of this migration
// Delete all cached entries
_, err := tx.Exec(ctx, `TRUNCATE cache;`)
if err != nil {
return err
}

_, err = tx.Exec(ctx, `
ALTER TABLE cache
ADD http_status_code SMALLINT NOT NULL,
ADD http_content_type TEXT NOT NULL
;`)

return err
},
func(ctx context.Context, tx pgx.Tx) error {
// The Down action of this migration
_, err := tx.Exec(ctx, `
ALTER TABLE cache
DROP http_status_code,
DROP http_content_type
;`)

return err
},
)
}
1 change: 0 additions & 1 deletion internal/resolvers/betterttv/emote_loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,6 @@ func (l *EmoteLoader) Load(ctx context.Context, emoteHash string, r *http.Reques
Tooltip: url.PathEscape(tooltip.String()),
Thumbnail: thumbnailURL,
}, resolver.NoSpecialDur, nil

}

func NewEmoteLoader(emoteAPIURL *url.URL) *EmoteLoader {
Expand Down
2 changes: 1 addition & 1 deletion internal/resolvers/betterttv/emote_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func (r *EmoteResolver) Check(ctx context.Context, url *url.URL) (context.Contex
return ctx, true
}

func (r *EmoteResolver) Run(ctx context.Context, url *url.URL, req *http.Request) ([]byte, error) {
func (r *EmoteResolver) Run(ctx context.Context, url *url.URL, req *http.Request) (*cache.Response, error) {
matches := emotePathRegex.FindStringSubmatch(url.Path)
if len(matches) != 2 {
return nil, ErrInvalidBTTVEmotePath
Expand Down
77 changes: 51 additions & 26 deletions internal/resolvers/betterttv/emote_resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"testing"

"github.com/Chatterino/api/internal/logger"
"github.com/Chatterino/api/pkg/cache"
"github.com/Chatterino/api/pkg/config"
"github.com/Chatterino/api/pkg/utils"
qt "github.com/frankban/quicktest"
Expand Down Expand Up @@ -107,9 +108,9 @@ func TestEmoteResolver(t *testing.T) {
inputURL *url.URL
inputEmoteHash string
inputReq *http.Request
// expectedBytes will be returned from the cache, and expected to be returned in the same form
expectedBytes []byte
expectedError error
// expectedResponse will be returned from the cache, and expected to be returned in the same form
expectedResponse *cache.Response
expectedError error
}

tests := []runTest{
Expand All @@ -118,49 +119,61 @@ func TestEmoteResolver(t *testing.T) {
inputURL: utils.MustParseURL("https://betterttv.com/emotes/566ca04265dbbdab32ec054a"),
inputEmoteHash: "566ca04265dbbdab32ec054a",
inputReq: nil,
expectedBytes: []byte(`{"status":200,"thumbnail":"https://cdn.betterttv.net/emote/566ca04265dbbdab32ec054a/3x","tooltip":"%3Cdiv%20style=%22text-align:%20left%3B%22%3E%3Cb%3EKKona%3C%2Fb%3E%3Cbr%3E%3Cb%3EGlobal%20BetterTTV%20Emote%3C%2Fb%3E%3Cbr%3E%3Cb%3EBy:%3C%2Fb%3E%20NightDev%3C%2Fdiv%3E"}`),
expectedError: nil,
expectedResponse: &cache.Response{
Payload: []byte(`{"status":200,"thumbnail":"https://cdn.betterttv.net/emote/566ca04265dbbdab32ec054a/3x","tooltip":"%3Cdiv%20style=%22text-align:%20left%3B%22%3E%3Cb%3EKKona%3C%2Fb%3E%3Cbr%3E%3Cb%3EGlobal%20BetterTTV%20Emote%3C%2Fb%3E%3Cbr%3E%3Cb%3EBy:%3C%2Fb%3E%20NightDev%3C%2Fdiv%3E"}`),
StatusCode: http.StatusOK,
ContentType: "application/json",
},
expectedError: nil,
},
{
label: "Matching link - cached 2",
inputURL: utils.MustParseURL("https://betterttv.com/emotes/566ca04265dbbdab32ec054a"),
inputEmoteHash: "566ca04265dbbdab32ec054a",
inputReq: nil,
expectedBytes: []byte(`{"status":200,"thumbnail":"https://cdn.betterttv.net/emote/566ca04265dbbdab32ec054a/3x","tooltip":"%3Cdiv%20style=%22text-align:%20left%3B%22%3E%3Cb%3EKKona%3C%2Fb%3E%3Cbr%3E%3Cb%3EGlobal%20BetterTTV%20Emote%3C%2Fb%3E%3Cbr%3E%3Cb%3EBy:%3C%2Fb%3E%20NightDev%3C%2Fdiv%3E"}`),
expectedError: nil,
expectedResponse: &cache.Response{
Payload: []byte(`{"status":200,"thumbnail":"https://cdn.betterttv.net/emote/566ca04265dbbdab32ec054a/3x","tooltip":"%3Cdiv%20style=%22text-align:%20left%3B%22%3E%3Cb%3EKKona%3C%2Fb%3E%3Cbr%3E%3Cb%3EGlobal%20BetterTTV%20Emote%3C%2Fb%3E%3Cbr%3E%3Cb%3EBy:%3C%2Fb%3E%20NightDev%3C%2Fdiv%3E"}`),
StatusCode: http.StatusOK,
ContentType: "application/json",
},
expectedError: nil,
},
{
label: "Matching link - 404",
inputURL: utils.MustParseURL("https://betterttv.com/emotes/404"),
inputEmoteHash: "404",
inputReq: nil,
expectedBytes: []byte(`{"status":404,"message":"No BetterTTV emote with this hash found"}`),
expectedError: nil,
expectedResponse: &cache.Response{
Payload: []byte(`{"status":404,"message":"No BetterTTV emote with this hash found"}`),
StatusCode: http.StatusOK,
ContentType: "application/json",
},
expectedError: nil,
},
}

for _, test := range tests {
c.Run(test.label, func(c *qt.C) {
rows := pgxmock.NewRows([]string{"value"}).AddRow(test.expectedBytes)
rows := pgxmock.NewRows([]string{"value", "http_status_code", "http_content_type"}).AddRow(test.expectedResponse.Payload, http.StatusOK, test.expectedResponse.ContentType)
pool.ExpectQuery("SELECT").
WithArgs("betterttv:emote:" + test.inputEmoteHash).
WillReturnRows(rows)
outputBytes, outputError := resolver.Run(ctx, test.inputURL, test.inputReq)
c.Assert(outputError, qt.Equals, test.expectedError)
c.Assert(outputBytes, qt.DeepEquals, test.expectedBytes)
c.Assert(outputBytes, qt.DeepEquals, test.expectedResponse)
})
}
})

c.Run("Not cached", func(c *qt.C) {
type runTest struct {
label string
inputURL *url.URL
inputEmoteHash string
inputReq *http.Request
expectedBytes []byte
expectedError error
rowsReturned int
label string
inputURL *url.URL
inputEmoteHash string
inputReq *http.Request
expectedResponse *cache.Response
expectedError error
rowsReturned int
}

tests := []runTest{
Expand All @@ -169,36 +182,48 @@ func TestEmoteResolver(t *testing.T) {
inputURL: utils.MustParseURL("https://betterttv.com/emotes/566ca04265dbbdab32ec054b"),
inputEmoteHash: "566ca04265dbbdab32ec054b",
inputReq: nil,
expectedBytes: []byte(`{"status":200,"thumbnail":"https://cdn.betterttv.net/emote/566ca04265dbbdab32ec054b/3x","tooltip":"%3Cdiv%20style=%22text-align:%20left%3B%22%3E%3Cb%3EKKona%3C%2Fb%3E%3Cbr%3E%3Cb%3EGlobal%20BetterTTV%20Emote%3C%2Fb%3E%3Cbr%3E%3Cb%3EBy:%3C%2Fb%3E%20zneix%3C%2Fdiv%3E"}`),
expectedError: nil,
expectedResponse: &cache.Response{
Payload: []byte(`{"status":200,"thumbnail":"https://cdn.betterttv.net/emote/566ca04265dbbdab32ec054b/3x","tooltip":"%3Cdiv%20style=%22text-align:%20left%3B%22%3E%3Cb%3EKKona%3C%2Fb%3E%3Cbr%3E%3Cb%3EGlobal%20BetterTTV%20Emote%3C%2Fb%3E%3Cbr%3E%3Cb%3EBy:%3C%2Fb%3E%20zneix%3C%2Fdiv%3E"}`),
StatusCode: http.StatusOK,
ContentType: "application/json",
},
expectedError: nil,
},
{
label: "404",
inputURL: utils.MustParseURL("https://betterttv.com/emotes/404"),
inputEmoteHash: "404",
inputReq: nil,
expectedBytes: []byte(`{"status":404,"message":"No BetterTTV emote with this hash found"}`),
expectedError: nil,
expectedResponse: &cache.Response{
Payload: []byte(`{"status":404,"message":"No BetterTTV emote with this hash found"}`),
StatusCode: http.StatusOK,
ContentType: "application/json",
},
expectedError: nil,
},
{
label: "Bad JSON",
inputURL: utils.MustParseURL("https://betterttv.com/emotes/bad"),
inputEmoteHash: "bad",
inputReq: nil,
expectedBytes: []byte(`{"status":500,"message":"betterttv api unmarshal error: invalid character \u0026#39;x\u0026#39; looking for beginning of value"}`),
expectedError: nil,
expectedResponse: &cache.Response{
Payload: []byte(`{"status":500,"message":"betterttv api unmarshal error: invalid character \u0026#39;x\u0026#39; looking for beginning of value"}`),
StatusCode: http.StatusOK,
ContentType: "application/json",
},
expectedError: nil,
},
}

for _, test := range tests {
c.Run(test.label, func(c *qt.C) {
pool.ExpectQuery("SELECT").WillReturnError(pgx.ErrNoRows)
pool.ExpectExec("INSERT INTO cache").
WithArgs("betterttv:emote:"+test.inputEmoteHash, test.expectedBytes, pgxmock.AnyArg()).
WithArgs("betterttv:emote:"+test.inputEmoteHash, test.expectedResponse.Payload, http.StatusOK, test.expectedResponse.ContentType, pgxmock.AnyArg()).
WillReturnResult(pgxmock.NewResult("INSERT", 1))
outputBytes, outputError := resolver.Run(ctx, test.inputURL, test.inputReq)
c.Assert(outputError, qt.Equals, test.expectedError)
c.Assert(outputBytes, qt.DeepEquals, test.expectedBytes)
c.Assert(outputBytes, qt.DeepEquals, test.expectedResponse)
})
}
})
Expand Down
Loading

0 comments on commit b13b28e

Please sign in to comment.