diff --git a/api/api.go b/api/api.go index c8dd9a55..0aaba5a7 100644 --- a/api/api.go +++ b/api/api.go @@ -41,6 +41,9 @@ func Serve(router *mux.Router, store *storage.Storage, pool *worker.Pool) { sr.HandleFunc("/categories/{categoryID}", handler.updateCategory).Methods(http.MethodPut) sr.HandleFunc("/categories/{categoryID}", handler.removeCategory).Methods(http.MethodDelete) sr.HandleFunc("/categories/{categoryID}/mark-all-as-read", handler.markCategoryAsRead).Methods(http.MethodPut) + sr.HandleFunc("/categories/{categoryID}/feeds", handler.getCategoryFeeds).Methods(http.MethodGet) + sr.HandleFunc("/categories/{categoryID}/entries", handler.getCategoryEntries).Methods(http.MethodGet) + sr.HandleFunc("/categories/{categoryID}/entries/{entryID}", handler.getCategoryEntry).Methods(http.MethodGet) sr.HandleFunc("/discover", handler.discoverSubscriptions).Methods(http.MethodPost) sr.HandleFunc("/feeds", handler.createFeed).Methods(http.MethodPost) sr.HandleFunc("/feeds", handler.getFeeds).Methods(http.MethodGet) diff --git a/api/entry.go b/api/entry.go index ad4032ae..55542584 100644 --- a/api/entry.go +++ b/api/entry.go @@ -17,6 +17,21 @@ import ( "miniflux.app/validator" ) +func getEntryFromBuilder(w http.ResponseWriter, r *http.Request, b *storage.EntryQueryBuilder) { + entry, err := b.GetEntry() + if err != nil { + json.ServerError(w, r, err) + return + } + + if entry == nil { + json.NotFound(w, r) + return + } + + json.OK(w, r, entry) +} + func (h *handler) getFeedEntry(w http.ResponseWriter, r *http.Request) { feedID := request.RouteInt64Param(r, "feedID") entryID := request.RouteInt64Param(r, "entryID") @@ -25,18 +40,18 @@ func (h *handler) getFeedEntry(w http.ResponseWriter, r *http.Request) { builder.WithFeedID(feedID) builder.WithEntryID(entryID) - entry, err := builder.GetEntry() - if err != nil { - json.ServerError(w, r, err) - return - } + getEntryFromBuilder(w, r, builder) +} - if entry == nil { - json.NotFound(w, r) - return - } +func (h *handler) getCategoryEntry(w http.ResponseWriter, r *http.Request) { + categoryID := request.RouteInt64Param(r, "categoryID") + entryID := request.RouteInt64Param(r, "entryID") - json.OK(w, r, entry) + builder := h.store.NewEntryQueryBuilder(request.UserID(r)) + builder.WithCategoryID(categoryID) + builder.WithEntryID(entryID) + + getEntryFromBuilder(w, r, builder) } func (h *handler) getEntry(w http.ResponseWriter, r *http.Request) { @@ -44,30 +59,24 @@ func (h *handler) getEntry(w http.ResponseWriter, r *http.Request) { builder := h.store.NewEntryQueryBuilder(request.UserID(r)) builder.WithEntryID(entryID) - entry, err := builder.GetEntry() - if err != nil { - json.ServerError(w, r, err) - return - } - - if entry == nil { - json.NotFound(w, r) - return - } - - json.OK(w, r, entry) + getEntryFromBuilder(w, r, builder) } func (h *handler) getFeedEntries(w http.ResponseWriter, r *http.Request) { feedID := request.RouteInt64Param(r, "feedID") - h.findEntries(w, r, feedID) + h.findEntries(w, r, feedID, 0) +} + +func (h *handler) getCategoryEntries(w http.ResponseWriter, r *http.Request) { + categoryID := request.RouteInt64Param(r, "categoryID") + h.findEntries(w, r, 0, categoryID) } func (h *handler) getEntries(w http.ResponseWriter, r *http.Request) { - h.findEntries(w, r, 0) + h.findEntries(w, r, 0, 0) } -func (h *handler) findEntries(w http.ResponseWriter, r *http.Request, feedID int64) { +func (h *handler) findEntries(w http.ResponseWriter, r *http.Request, feedID int64, categoryID int64) { statuses := request.QueryStringParamList(r, "status") for _, status := range statuses { if err := validator.ValidateEntryStatus(status); err != nil { @@ -96,7 +105,7 @@ func (h *handler) findEntries(w http.ResponseWriter, r *http.Request, feedID int } userID := request.UserID(r) - categoryID := request.QueryInt64Param(r, "category_id", 0) + categoryID = request.QueryInt64Param(r, "category_id", categoryID) if categoryID > 0 && !h.store.CategoryIDExists(userID, categoryID) { json.BadRequest(w, r, errors.New("Invalid category ID")) return diff --git a/api/feed.go b/api/feed.go index 192aedfe..87270ef0 100644 --- a/api/feed.go +++ b/api/feed.go @@ -136,6 +136,30 @@ func (h *handler) markFeedAsRead(w http.ResponseWriter, r *http.Request) { json.NoContent(w, r) } +func (h *handler) getCategoryFeeds(w http.ResponseWriter, r *http.Request) { + userID := request.UserID(r) + categoryID := request.RouteInt64Param(r, "categoryID") + + category, err := h.store.Category(userID, categoryID) + if err != nil { + json.ServerError(w, r, err) + return + } + + if category == nil { + json.NotFound(w, r) + return + } + + feeds, err := h.store.FeedsByCategoryWithCounters(userID, categoryID) + if err != nil { + json.ServerError(w, r, err) + return + } + + json.OK(w, r, feeds) +} + func (h *handler) getFeeds(w http.ResponseWriter, r *http.Request) { feeds, err := h.store.Feeds(request.UserID(r)) if err != nil { diff --git a/client/client.go b/client/client.go index df34a0f8..ee67f70e 100644 --- a/client/client.go +++ b/client/client.go @@ -223,6 +223,23 @@ func (c *Client) MarkCategoryAsRead(categoryID int64) error { return err } +// CategoryFeeds gets feeds of a cateogry. +func (c *Client) CategoryFeeds(categoryID int64) (Feeds, error) { + body, err := c.request.Get(fmt.Sprintf("/v1/categories/%d/feeds", categoryID)) + if err != nil { + return nil, err + } + defer body.Close() + + var feeds Feeds + decoder := json.NewDecoder(body) + if err := decoder.Decode(&feeds); err != nil { + return nil, fmt.Errorf("miniflux: response error (%v)", err) + } + + return feeds, nil +} + // DeleteCategory removes a category. func (c *Client) DeleteCategory(categoryID int64) error { return c.request.Delete(fmt.Sprintf("/v1/categories/%d", categoryID)) @@ -378,6 +395,23 @@ func (c *Client) FeedEntry(feedID, entryID int64) (*Entry, error) { return entry, nil } +// CategoryEntry gets a single category entry. +func (c *Client) CategoryEntry(categoryID, entryID int64) (*Entry, error) { + body, err := c.request.Get(fmt.Sprintf("/v1/categories/%d/entries/%d", categoryID, entryID)) + if err != nil { + return nil, err + } + defer body.Close() + + var entry *Entry + decoder := json.NewDecoder(body) + if err := decoder.Decode(&entry); err != nil { + return nil, fmt.Errorf("miniflux: response error (%v)", err) + } + + return entry, nil +} + // Entry gets a single entry. func (c *Client) Entry(entryID int64) (*Entry, error) { body, err := c.request.Get(fmt.Sprintf("/v1/entries/%d", entryID)) @@ -433,6 +467,25 @@ func (c *Client) FeedEntries(feedID int64, filter *Filter) (*EntryResultSet, err return &result, nil } +// CategoryEntries fetch entries of a category. +func (c *Client) CategoryEntries(categoryID int64, filter *Filter) (*EntryResultSet, error) { + path := buildFilterQueryString(fmt.Sprintf("/v1/categories/%d/entries", categoryID), filter) + + 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 +} + // UpdateEntries updates the status of a list of entries. func (c *Client) UpdateEntries(entryIDs []int64, status string) error { type payload struct { diff --git a/tests/entry_test.go b/tests/entry_test.go index 001ae5a1..253e02d2 100644 --- a/tests/entry_test.go +++ b/tests/entry_test.go @@ -48,7 +48,47 @@ func TestGetAllFeedEntries(t *testing.T) { } if filteredResultsByEntryID.Entries[0].ID == allResults.Entries[0].ID { - t.Fatal(`The first entry should filtered out`) + t.Fatal(`The first entry should be filtered out`) + } +} + +func TestGetAllCategoryEntries(t *testing.T) { + client := createClient(t) + _, category := createFeed(t, client) + + allResults, err := client.CategoryEntries(category.ID, 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.CategoryEntries(category.ID, &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 results`) + } + + filteredResultsByEntryID, err := client.CategoryEntries(category.ID, &miniflux.Filter{BeforeEntryID: allResults.Entries[0].ID}) + if err != nil { + t.Fatal(err) + } + + if filteredResultsByEntryID.Entries[0].ID == allResults.Entries[0].ID { + t.Fatal(`The first entry should be filtered out`) } } @@ -130,6 +170,43 @@ func TestFilterEntriesByCategory(t *testing.T) { } } +func TestFilterEntriesByFeed(t *testing.T) { + client := createClient(t) + category, err := client.CreateCategory("Test Filter by Feed") + if err != nil { + t.Fatal(err) + } + + feedID, err := client.CreateFeed(&miniflux.FeedCreationRequest{ + FeedURL: testFeedURL, + CategoryID: category.ID, + }) + if err != nil { + t.Fatal(err) + } + + if feedID == 0 { + t.Fatalf(`Invalid feed ID, got %q`, feedID) + } + + results, err := client.Entries(&miniflux.Filter{FeedID: feedID}) + if err != nil { + t.Fatal(err) + } + + if results.Total == 0 { + t.Fatalf(`We should have more than one entry`) + } + + if results.Entries[0].Feed.Category == nil { + t.Fatalf(`The entry feed category should not be nil`) + } + + if results.Entries[0].Feed.Category.ID != category.ID { + t.Errorf(`Entries should be filtered by category_id=%d`, category.ID) + } +} + func TestFilterEntriesByStatuses(t *testing.T) { client := createClient(t) category, err := client.CreateCategory("Test Filter by statuses") @@ -229,6 +306,44 @@ func TestInvalidFilters(t *testing.T) { } } +func TestGetFeedEntry(t *testing.T) { + client := createClient(t) + createFeed(t, client) + + result, err := client.Entries(&miniflux.Filter{Limit: 1}) + if err != nil { + t.Fatal(err) + } + + // Test get entry by entry id and feed id + entry, err := client.FeedEntry(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 TestGetCategoryEntry(t *testing.T) { + client := createClient(t) + _, category := createFeed(t, client) + + result, err := client.Entries(&miniflux.Filter{Limit: 1}) + if err != nil { + t.Fatal(err) + } + + // Test get entry by entry id and category id + entry, err := client.CategoryEntry(category.ID, result.Entries[0].ID) + if err != nil { + t.Fatal(err) + } + if entry.ID != result.Entries[0].ID { + t.Fatal("Wrong entry returned") + } +} + func TestGetEntry(t *testing.T) { client := createClient(t) createFeed(t, client) @@ -238,20 +353,11 @@ func TestGetEntry(t *testing.T) { t.Fatal(err) } - entry, err := client.FeedEntry(result.Entries[0].FeedID, result.Entries[0].ID) + // Test get entry by entry id only + entry, err := client.Entry(result.Entries[0].ID) if err != nil { t.Fatal(err) } - - if entry.ID != result.Entries[0].ID { - t.Fatal("Wrong entry returned") - } - - entry, err = client.Entry(result.Entries[0].ID) - if err != nil { - t.Fatal(err) - } - if entry.ID != result.Entries[0].ID { t.Fatal("Wrong entry returned") } diff --git a/tests/feed_test.go b/tests/feed_test.go index b01c346c..f1fdad1d 100644 --- a/tests/feed_test.go +++ b/tests/feed_test.go @@ -661,3 +661,45 @@ func TestGetFeeds(t *testing.T) { t.Fatalf(`Invalid feed category title, got "%v" instead of "%v"`, feeds[0].Category.Title, category.Title) } } + +func TestGetFeedsByCategory(t *testing.T) { + client := createClient(t) + feed, category := createFeed(t, client) + + feeds, err := client.CategoryFeeds(category.ID) + if err != nil { + t.Fatal(err) + } + + if len(feeds) != 1 { + t.Fatalf(`Invalid number of feeds`) + } + + if feeds[0].ID != feed.ID { + t.Fatalf(`Invalid feed ID, got "%v" instead of "%v"`, feeds[0].ID, feed.ID) + } + + if feeds[0].Title != testFeedTitle { + t.Fatalf(`Invalid feed title, got "%v" instead of "%v"`, feeds[0].Title, testFeedTitle) + } + + if feeds[0].SiteURL != testWebsiteURL { + t.Fatalf(`Invalid site URL, got "%v" instead of "%v"`, feeds[0].SiteURL, testWebsiteURL) + } + + if feeds[0].FeedURL != testFeedURL { + t.Fatalf(`Invalid feed URL, got "%v" instead of "%v"`, feeds[0].FeedURL, testFeedURL) + } + + if feeds[0].Category.ID != category.ID { + t.Fatalf(`Invalid feed category ID, got "%v" instead of "%v"`, feeds[0].Category.ID, category.ID) + } + + if feeds[0].Category.UserID != category.UserID { + t.Fatalf(`Invalid feed category user ID, got "%v" instead of "%v"`, feeds[0].Category.UserID, category.UserID) + } + + if feeds[0].Category.Title != category.Title { + t.Fatalf(`Invalid feed category title, got "%v" instead of "%v"`, feeds[0].Category.Title, category.Title) + } +}