memos/server/router/rss/rss_test.go

180 lines
5.6 KiB
Go

package rss
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/labstack/echo/v5"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/internal/markdown"
storepb "github.com/usememos/memos/proto/gen/store"
"github.com/usememos/memos/store"
teststore "github.com/usememos/memos/store/test"
)
func TestPublicRSSExcludesComments(t *testing.T) {
ctx := context.Background()
stores := teststore.NewTestingStore(ctx, t)
defer stores.Close()
setInstanceAccessMode(ctx, t, stores, storepb.InstanceAccessMode_INSTANCE_ACCESS_MODE_PUBLIC)
user, err := stores.CreateUser(ctx, &store.User{
Username: "rss-comment-owner",
Role: store.RoleUser,
Email: "rss-comment-owner@example.com",
})
require.NoError(t, err)
parent, err := stores.CreateMemo(ctx, &store.Memo{
UID: "rss-public-parent",
CreatorID: user.ID,
Content: "public parent should stay in rss",
Visibility: store.Public,
})
require.NoError(t, err)
_, err = stores.CreateMemoComment(ctx, &store.Memo{
UID: "rss-public-comment",
CreatorID: user.ID,
Content: "public comment should not be in rss",
Visibility: store.Private,
}, parent.ID, user.ID)
require.NoError(t, err)
service := NewRSSService(stores, markdown.NewService())
exploreRSS := renderRSS(t, service, "/explore/rss.xml", "")
require.Contains(t, exploreRSS, "public parent should stay in rss")
require.NotContains(t, exploreRSS, "public comment should not be in rss")
userRSS := renderRSS(t, service, "/u/rss-comment-owner/rss.xml", user.Username)
require.Contains(t, userRSS, "public parent should stay in rss")
require.NotContains(t, userRSS, "public comment should not be in rss")
}
func TestPrivateInstanceDisablesRSS(t *testing.T) {
ctx := context.Background()
stores := teststore.NewTestingStore(ctx, t)
defer stores.Close()
setInstanceAccessMode(ctx, t, stores, storepb.InstanceAccessMode_INSTANCE_ACCESS_MODE_PRIVATE)
service := NewRSSService(stores, nil)
for _, test := range []struct {
name string
target string
username string
}{
{name: "explore", target: "/explore/rss.xml"},
{name: "user", target: "/u/alice/rss.xml", username: "alice"},
} {
t.Run(test.name, func(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodGet, test.target, strings.NewReader(""))
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
if test.username != "" {
c.SetPathValues(echo.PathValues{{Name: "username", Value: test.username}})
}
var err error
if test.username == "" {
err = service.GetExploreRSS(c)
} else {
err = service.GetUserRSS(c)
}
var httpError *echo.HTTPError
require.ErrorAs(t, err, &httpError)
require.Equal(t, http.StatusNotFound, httpError.Code)
})
}
}
func TestRSSIfNoneMatchRemainsStableAcrossSeconds(t *testing.T) {
ctx := context.Background()
stores := teststore.NewTestingStore(ctx, t)
defer stores.Close()
setInstanceAccessMode(ctx, t, stores, storepb.InstanceAccessMode_INSTANCE_ACCESS_MODE_PUBLIC)
user, err := stores.CreateUser(ctx, &store.User{
Username: "rss-etag-owner",
Role: store.RoleUser,
Email: "rss-etag-owner@example.com",
})
require.NoError(t, err)
_, err = stores.CreateMemo(ctx, &store.Memo{
UID: "rss-etag-stable",
CreatorID: user.ID,
Content: "stable feed content",
Visibility: store.Public,
})
require.NoError(t, err)
service := NewRSSService(stores, markdown.NewService())
explore := requestRSS(t, service, "/explore/rss.xml", "", "")
userFeed := requestRSS(t, service, "/u/rss-etag-owner/rss.xml", user.Username, "")
require.Equal(t, http.StatusOK, explore.Code)
require.Equal(t, http.StatusOK, userFeed.Code)
exploreETag := explore.Header().Get("ETag")
userETag := userFeed.Header().Get("ETag")
require.NotEmpty(t, exploreETag)
require.NotEmpty(t, userETag)
// The previous request-time channel timestamp changed once per second even
// when no feed data changed, causing these conditional reads to return 200.
time.Sleep(1100 * time.Millisecond)
explore = requestRSS(t, service, "/explore/rss.xml", "", exploreETag)
userFeed = requestRSS(t, service, "/u/rss-etag-owner/rss.xml", user.Username, userETag)
require.Equal(t, http.StatusNotModified, explore.Code)
require.Equal(t, http.StatusNotModified, userFeed.Code)
require.Equal(t, exploreETag, explore.Header().Get("ETag"))
require.Equal(t, userETag, userFeed.Header().Get("ETag"))
}
func setInstanceAccessMode(ctx context.Context, t *testing.T, stores *store.Store, mode storepb.InstanceAccessMode) {
t.Helper()
_, err := stores.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{
Key: storepb.InstanceSettingKey_ACCESS,
Value: &storepb.InstanceSetting_AccessSetting{AccessSetting: &storepb.InstanceAccessSetting{
AccessMode: mode,
}},
})
require.NoError(t, err)
}
func renderRSS(t *testing.T, service *RSSService, target string, username string) string {
t.Helper()
rec := requestRSS(t, service, target, username, "")
require.Equal(t, http.StatusOK, rec.Code)
return rec.Body.String()
}
func requestRSS(t *testing.T, service *RSSService, target, username, ifNoneMatch string) *httptest.ResponseRecorder {
t.Helper()
e := echo.New()
req := httptest.NewRequest(http.MethodGet, target, strings.NewReader(""))
req.Host = "example.com"
if ifNoneMatch != "" {
req.Header.Set("If-None-Match", ifNoneMatch)
}
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
if username != "" {
c.SetPathValues(echo.PathValues{{Name: "username", Value: username}})
}
var err error
if username == "" {
err = service.GetExploreRSS(c)
} else {
err = service.GetUserRSS(c)
}
require.NoError(t, err)
return rec
}