memos/store/test/reaction_policy_test.go

198 lines
9.2 KiB
Go

package test
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/store"
)
func TestReactionWritePolicyUsesTargetMemoAudience(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
defer ts.Close()
owner, err := ts.CreateUser(ctx, &store.User{Username: "reaction-local-owner", Role: store.RoleUser, PasswordHash: "hash"})
require.NoError(t, err)
viewer, err := ts.CreateUser(ctx, &store.User{Username: "reaction-local-viewer", Role: store.RoleUser, PasswordHash: "hash"})
require.NoError(t, err)
outsider, err := ts.CreateUser(ctx, &store.User{Username: "reaction-local-outsider", Role: store.RoleUser, PasswordHash: "hash"})
require.NoError(t, err)
contextMemo, err := ts.CreateMemo(ctx, &store.Memo{
UID: "reaction-local-context", CreatorID: owner.ID, Content: "public context", Visibility: store.Public,
})
require.NoError(t, err)
privateComment, err := ts.CreateMemoComment(ctx, &store.Memo{
UID: "reaction-local-private-comment", CreatorID: viewer.ID, Content: "private comment", Visibility: store.Private,
}, contextMemo.ID, viewer.ID)
require.NoError(t, err)
_, err = ts.UpsertReaction(ctx, &store.Reaction{
CreatorID: owner.ID, MemoID: privateComment.ID, ReactionType: "context-author",
Policy: reactionWritePolicy(owner.ID),
})
require.ErrorIs(t, err, store.ErrMemoPermissionDenied, "a public context relation must not grant access to a private comment")
_, err = ts.UpsertReaction(ctx, &store.Reaction{
CreatorID: viewer.ID, MemoID: privateComment.ID, ReactionType: "comment-author",
Policy: reactionWritePolicy(viewer.ID),
})
require.NoError(t, err)
privateContext, err := ts.CreateMemo(ctx, &store.Memo{
UID: "reaction-local-private-context", CreatorID: owner.ID, Content: "private context", Visibility: store.Private,
})
require.NoError(t, err)
publicComment, err := ts.CreateMemoComment(ctx, &store.Memo{
UID: "reaction-local-public-comment", CreatorID: owner.ID, Content: "public comment", Visibility: store.Public,
}, privateContext.ID, owner.ID)
require.NoError(t, err)
_, err = ts.UpsertReaction(ctx, &store.Reaction{
CreatorID: outsider.ID, MemoID: publicComment.ID, ReactionType: "public-comment",
Policy: reactionWritePolicy(outsider.ID),
})
require.NoError(t, err, "a private context relation must not restrict a public comment")
_, err = ts.UpsertReaction(ctx, &store.Reaction{
CreatorID: outsider.ID, MemoID: publicComment.ID, ReactionType: "wrong-actor", Policy: reactionWritePolicy(owner.ID),
})
require.ErrorIs(t, err, store.ErrReactionPermissionDenied)
}
func TestReactionWritePolicySpaceParticipationIsMemoLocal(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
defer ts.Close()
owner, err := ts.CreateUser(ctx, &store.User{Username: "reaction-space-owner", Role: store.RoleUser, PasswordHash: "hash"})
require.NoError(t, err)
member, err := ts.CreateUser(ctx, &store.User{Username: "reaction-space-member", Role: store.RoleUser, PasswordHash: "hash"})
require.NoError(t, err)
outsider, err := ts.CreateUser(ctx, &store.User{Username: "reaction-space-outsider", Role: store.RoleAdmin, PasswordHash: "hash"})
require.NoError(t, err)
space, err := ts.CreateSpace(ctx, &store.Space{UID: "reaction-space", Title: "Reaction Space"}, owner.ID)
require.NoError(t, err)
_, err = createSpaceMemberForTest(ctx, ts, &store.SpaceMember{SpaceID: space.ID, UserID: member.ID, Role: store.SpaceMemberRoleUser}, owner.ID)
require.NoError(t, err)
spaceMemo, err := ts.CreateMemo(ctx, &store.Memo{
UID: "reaction-space-memo", CreatorID: owner.ID, Content: "space memo", Visibility: store.SpaceAudience, SpaceID: &space.ID,
})
require.NoError(t, err)
_, err = ts.UpsertReaction(ctx, &store.Reaction{
CreatorID: member.ID, MemoID: spaceMemo.ID, ReactionType: "member", Policy: reactionWritePolicy(member.ID),
})
require.NoError(t, err)
_, err = ts.UpsertReaction(ctx, &store.Reaction{
CreatorID: outsider.ID, MemoID: spaceMemo.ID, ReactionType: "application-admin", Policy: reactionWritePolicy(outsider.ID),
})
require.ErrorIs(t, err, store.ErrMemoSpaceMembershipRequired, "application ADMIN must not bypass Space membership")
require.NoError(t, ts.DeleteSpaceMember(ctx, &store.DeleteSpaceMember{SpaceID: space.ID, UserID: member.ID}, owner.ID))
_, err = ts.UpsertReaction(ctx, &store.Reaction{
CreatorID: member.ID, MemoID: spaceMemo.ID, ReactionType: "former-member", Policy: reactionWritePolicy(member.ID),
})
require.ErrorIs(t, err, store.ErrMemoSpaceMembershipRequired)
assignedPrivate, err := ts.CreateMemo(ctx, &store.Memo{
UID: "reaction-space-private", CreatorID: owner.ID, Content: "private", Visibility: store.Private, SpaceID: &space.ID,
})
require.NoError(t, err)
_, err = createSpaceMemberForTest(ctx, ts, &store.SpaceMember{SpaceID: space.ID, UserID: member.ID, Role: store.SpaceMemberRoleUser}, owner.ID)
require.NoError(t, err)
_, err = ts.UpsertReaction(ctx, &store.Reaction{
CreatorID: member.ID, MemoID: assignedPrivate.ID, ReactionType: "private-member", Policy: reactionWritePolicy(member.ID),
})
require.ErrorIs(t, err, store.ErrMemoPermissionDenied, "Space membership must not broaden a PRIVATE memo")
}
func TestReactionWritePolicyRejectsInactiveActorAndMemo(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
defer ts.Close()
owner, err := ts.CreateUser(ctx, &store.User{Username: "reaction-state-owner", Role: store.RoleUser, PasswordHash: "hash"})
require.NoError(t, err)
actor, err := ts.CreateUser(ctx, &store.User{Username: "reaction-state-actor", Role: store.RoleUser, PasswordHash: "hash"})
require.NoError(t, err)
memo, err := ts.CreateMemo(ctx, &store.Memo{UID: "reaction-state-memo", CreatorID: owner.ID, Content: "memo", Visibility: store.Public})
require.NoError(t, err)
archived := store.Archived
_, err = ts.UpdateUser(ctx, &store.UpdateUser{ID: actor.ID, RowStatus: &archived})
require.NoError(t, err)
_, err = ts.UpsertReaction(ctx, &store.Reaction{
CreatorID: actor.ID, MemoID: memo.ID, ReactionType: "inactive-actor", Policy: reactionWritePolicy(actor.ID),
})
require.ErrorIs(t, err, store.ErrReactionPermissionDenied)
require.NoError(t, ts.UpdateMemo(ctx, &store.UpdateMemo{ID: memo.ID, RowStatus: &archived}))
_, err = ts.UpsertReaction(ctx, &store.Reaction{
CreatorID: owner.ID, MemoID: memo.ID, ReactionType: "inactive-memo", Policy: reactionWritePolicy(owner.ID),
})
require.ErrorIs(t, err, store.ErrMemoSpaceNotWritable)
}
func TestDeleteReactionAtomicallyEnforcesCreatorAndParticipation(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
defer ts.Close()
owner, err := ts.CreateUser(ctx, &store.User{Username: "reaction-delete-owner", Role: store.RoleUser, PasswordHash: "hash"})
require.NoError(t, err)
member, err := ts.CreateUser(ctx, &store.User{Username: "reaction-delete-member", Role: store.RoleUser, PasswordHash: "hash"})
require.NoError(t, err)
space, err := ts.CreateSpace(ctx, &store.Space{UID: "reaction-delete-space", Title: "Delete Reactions"}, owner.ID)
require.NoError(t, err)
_, err = createSpaceMemberForTest(ctx, ts, &store.SpaceMember{SpaceID: space.ID, UserID: member.ID, Role: store.SpaceMemberRoleUser}, owner.ID)
require.NoError(t, err)
memo, err := ts.CreateMemo(ctx, &store.Memo{
UID: "reaction-delete-memo", CreatorID: owner.ID, Content: "memo", Visibility: store.SpaceAudience, SpaceID: &space.ID,
})
require.NoError(t, err)
reaction, err := ts.UpsertReaction(ctx, &store.Reaction{
CreatorID: member.ID, MemoID: memo.ID, ReactionType: "delete", Policy: reactionWritePolicy(member.ID),
})
require.NoError(t, err)
err = ts.DeleteReaction(ctx, &store.DeleteReaction{
ID: &reaction.ID, MemoID: &memo.ID, ActorUserID: &owner.ID, Policy: reactionWritePolicy(owner.ID),
})
require.ErrorIs(t, err, store.ErrReactionPermissionDenied)
requireReactionPresent(ctx, t, ts, reaction.ID)
require.NoError(t, ts.DeleteSpaceMember(ctx, &store.DeleteSpaceMember{SpaceID: space.ID, UserID: member.ID}, owner.ID))
err = ts.DeleteReaction(ctx, &store.DeleteReaction{
ID: &reaction.ID, MemoID: &memo.ID, ActorUserID: &member.ID, Policy: reactionWritePolicy(member.ID),
})
require.ErrorIs(t, err, store.ErrMemoSpaceMembershipRequired)
requireReactionPresent(ctx, t, ts, reaction.ID)
_, err = createSpaceMemberForTest(ctx, ts, &store.SpaceMember{SpaceID: space.ID, UserID: member.ID, Role: store.SpaceMemberRoleUser}, owner.ID)
require.NoError(t, err)
require.NoError(t, ts.DeleteReaction(ctx, &store.DeleteReaction{
ID: &reaction.ID, MemoID: &memo.ID, ActorUserID: &member.ID, Policy: reactionWritePolicy(member.ID),
}))
requireReactionMissing(ctx, t, ts, reaction.ID)
}
func reactionWritePolicy(actorUserID int32) *store.ReactionWritePolicy {
return &store.ReactionWritePolicy{ActorUserID: actorUserID}
}
func requireReactionPresent(ctx context.Context, t *testing.T, ts *store.Store, reactionID int32) {
t.Helper()
reaction, err := ts.GetReaction(ctx, &store.FindReaction{ID: &reactionID})
require.NoError(t, err)
require.NotNil(t, reaction)
}
func requireReactionMissing(ctx context.Context, t *testing.T, ts *store.Store, reactionID int32) {
t.Helper()
reaction, err := ts.GetReaction(ctx, &store.FindReaction{ID: &reactionID})
require.NoError(t, err)
require.Nil(t, reaction)
}