memos/store/test/memo_access_test.go

217 lines
9.6 KiB
Go

package test
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/store"
)
func TestMemoAccessScopeUsesEachMemoAudience(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
defer ts.Close()
owner, err := ts.CreateUser(ctx, &store.User{Username: "access-local-owner", Role: store.RoleUser, PasswordHash: "hash"})
require.NoError(t, err)
viewer, err := ts.CreateUser(ctx, &store.User{Username: "access-local-viewer", Role: store.RoleUser, PasswordHash: "hash"})
require.NoError(t, err)
privateContext, err := ts.CreateMemo(ctx, &store.Memo{
UID: "access-private-context", CreatorID: owner.ID, Content: "private context", Visibility: store.Private,
})
require.NoError(t, err)
publicComment, err := ts.CreateMemoComment(ctx, &store.Memo{
UID: "access-public-comment", CreatorID: owner.ID, Content: "public comment", Visibility: store.Public,
}, privateContext.ID, owner.ID)
require.NoError(t, err)
publicContext, err := ts.CreateMemo(ctx, &store.Memo{
UID: "access-public-context", CreatorID: owner.ID, Content: "public context", Visibility: store.Public,
})
require.NoError(t, err)
privateComment, err := ts.CreateMemoComment(ctx, &store.Memo{
UID: "access-private-comment", CreatorID: owner.ID, Content: "private comment", Visibility: store.Private,
}, publicContext.ID, owner.ID)
require.NoError(t, err)
visible, err := ts.ListMemos(ctx, &store.FindMemo{
Access: &store.MemoAccessScope{UserID: &viewer.ID, AllowPublic: true, AllowProtected: true},
})
require.NoError(t, err)
require.ElementsMatch(t, []int32{publicComment.ID, publicContext.ID}, memoIDs(visible),
"COMMENT relations must neither grant nor restrict memo-local read access")
commentsForPrivateContext, err := ts.ListMemos(ctx, &store.FindMemo{
CommentContextMemoID: &privateContext.ID,
Access: &store.MemoAccessScope{UserID: &viewer.ID, AllowPublic: true, AllowProtected: true},
})
require.NoError(t, err)
require.Equal(t, []int32{publicComment.ID}, memoIDs(commentsForPrivateContext),
"a readable comment may be returned even when its context memo is not readable")
nonComments, err := ts.ListMemos(ctx, &store.FindMemo{
ExcludeComments: true,
Access: &store.MemoAccessScope{UserID: &viewer.ID, AllowPublic: true, AllowProtected: true},
})
require.NoError(t, err)
require.Equal(t, []int32{publicContext.ID}, memoIDs(nonComments), "ordinary feeds exclude memos with COMMENT context")
ownerRows, err := ts.ListMemos(ctx, &store.FindMemo{
Access: &store.MemoAccessScope{UserID: &owner.ID, AllowPublic: true, AllowProtected: true},
})
require.NoError(t, err)
require.ElementsMatch(t, []int32{privateContext.ID, publicComment.ID, publicContext.ID, privateComment.ID}, memoIDs(ownerRows))
}
func TestMemoAccessScopeUsesAudienceForInvalidLocalPlacement(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
defer ts.Close()
owner, err := ts.CreateUser(ctx, &store.User{Username: "access-invalid-owner", Role: store.RoleUser, PasswordHash: "hash"})
require.NoError(t, err)
memo, err := ts.CreateMemo(ctx, &store.Memo{UID: "access-invalid-memo", CreatorID: owner.ID, Content: "memo", Visibility: store.Public})
require.NoError(t, err)
_, err = ts.GetDriver().GetDB().ExecContext(ctx, fmt.Sprintf("UPDATE memo SET space_id = 2147483000 WHERE id = %d", memo.ID))
require.NoError(t, err)
visible, err := ts.ListMemos(ctx, &store.FindMemo{
Access: &store.MemoAccessScope{UserID: &owner.ID, AllowPublic: true, AllowProtected: true},
})
require.NoError(t, err)
require.Equal(t, []int32{memo.ID}, memoIDs(visible), "a dangling placement must not override a non-Space audience")
_, err = ts.GetDriver().GetDB().ExecContext(ctx, fmt.Sprintf("UPDATE memo SET space_id = NULL, visibility = 'SPACE' WHERE id = %d", memo.ID))
require.NoError(t, err)
visible, err = ts.ListMemos(ctx, &store.FindMemo{
Access: &store.MemoAccessScope{UserID: &owner.ID, AllowPublic: true, AllowProtected: true},
})
require.NoError(t, err)
require.Empty(t, visible, "SPACE without placement fails closed even for the author")
}
func TestMemoAccessScopeSpaceAudienceHasNoAuthorBypass(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
defer ts.Close()
author, err := ts.CreateUser(ctx, &store.User{Username: "access-removed-author", Role: store.RoleUser, PasswordHash: "hash"})
require.NoError(t, err)
admin, err := ts.CreateUser(ctx, &store.User{Username: "access-remaining-admin", Role: store.RoleUser, PasswordHash: "hash"})
require.NoError(t, err)
space, err := ts.CreateSpace(ctx, &store.Space{UID: "access-author-space", Title: "Author access"}, author.ID)
require.NoError(t, err)
_, err = createSpaceMemberForTest(ctx, ts, &store.SpaceMember{SpaceID: space.ID, UserID: admin.ID, Role: store.SpaceMemberRoleAdmin}, author.ID)
require.NoError(t, err)
membersMemo, err := ts.CreateMemo(ctx, &store.Memo{
UID: "access-author-members", CreatorID: author.ID, Content: "members", Visibility: store.SpaceAudience, SpaceID: &space.ID,
})
require.NoError(t, err)
privateMemo, err := ts.CreateMemo(ctx, &store.Memo{
UID: "access-author-private", CreatorID: author.ID, Content: "private", Visibility: store.Private, SpaceID: &space.ID,
})
require.NoError(t, err)
require.NoError(t, ts.DeleteSpaceMember(ctx, &store.DeleteSpaceMember{SpaceID: space.ID, UserID: author.ID}, admin.ID))
authorRows, err := ts.ListMemos(ctx, &store.FindMemo{
Access: &store.MemoAccessScope{UserID: &author.ID, AllowPublic: true, AllowProtected: true},
})
require.NoError(t, err)
require.Equal(t, []int32{privateMemo.ID}, memoIDs(authorRows), "PRIVATE remains author-readable, but SPACE requires current membership")
adminRows, err := ts.ListMemos(ctx, &store.FindMemo{
Access: &store.MemoAccessScope{UserID: &admin.ID, AllowPublic: true, AllowProtected: true},
})
require.NoError(t, err)
require.Equal(t, []int32{membersMemo.ID}, memoIDs(adminRows), "membership reads SPACE but does not expand PRIVATE")
}
func TestMemoAccessScopeArchivedRowsRemainAuthorOnly(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
defer ts.Close()
owner, err := ts.CreateUser(ctx, &store.User{Username: "access-archive-owner", Role: store.RoleUser, PasswordHash: "hash"})
require.NoError(t, err)
viewer, err := ts.CreateUser(ctx, &store.User{Username: "access-archive-viewer", Role: store.RoleUser, PasswordHash: "hash"})
require.NoError(t, err)
memo, err := ts.CreateMemo(ctx, &store.Memo{UID: "access-archive-memo", CreatorID: owner.ID, Content: "memo", Visibility: store.Public})
require.NoError(t, err)
archived := store.Archived
require.NoError(t, ts.UpdateMemo(ctx, &store.UpdateMemo{ID: memo.ID, RowStatus: &archived}))
ownerRows, err := ts.ListMemos(ctx, &store.FindMemo{
RowStatus: &archived,
Access: &store.MemoAccessScope{UserID: &owner.ID, AllowPublic: true, AllowProtected: true},
})
require.NoError(t, err)
require.Equal(t, []int32{memo.ID}, memoIDs(ownerRows))
viewerRows, err := ts.ListMemos(ctx, &store.FindMemo{
RowStatus: &archived,
Access: &store.MemoAccessScope{UserID: &viewer.ID, AllowPublic: true, AllowProtected: true},
})
require.NoError(t, err)
require.Empty(t, viewerRows)
anonymousRows, err := ts.ListMemos(ctx, &store.FindMemo{RowStatus: &archived, Access: &store.MemoAccessScope{AllowPublic: true}})
require.NoError(t, err)
require.Empty(t, anonymousRows)
}
func TestMemoAccessScopeAllowsArchivedCreatorButFiltersMissingCreatorBeforePagination(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
defer ts.Close()
archivedCreator, err := ts.CreateUser(ctx, &store.User{Username: "access-archived-creator", Role: store.RoleUser, PasswordHash: "hash"})
require.NoError(t, err)
viewer, err := ts.CreateUser(ctx, &store.User{Username: "access-creator-viewer", Role: store.RoleUser, PasswordHash: "hash"})
require.NoError(t, err)
publicMemo, err := ts.CreateMemo(ctx, &store.Memo{
UID: "access-archived-creator-public", CreatorID: archivedCreator.ID, Content: "public", Visibility: store.Public,
})
require.NoError(t, err)
protectedMemo, err := ts.CreateMemo(ctx, &store.Memo{
UID: "access-archived-creator-protected", CreatorID: archivedCreator.ID, Content: "protected", Visibility: store.Protected,
})
require.NoError(t, err)
archived := store.Archived
_, err = ts.UpdateUser(ctx, &store.UpdateUser{ID: archivedCreator.ID, RowStatus: &archived})
require.NoError(t, err)
visible, err := ts.ListMemos(ctx, &store.FindMemo{
Access: &store.MemoAccessScope{UserID: &viewer.ID, AllowPublic: true, AllowProtected: true},
})
require.NoError(t, err)
require.ElementsMatch(t, []int32{publicMemo.ID, protectedMemo.ID}, memoIDs(visible),
"archiving a creator does not change the audience of their active memos")
missingCreatorMemo, err := ts.CreateMemo(ctx, &store.Memo{
UID: "access-missing-creator", CreatorID: viewer.ID, Content: "corrupt", Visibility: store.Public,
})
require.NoError(t, err)
_, err = ts.GetDriver().GetDB().ExecContext(ctx,
fmt.Sprintf("UPDATE memo SET creator_id = 2147483000 WHERE id = %d", missingCreatorMemo.ID))
require.NoError(t, err)
limit := 2
visible, err = ts.ListMemos(ctx, &store.FindMemo{
Access: &store.MemoAccessScope{UserID: &viewer.ID, AllowPublic: true, AllowProtected: true},
Limit: &limit,
})
require.NoError(t, err)
require.Len(t, visible, 2)
require.ElementsMatch(t, []int32{publicMemo.ID, protectedMemo.ID}, memoIDs(visible),
"a newer dangling-creator row must be removed before LIMIT is applied")
}
func memoIDs(memos []*store.Memo) []int32 {
ids := make([]int32, 0, len(memos))
for _, memo := range memos {
ids = append(ids, memo.ID)
}
return ids
}