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.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)

View file

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

View file

@ -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 {

View file

@ -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 {

View file

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

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