Add API endpoints to get feeds and entries of a category

This commit is contained in:
Shizun Ge 2021-01-18 21:44:02 -06:00 committed by GitHub
parent 02a4c9db53
commit 4ff52bd730
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 275 additions and 38 deletions

View file

@ -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.updateCategory).Methods(http.MethodPut)
sr.HandleFunc("/categories/{categoryID}", handler.removeCategory).Methods(http.MethodDelete) 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}/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("/discover", handler.discoverSubscriptions).Methods(http.MethodPost)
sr.HandleFunc("/feeds", handler.createFeed).Methods(http.MethodPost) sr.HandleFunc("/feeds", handler.createFeed).Methods(http.MethodPost)
sr.HandleFunc("/feeds", handler.getFeeds).Methods(http.MethodGet) sr.HandleFunc("/feeds", handler.getFeeds).Methods(http.MethodGet)

View file

@ -17,6 +17,21 @@ import (
"miniflux.app/validator" "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) { func (h *handler) getFeedEntry(w http.ResponseWriter, r *http.Request) {
feedID := request.RouteInt64Param(r, "feedID") feedID := request.RouteInt64Param(r, "feedID")
entryID := request.RouteInt64Param(r, "entryID") entryID := request.RouteInt64Param(r, "entryID")
@ -25,18 +40,18 @@ func (h *handler) getFeedEntry(w http.ResponseWriter, r *http.Request) {
builder.WithFeedID(feedID) builder.WithFeedID(feedID)
builder.WithEntryID(entryID) builder.WithEntryID(entryID)
entry, err := builder.GetEntry() getEntryFromBuilder(w, r, builder)
if err != nil { }
json.ServerError(w, r, err)
return
}
if entry == nil { func (h *handler) getCategoryEntry(w http.ResponseWriter, r *http.Request) {
json.NotFound(w, r) categoryID := request.RouteInt64Param(r, "categoryID")
return 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) { 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 := h.store.NewEntryQueryBuilder(request.UserID(r))
builder.WithEntryID(entryID) builder.WithEntryID(entryID)
entry, err := builder.GetEntry() getEntryFromBuilder(w, r, builder)
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) getFeedEntries(w http.ResponseWriter, r *http.Request) { func (h *handler) getFeedEntries(w http.ResponseWriter, r *http.Request) {
feedID := request.RouteInt64Param(r, "feedID") 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) { 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") statuses := request.QueryStringParamList(r, "status")
for _, status := range statuses { for _, status := range statuses {
if err := validator.ValidateEntryStatus(status); err != nil { 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) 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) { if categoryID > 0 && !h.store.CategoryIDExists(userID, categoryID) {
json.BadRequest(w, r, errors.New("Invalid category ID")) json.BadRequest(w, r, errors.New("Invalid category ID"))
return return

View file

@ -136,6 +136,30 @@ func (h *handler) markFeedAsRead(w http.ResponseWriter, r *http.Request) {
json.NoContent(w, r) 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) { func (h *handler) getFeeds(w http.ResponseWriter, r *http.Request) {
feeds, err := h.store.Feeds(request.UserID(r)) feeds, err := h.store.Feeds(request.UserID(r))
if err != nil { if err != nil {

View file

@ -223,6 +223,23 @@ func (c *Client) MarkCategoryAsRead(categoryID int64) error {
return err 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. // DeleteCategory removes a category.
func (c *Client) DeleteCategory(categoryID int64) error { func (c *Client) DeleteCategory(categoryID int64) error {
return c.request.Delete(fmt.Sprintf("/v1/categories/%d", categoryID)) 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 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. // Entry gets a single entry.
func (c *Client) Entry(entryID int64) (*Entry, error) { func (c *Client) Entry(entryID int64) (*Entry, error) {
body, err := c.request.Get(fmt.Sprintf("/v1/entries/%d", entryID)) 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 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. // UpdateEntries updates the status of a list of entries.
func (c *Client) UpdateEntries(entryIDs []int64, status string) error { func (c *Client) UpdateEntries(entryIDs []int64, status string) error {
type payload struct { type payload struct {

View file

@ -48,7 +48,47 @@ func TestGetAllFeedEntries(t *testing.T) {
} }
if filteredResultsByEntryID.Entries[0].ID == allResults.Entries[0].ID { 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) { func TestFilterEntriesByStatuses(t *testing.T) {
client := createClient(t) client := createClient(t)
category, err := client.CreateCategory("Test Filter by statuses") 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) { func TestGetEntry(t *testing.T) {
client := createClient(t) client := createClient(t)
createFeed(t, client) createFeed(t, client)
@ -238,20 +353,11 @@ func TestGetEntry(t *testing.T) {
t.Fatal(err) 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 { if err != nil {
t.Fatal(err) 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 { if entry.ID != result.Entries[0].ID {
t.Fatal("Wrong entry returned") t.Fatal("Wrong entry returned")
} }

View file

@ -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) 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)
}
}