Skip to content
Snippets Groups Projects
Commit e17d395a authored by Benjamin Congdon's avatar Benjamin Congdon Committed by fguillot
Browse files

Add API routes for "mark all as read"

parent eeeea74b
No related branches found
No related tags found
No related merge requests found
...@@ -29,12 +29,14 @@ func Serve(router *mux.Router, store *storage.Storage, pool *worker.Pool, feedHa ...@@ -29,12 +29,14 @@ func Serve(router *mux.Router, store *storage.Storage, pool *worker.Pool, feedHa
sr.HandleFunc("/users/{userID:[0-9]+}", handler.userByID).Methods(http.MethodGet) sr.HandleFunc("/users/{userID:[0-9]+}", handler.userByID).Methods(http.MethodGet)
sr.HandleFunc("/users/{userID:[0-9]+}", handler.updateUser).Methods(http.MethodPut) sr.HandleFunc("/users/{userID:[0-9]+}", handler.updateUser).Methods(http.MethodPut)
sr.HandleFunc("/users/{userID:[0-9]+}", handler.removeUser).Methods(http.MethodDelete) sr.HandleFunc("/users/{userID:[0-9]+}", handler.removeUser).Methods(http.MethodDelete)
sr.HandleFunc("/users/{userID:[0-9]+}/mark-all-as-read", handler.markUserAsRead).Methods(http.MethodPut)
sr.HandleFunc("/users/{username}", handler.userByUsername).Methods(http.MethodGet) sr.HandleFunc("/users/{username}", handler.userByUsername).Methods(http.MethodGet)
sr.HandleFunc("/me", handler.currentUser).Methods(http.MethodGet) sr.HandleFunc("/me", handler.currentUser).Methods(http.MethodGet)
sr.HandleFunc("/categories", handler.createCategory).Methods(http.MethodPost) sr.HandleFunc("/categories", handler.createCategory).Methods(http.MethodPost)
sr.HandleFunc("/categories", handler.getCategories).Methods(http.MethodGet) sr.HandleFunc("/categories", handler.getCategories).Methods(http.MethodGet)
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("/discover", handler.getSubscriptions).Methods(http.MethodPost) sr.HandleFunc("/discover", handler.getSubscriptions).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)
...@@ -44,6 +46,7 @@ func Serve(router *mux.Router, store *storage.Storage, pool *worker.Pool, feedHa ...@@ -44,6 +46,7 @@ func Serve(router *mux.Router, store *storage.Storage, pool *worker.Pool, feedHa
sr.HandleFunc("/feeds/{feedID}", handler.updateFeed).Methods(http.MethodPut) sr.HandleFunc("/feeds/{feedID}", handler.updateFeed).Methods(http.MethodPut)
sr.HandleFunc("/feeds/{feedID}", handler.removeFeed).Methods(http.MethodDelete) sr.HandleFunc("/feeds/{feedID}", handler.removeFeed).Methods(http.MethodDelete)
sr.HandleFunc("/feeds/{feedID}/icon", handler.feedIcon).Methods(http.MethodGet) sr.HandleFunc("/feeds/{feedID}/icon", handler.feedIcon).Methods(http.MethodGet)
sr.HandleFunc("/feeds/{feedID}/mark-all-as-read", handler.markFeedAsRead).Methods(http.MethodPut)
sr.HandleFunc("/export", handler.exportFeeds).Methods(http.MethodGet) sr.HandleFunc("/export", handler.exportFeeds).Methods(http.MethodGet)
sr.HandleFunc("/import", handler.importFeeds).Methods(http.MethodPost) sr.HandleFunc("/import", handler.importFeeds).Methods(http.MethodPost)
sr.HandleFunc("/feeds/{feedID}/entries", handler.getFeedEntries).Methods(http.MethodGet) sr.HandleFunc("/feeds/{feedID}/entries", handler.getFeedEntries).Methods(http.MethodGet)
......
...@@ -7,6 +7,7 @@ package api // import "miniflux.app/api" ...@@ -7,6 +7,7 @@ package api // import "miniflux.app/api"
import ( import (
"errors" "errors"
"net/http" "net/http"
"time"
"miniflux.app/http/request" "miniflux.app/http/request"
"miniflux.app/http/response/json" "miniflux.app/http/response/json"
...@@ -64,6 +65,29 @@ func (h *handler) updateCategory(w http.ResponseWriter, r *http.Request) { ...@@ -64,6 +65,29 @@ func (h *handler) updateCategory(w http.ResponseWriter, r *http.Request) {
json.Created(w, r, category) json.Created(w, r, category)
} }
func (h *handler) markCategoryAsRead(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
}
if err = h.store.MarkCategoryAsRead(userID, categoryID, time.Now()); err != nil {
json.ServerError(w, r, err)
return
}
json.NoContent(w, r)
}
func (h *handler) getCategories(w http.ResponseWriter, r *http.Request) { func (h *handler) getCategories(w http.ResponseWriter, r *http.Request) {
categories, err := h.store.Categories(request.UserID(r)) categories, err := h.store.Categories(request.UserID(r))
if err != nil { if err != nil {
......
...@@ -7,6 +7,7 @@ package api // import "miniflux.app/api" ...@@ -7,6 +7,7 @@ package api // import "miniflux.app/api"
import ( import (
"errors" "errors"
"net/http" "net/http"
"time"
"miniflux.app/http/request" "miniflux.app/http/request"
"miniflux.app/http/response/json" "miniflux.app/http/response/json"
...@@ -142,6 +143,29 @@ func (h *handler) updateFeed(w http.ResponseWriter, r *http.Request) { ...@@ -142,6 +143,29 @@ func (h *handler) updateFeed(w http.ResponseWriter, r *http.Request) {
json.Created(w, r, originalFeed) json.Created(w, r, originalFeed)
} }
func (h *handler) markFeedAsRead(w http.ResponseWriter, r *http.Request) {
feedID := request.RouteInt64Param(r, "feedID")
userID := request.UserID(r)
feed, err := h.store.FeedByID(userID, feedID)
if err != nil {
json.NotFound(w, r)
return
}
if feed == nil {
json.NotFound(w, r)
return
}
if err := h.store.MarkFeedAsRead(userID, feedID, time.Now()); err != nil {
json.ServerError(w, r, err)
return
}
json.NoContent(w, r)
}
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 {
......
...@@ -92,6 +92,26 @@ func (h *handler) updateUser(w http.ResponseWriter, r *http.Request) { ...@@ -92,6 +92,26 @@ func (h *handler) updateUser(w http.ResponseWriter, r *http.Request) {
json.Created(w, r, originalUser) json.Created(w, r, originalUser)
} }
func (h *handler) markUserAsRead(w http.ResponseWriter, r *http.Request) {
userID := request.RouteInt64Param(r, "userID")
if userID != request.UserID(r) {
json.Forbidden(w, r)
return
}
if _, err := h.store.UserByID(userID); err != nil {
json.NotFound(w, r)
return
}
if err := h.store.MarkAllAsRead(userID); err != nil {
json.ServerError(w, r, err)
return
}
json.NoContent(w, r)
}
func (h *handler) users(w http.ResponseWriter, r *http.Request) { func (h *handler) users(w http.ResponseWriter, r *http.Request) {
if !request.IsAdminUser(r) { if !request.IsAdminUser(r) {
json.Forbidden(w, r) json.Forbidden(w, r)
......
...@@ -133,6 +133,12 @@ func (c *Client) DeleteUser(userID int64) error { ...@@ -133,6 +133,12 @@ func (c *Client) DeleteUser(userID int64) error {
return c.request.Delete(fmt.Sprintf("/v1/users/%d", userID)) return c.request.Delete(fmt.Sprintf("/v1/users/%d", userID))
} }
// MarkAllAsRead marks all unread entries as read for a given user.
func (c *Client) MarkAllAsRead(userID int64) error {
_, err := c.request.Put(fmt.Sprintf("/v1/users/%d/mark-all-as-read", userID), nil)
return err
}
// Discover try to find subscriptions from a website. // Discover try to find subscriptions from a website.
func (c *Client) Discover(url string) (Subscriptions, error) { func (c *Client) Discover(url string) (Subscriptions, error) {
body, err := c.request.Post("/v1/discover", map[string]string{"url": url}) body, err := c.request.Post("/v1/discover", map[string]string{"url": url})
...@@ -207,6 +213,12 @@ func (c *Client) UpdateCategory(categoryID int64, title string) (*Category, erro ...@@ -207,6 +213,12 @@ func (c *Client) UpdateCategory(categoryID int64, title string) (*Category, erro
return category, nil return category, nil
} }
// MarkCategoryAsRead marks all unread entries in a category as read.
func (c *Client) MarkCategoryAsRead(categoryID int64) error {
_, err := c.request.Put(fmt.Sprintf("/v1/categories/%d/mark-all-as-read", categoryID), nil)
return err
}
// 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))
...@@ -309,6 +321,12 @@ func (c *Client) UpdateFeed(feedID int64, feedChanges *FeedModification) (*Feed, ...@@ -309,6 +321,12 @@ func (c *Client) UpdateFeed(feedID int64, feedChanges *FeedModification) (*Feed,
return f, nil return f, nil
} }
// MarkFeedAsRead marks all unread entries of the feed as read.
func (c *Client) MarkFeedAsRead(feedID int64) error {
_, err := c.request.Put(fmt.Sprintf("/v1/feeds/%d/mark-all-as-read", feedID), nil)
return err
}
// RefreshAllFeeds refreshes all feeds. // RefreshAllFeeds refreshes all feeds.
func (c *Client) RefreshAllFeeds() error { func (c *Client) RefreshAllFeeds() error {
_, err := c.request.Put(fmt.Sprintf("/v1/feeds/refresh"), nil) _, err := c.request.Put(fmt.Sprintf("/v1/feeds/refresh"), nil)
......
...@@ -8,6 +8,8 @@ package tests ...@@ -8,6 +8,8 @@ package tests
import ( import (
"testing" "testing"
miniflux "miniflux.app/client"
) )
func TestCreateCategory(t *testing.T) { func TestCreateCategory(t *testing.T) {
...@@ -81,6 +83,38 @@ func TestUpdateCategory(t *testing.T) { ...@@ -81,6 +83,38 @@ func TestUpdateCategory(t *testing.T) {
} }
} }
func TestMarkCategoryAsRead(t *testing.T) {
client := createClient(t)
feed, category := createFeed(t, client)
results, err := client.FeedEntries(feed.ID, nil)
if err != nil {
t.Fatalf(`Failed to get entries: %v`, err)
}
if results.Total == 0 {
t.Fatalf(`Invalid number of entries: %d`, results.Total)
}
if results.Entries[0].Status != miniflux.EntryStatusUnread {
t.Fatalf(`Invalid entry status, got %q instead of %q`, results.Entries[0].Status, miniflux.EntryStatusUnread)
}
if err := client.MarkCategoryAsRead(category.ID); err != nil {
t.Fatalf(`Failed to mark category as read: %v`, err)
}
results, err = client.FeedEntries(feed.ID, nil)
if err != nil {
t.Fatalf(`Failed to get updated entries: %v`, err)
}
for _, entry := range results.Entries {
if entry.Status != miniflux.EntryStatusRead {
t.Errorf(`Status for entry %d was %q instead of %q`, entry.ID, entry.Status, miniflux.EntryStatusRead)
}
}
}
func TestListCategories(t *testing.T) { func TestListCategories(t *testing.T) {
categoryName := "My category" categoryName := "My category"
client := createClient(t) client := createClient(t)
......
...@@ -324,6 +324,38 @@ func TestUpdateFeedCategory(t *testing.T) { ...@@ -324,6 +324,38 @@ func TestUpdateFeedCategory(t *testing.T) {
} }
} }
func TestMarkFeedAsRead(t *testing.T) {
client := createClient(t)
feed, _ := createFeed(t, client)
results, err := client.FeedEntries(feed.ID, nil)
if err != nil {
t.Fatalf(`Failed to get entries: %v`, err)
}
if results.Total == 0 {
t.Fatalf(`Invalid number of entries: %d`, results.Total)
}
if results.Entries[0].Status != miniflux.EntryStatusUnread {
t.Fatalf(`Invalid entry status, got %q instead of %q`, results.Entries[0].Status, miniflux.EntryStatusUnread)
}
if err := client.MarkFeedAsRead(feed.ID); err != nil {
t.Fatalf(`Failed to mark feed as read: %v`, err)
}
results, err = client.FeedEntries(feed.ID, nil)
if err != nil {
t.Fatalf(`Failed to get updated entries: %v`, err)
}
for _, entry := range results.Entries {
if entry.Status != miniflux.EntryStatusRead {
t.Errorf(`Status for entry %d was %q instead of %q`, entry.ID, entry.Status, miniflux.EntryStatusRead)
}
}
}
func TestDeleteFeed(t *testing.T) { func TestDeleteFeed(t *testing.T) {
client := createClient(t) client := createClient(t)
feed, _ := createFeed(t, client) feed, _ := createFeed(t, client)
......
...@@ -394,3 +394,65 @@ func TestCannotDeleteUserAsNonAdmin(t *testing.T) { ...@@ -394,3 +394,65 @@ func TestCannotDeleteUserAsNonAdmin(t *testing.T) {
t.Fatal(`A "Forbidden" error should be raised`) t.Fatal(`A "Forbidden" error should be raised`)
} }
} }
func TestMarkUserAsReadAsUser(t *testing.T) {
username := getRandomUsername()
adminClient := miniflux.New(testBaseURL, testAdminUsername, testAdminPassword)
user, err := adminClient.CreateUser(username, testStandardPassword, false)
if err != nil {
t.Fatal(err)
}
client := miniflux.New(testBaseURL, username, testStandardPassword)
feed, _ := createFeed(t, client)
results, err := client.FeedEntries(feed.ID, nil)
if err != nil {
t.Fatalf(`Failed to get entries: %v`, err)
}
if results.Total == 0 {
t.Fatalf(`Invalid number of entries: %d`, results.Total)
}
if results.Entries[0].Status != miniflux.EntryStatusUnread {
t.Fatalf(`Invalid entry status, got %q instead of %q`, results.Entries[0].Status, miniflux.EntryStatusUnread)
}
if err := client.MarkAllAsRead(user.ID); err != nil {
t.Fatalf(`Failed to mark user's unread entries as read: %v`, err)
}
results, err = client.FeedEntries(feed.ID, nil)
if err != nil {
t.Fatalf(`Failed to get updated entries: %v`, err)
}
for _, entry := range results.Entries {
if entry.Status != miniflux.EntryStatusRead {
t.Errorf(`Status for entry %d was %q instead of %q`, entry.ID, entry.Status, miniflux.EntryStatusRead)
}
}
}
func TestCannotMarkUserAsReadAsOtherUser(t *testing.T) {
username := getRandomUsername()
adminClient := miniflux.New(testBaseURL, testAdminUsername, testAdminPassword)
user1, err := adminClient.CreateUser(username, testStandardPassword, false)
if err != nil {
t.Fatal(err)
}
createFeed(t, miniflux.New(testBaseURL, username, testStandardPassword))
username2 := getRandomUsername()
if _, err = adminClient.CreateUser(username2, testStandardPassword, false); err != nil {
t.Fatal(err)
}
client := miniflux.New(testBaseURL, username2, testStandardPassword)
err = client.MarkAllAsRead(user1.ID)
if err == nil {
t.Fatalf(`Non-admin users should not be able to mark another user as read`)
}
if err != miniflux.ErrForbidden {
t.Errorf(`A "Forbidden" error should be raised, got %q`, err)
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment