From 8781648af9f730d8bd1a7d9c395c1f28f9058716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Guillot?= Date: Sun, 26 Nov 2017 15:07:59 -0800 Subject: [PATCH] Add integration tests for entries --- Gopkg.lock | 4 +- README.md | 3 +- integration_test.go | 206 ++++++++++++++++++ model/entry.go | 13 ++ model/entry_test.go | 14 ++ server/api/controller/entry.go | 65 +++++- server/routes.go | 1 + vendor/github.com/lib/pq/.travis.yml | 14 +- .../github.com/miniflux/miniflux-go/client.go | 79 ++++--- 9 files changed, 356 insertions(+), 43 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 3cedbc88..cf608e81 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -35,13 +35,13 @@ branch = "master" name = "github.com/lib/pq" packages = [".","hstore","oid"] - revision = "8c6ee72f3e6bcb1542298dd5f76cb74af9742cec" + revision = "83612a56d3dd153a94a629cd64925371c9adad78" [[projects]] branch = "master" name = "github.com/miniflux/miniflux-go" packages = ["."] - revision = "2efd82e81054cf01433e81c419a7c84e62e6a52c" + revision = "c5788cd2d2248ee9fc148f3852dda7e24fe54cfa" [[projects]] name = "github.com/tdewolff/minify" diff --git a/README.md b/README.md index a8712fe2..308efcb4 100644 --- a/README.md +++ b/README.md @@ -29,11 +29,12 @@ TODO - [X] Bookmarklet - [ ] External integrations (Pinboard, Wallabag...) - [ ] Gzip compression -- [ ] Integration tests +- [X] Integration tests - [X] Flush history - [X] OAuth2 - [ ] Bookmarks - [ ] Touch events +- [ ] Fever API? Credits ------- diff --git a/integration_test.go b/integration_test.go index c9974322..1acffa83 100644 --- a/integration_test.go +++ b/integration_test.go @@ -771,6 +771,212 @@ func TestGetFeeds(t *testing.T) { } } +func TestGetAllFeedEntries(t *testing.T) { + username := getRandomUsername() + client := miniflux.NewClient(testBaseURL, testAdminUsername, testAdminPassword) + _, err := client.CreateUser(username, testStandardPassword, false) + if err != nil { + t.Fatal(err) + } + + client = miniflux.NewClient(testBaseURL, username, testStandardPassword) + categories, err := client.Categories() + if err != nil { + t.Fatal(err) + } + + feedID, err := client.CreateFeed("https://miniflux.net/feed", categories[0].ID) + if err != nil { + t.Fatal(err) + } + + allResults, err := client.FeedEntries(feedID, nil) + if err != nil { + t.Fatal(err) + } + + if allResults.Total == 0 { + t.Fatal(`Invalid number of entries`) + } + + if allResults.Entries[0].Title == "" { + t.Fatal(`Invalid entry title`) + } + + filteredResults, err := client.FeedEntries(feedID, &miniflux.Filter{Limit: 1, Offset: 5}) + if err != nil { + t.Fatal(err) + } + + if allResults.Total != filteredResults.Total { + t.Fatal(`Total should always contains the total number of items regardless of filters`) + } + + if allResults.Entries[0].ID == filteredResults.Entries[0].ID { + t.Fatal(`Filtered entries should be different than previous result`) + } +} + +func TestGetAllEntries(t *testing.T) { + username := getRandomUsername() + client := miniflux.NewClient(testBaseURL, testAdminUsername, testAdminPassword) + _, err := client.CreateUser(username, testStandardPassword, false) + if err != nil { + t.Fatal(err) + } + + client = miniflux.NewClient(testBaseURL, username, testStandardPassword) + categories, err := client.Categories() + if err != nil { + t.Fatal(err) + } + + _, err = client.CreateFeed("https://miniflux.net/feed", categories[0].ID) + if err != nil { + t.Fatal(err) + } + + resultWithoutSorting, err := client.Entries(nil) + if err != nil { + t.Fatal(err) + } + + if resultWithoutSorting.Total == 0 { + t.Fatal(`Invalid number of entries`) + } + + resultWithStatusFilter, err := client.Entries(&miniflux.Filter{Status: miniflux.EntryStatusRead}) + if err != nil { + t.Fatal(err) + } + + if resultWithStatusFilter.Total != 0 { + t.Fatal(`We should have 0 read entries`) + } + + resultWithDifferentSorting, err := client.Entries(&miniflux.Filter{Order: "published_at", Direction: "asc"}) + if err != nil { + t.Fatal(err) + } + + if resultWithDifferentSorting.Entries[0].Title == resultWithoutSorting.Entries[0].Title { + t.Fatalf(`The items should be sorted differently "%v" vs "%v"`, resultWithDifferentSorting.Entries[0].Title, resultWithoutSorting.Entries[0].Title) + } +} + +func TestInvalidFilters(t *testing.T) { + username := getRandomUsername() + client := miniflux.NewClient(testBaseURL, testAdminUsername, testAdminPassword) + _, err := client.CreateUser(username, testStandardPassword, false) + if err != nil { + t.Fatal(err) + } + + client = miniflux.NewClient(testBaseURL, username, testStandardPassword) + categories, err := client.Categories() + if err != nil { + t.Fatal(err) + } + + _, err = client.CreateFeed("https://miniflux.net/feed", categories[0].ID) + if err != nil { + t.Fatal(err) + } + + _, err = client.Entries(&miniflux.Filter{Status: "invalid"}) + if err == nil { + t.Fatal(`Using invalid status should raise an error`) + } + + _, err = client.Entries(&miniflux.Filter{Direction: "invalid"}) + if err == nil { + t.Fatal(`Using invalid direction should raise an error`) + } + + _, err = client.Entries(&miniflux.Filter{Order: "invalid"}) + if err == nil { + t.Fatal(`Using invalid order should raise an error`) + } +} + +func TestGetEntry(t *testing.T) { + username := getRandomUsername() + client := miniflux.NewClient(testBaseURL, testAdminUsername, testAdminPassword) + _, err := client.CreateUser(username, testStandardPassword, false) + if err != nil { + t.Fatal(err) + } + + client = miniflux.NewClient(testBaseURL, username, testStandardPassword) + categories, err := client.Categories() + if err != nil { + t.Fatal(err) + } + + _, err = client.CreateFeed("https://miniflux.net/feed", categories[0].ID) + if err != nil { + t.Fatal(err) + } + + result, err := client.Entries(&miniflux.Filter{Limit: 1}) + if err != nil { + t.Fatal(err) + } + + entry, err := client.Entry(result.Entries[0].FeedID, result.Entries[0].ID) + if err != nil { + t.Fatal(err) + } + + if entry.ID != result.Entries[0].ID { + t.Fatal("Wrong entry returned") + } +} + +func TestUpdateStatus(t *testing.T) { + username := getRandomUsername() + client := miniflux.NewClient(testBaseURL, testAdminUsername, testAdminPassword) + _, err := client.CreateUser(username, testStandardPassword, false) + if err != nil { + t.Fatal(err) + } + + client = miniflux.NewClient(testBaseURL, username, testStandardPassword) + categories, err := client.Categories() + if err != nil { + t.Fatal(err) + } + + _, err = client.CreateFeed("https://miniflux.net/feed", categories[0].ID) + if err != nil { + t.Fatal(err) + } + + result, err := client.Entries(&miniflux.Filter{Limit: 1}) + if err != nil { + t.Fatal(err) + } + + err = client.UpdateEntries([]int64{result.Entries[0].ID}, miniflux.EntryStatusRead) + if err != nil { + t.Fatal(err) + } + + entry, err := client.Entry(result.Entries[0].FeedID, result.Entries[0].ID) + if err != nil { + t.Fatal(err) + } + + if entry.Status != miniflux.EntryStatusRead { + t.Fatal("The entry status should be updated") + } + + err = client.UpdateEntries([]int64{result.Entries[0].ID}, "invalid") + if err == nil { + t.Fatal(`Invalid entry status should ne be accepted`) + } +} + func getRandomUsername() string { rand.Seed(time.Now().UnixNano()) var suffix []string diff --git a/model/entry.go b/model/entry.go index 1053130b..79a3bcbb 100644 --- a/model/entry.go +++ b/model/entry.go @@ -68,6 +68,19 @@ func ValidateDirection(direction string) error { return fmt.Errorf(`Invalid direction, valid direction values are: "asc" or "desc"`) } +// ValidateRange makes sure the offset/limit values are valid. +func ValidateRange(offset, limit int) error { + if offset < 0 { + return fmt.Errorf(`Offset value should be >= 0`) + } + + if limit < 0 { + return fmt.Errorf(`Limit value should be >= 0`) + } + + return nil +} + // GetOppositeDirection returns the opposite sorting direction. func GetOppositeDirection(direction string) string { if direction == "asc" { diff --git a/model/entry_test.go b/model/entry_test.go index 3f2e196e..2f8c25de 100644 --- a/model/entry_test.go +++ b/model/entry_test.go @@ -42,6 +42,20 @@ func TestValidateEntryDirection(t *testing.T) { } } +func TestValidateRange(t *testing.T) { + if err := ValidateRange(-1, 0); err == nil { + t.Error(`An invalid offset should generate a error`) + } + + if err := ValidateRange(0, -1); err == nil { + t.Error(`An invalid limit should generate a error`) + } + + if err := ValidateRange(42, 42); err != nil { + t.Error(`A valid offset and limit should not generate any error`) + } +} + func TestGetOppositeDirection(t *testing.T) { if GetOppositeDirection("asc") != "desc" { t.Errorf(`The opposite direction of "asc" should be "desc"`) diff --git a/server/api/controller/entry.go b/server/api/controller/entry.go index f5833783..b09cc8c9 100644 --- a/server/api/controller/entry.go +++ b/server/api/controller/entry.go @@ -62,13 +62,13 @@ func (c *Controller) GetFeedEntries(ctx *core.Context, request *core.Request, re } } - order := request.QueryStringParam("order", "id") + order := request.QueryStringParam("order", model.DefaultSortingOrder) if err := model.ValidateEntryOrder(order); err != nil { response.JSON().BadRequest(err) return } - direction := request.QueryStringParam("direction", "desc") + direction := request.QueryStringParam("direction", model.DefaultSortingDirection) if err := model.ValidateDirection(direction); err != nil { response.JSON().BadRequest(err) return @@ -76,12 +76,69 @@ func (c *Controller) GetFeedEntries(ctx *core.Context, request *core.Request, re limit := request.QueryIntegerParam("limit", 100) offset := request.QueryIntegerParam("offset", 0) + if err := model.ValidateRange(offset, limit); err != nil { + response.JSON().BadRequest(err) + return + } builder := c.store.GetEntryQueryBuilder(userID, ctx.UserTimezone()) builder.WithFeedID(feedID) builder.WithStatus(status) - builder.WithOrder(model.DefaultSortingOrder) - builder.WithDirection(model.DefaultSortingDirection) + builder.WithOrder(order) + builder.WithDirection(direction) + builder.WithOffset(offset) + builder.WithLimit(limit) + + entries, err := builder.GetEntries() + if err != nil { + response.JSON().ServerError(errors.New("Unable to fetch the list of entries")) + return + } + + count, err := builder.CountEntries() + if err != nil { + response.JSON().ServerError(errors.New("Unable to count the number of entries")) + return + } + + response.JSON().Standard(&payload.EntriesResponse{Total: count, Entries: entries}) +} + +// GetEntries is the API handler to fetch entries. +func (c *Controller) GetEntries(ctx *core.Context, request *core.Request, response *core.Response) { + userID := ctx.UserID() + + status := request.QueryStringParam("status", "") + if status != "" { + if err := model.ValidateEntryStatus(status); err != nil { + response.JSON().BadRequest(err) + return + } + } + + order := request.QueryStringParam("order", model.DefaultSortingOrder) + if err := model.ValidateEntryOrder(order); err != nil { + response.JSON().BadRequest(err) + return + } + + direction := request.QueryStringParam("direction", model.DefaultSortingDirection) + if err := model.ValidateDirection(direction); err != nil { + response.JSON().BadRequest(err) + return + } + + limit := request.QueryIntegerParam("limit", 100) + offset := request.QueryIntegerParam("offset", 0) + if err := model.ValidateRange(offset, limit); err != nil { + response.JSON().BadRequest(err) + return + } + + builder := c.store.GetEntryQueryBuilder(userID, ctx.UserTimezone()) + builder.WithStatus(status) + builder.WithOrder(order) + builder.WithDirection(direction) builder.WithOffset(offset) builder.WithLimit(limit) diff --git a/server/routes.go b/server/routes.go index 728e4ad6..8d5a5c51 100644 --- a/server/routes.go +++ b/server/routes.go @@ -62,6 +62,7 @@ func getRoutes(cfg *config.Config, store *storage.Storage, feedHandler *feed.Han router.Handle("/v1/feeds/{feedID}/entries", apiHandler.Use(apiController.GetFeedEntries)).Methods("GET") router.Handle("/v1/feeds/{feedID}/entries/{entryID}", apiHandler.Use(apiController.GetEntry)).Methods("GET") + router.Handle("/v1/entries", apiHandler.Use(apiController.GetEntries)).Methods("GET") router.Handle("/v1/entries", apiHandler.Use(apiController.SetEntryStatus)).Methods("PUT") router.Handle("/stylesheets/{name}.css", uiHandler.Use(uiController.Stylesheet)).Name("stylesheet").Methods("GET") diff --git a/vendor/github.com/lib/pq/.travis.yml b/vendor/github.com/lib/pq/.travis.yml index 01468f05..4e34e885 100644 --- a/vendor/github.com/lib/pq/.travis.yml +++ b/vendor/github.com/lib/pq/.travis.yml @@ -16,7 +16,7 @@ env: - PQGOSSLTESTS=1 - PQSSLCERTTEST_PATH=$PWD/certs - PGHOST=127.0.0.1 - - MEGACHECK_VERSION=2017.1 + - MEGACHECK_VERSION=2017.2.1 matrix: - PGVERSION=10 - PGVERSION=9.6 @@ -46,15 +46,13 @@ script: - > goimports -d -e $(find -name '*.go') | awk '{ print } END { exit NR == 0 ? 0 : 1 }' - go vet ./... - # For compatibility with Go 1.5, launch only if megacheck is present, - # ignore SA1019 (deprecation warnings) in conn_test.go (we have to use the - # deprecated driver.Execer and driver.Queryer interfaces) and S1024 - # (time.Until) everywhere. + # For compatibility with Go 1.5, launch only if megacheck is present. - > - which megacheck > /dev/null - && megacheck -ignore 'github.com/lib/pq/conn_test.go:SA1019 github.com/lib/pq/*.go:S1024' ./... + which megacheck > /dev/null && megacheck -go 1.5 ./... || echo 'megacheck is not supported, skipping check' # For compatibility with Go 1.5, launch only if golint is present. - - which golint > /dev/null && golint ./... || echo 'golint is not supported, skipping check' + - > + which golint > /dev/null && golint ./... + || echo 'golint is not supported, skipping check' - PQTEST_BINARY_PARAMETERS=no go test -race -v ./... - PQTEST_BINARY_PARAMETERS=yes go test -race -v ./... diff --git a/vendor/github.com/miniflux/miniflux-go/client.go b/vendor/github.com/miniflux/miniflux-go/client.go index 6a5f678c..b43b8a93 100644 --- a/vendor/github.com/miniflux/miniflux-go/client.go +++ b/vendor/github.com/miniflux/miniflux-go/client.go @@ -196,7 +196,7 @@ func (c *Client) Feeds() (Feeds, error) { return feeds, nil } -// Feed gets a new feed. +// Feed gets a feed. func (c *Client) Feed(feedID int64) (*Feed, error) { body, err := c.request.Get(fmt.Sprintf("/v1/feeds/%d", feedID)) if err != nil { @@ -291,35 +291,28 @@ func (c *Client) Entry(feedID, entryID int64) (*Entry, error) { return entry, nil } -// Entries gets feed entries. -func (c *Client) Entries(feedID int64, filter *Filter) (*EntryResultSet, error) { - path := fmt.Sprintf("/v1/feeds/%d/entries", feedID) +// Entries fetch entries. +func (c *Client) Entries(filter *Filter) (*EntryResultSet, error) { + path := buildFilterQueryString("/v1/entries", filter) - if filter != nil { - values := url.Values{} - - if filter.Status != "" { - values.Set("status", filter.Status) - } - - if filter.Direction != "" { - values.Set("direction", filter.Direction) - } - - if filter.Order != "" { - values.Set("order", filter.Order) - } - - if filter.Limit != 0 { - values.Set("limit", strconv.Itoa(filter.Limit)) - } - - if filter.Offset != 0 { - values.Set("offset", strconv.Itoa(filter.Offset)) - } - - path = fmt.Sprintf("%s?%s", path, values.Encode()) + body, err := c.request.Get(path) + if err != nil { + return nil, err } + defer body.Close() + + var result EntryResultSet + decoder := json.NewDecoder(body) + if err := decoder.Decode(&result); err != nil { + return nil, fmt.Errorf("miniflux: response error (%v)", err) + } + + return &result, nil +} + +// FeedEntries fetch feed entries. +func (c *Client) FeedEntries(feedID int64, filter *Filter) (*EntryResultSet, error) { + path := buildFilterQueryString(fmt.Sprintf("/v1/feeds/%d/entries", feedID), filter) body, err := c.request.Get(path) if err != nil { @@ -356,3 +349,33 @@ func (c *Client) UpdateEntries(entryIDs []int64, status string) error { func NewClient(endpoint, username, password string) *Client { return &Client{request: &request{endpoint: endpoint, username: username, password: password}} } + +func buildFilterQueryString(path string, filter *Filter) string { + if filter != nil { + values := url.Values{} + + if filter.Status != "" { + values.Set("status", filter.Status) + } + + if filter.Direction != "" { + values.Set("direction", filter.Direction) + } + + if filter.Order != "" { + values.Set("order", filter.Order) + } + + if filter.Limit >= 0 { + values.Set("limit", strconv.Itoa(filter.Limit)) + } + + if filter.Offset >= 0 { + values.Set("offset", strconv.Itoa(filter.Offset)) + } + + path = fmt.Sprintf("%s?%s", path, values.Encode()) + } + + return path +}