181 lines
8.2 KiB
Go
181 lines
8.2 KiB
Go
package test
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/usememos/memos/store"
|
|
)
|
|
|
|
func TestDeleteAttachmentsWithPolicyUsesCurrentBinding(t *testing.T) {
|
|
ctx := context.Background()
|
|
ts := NewTestingStore(ctx, t)
|
|
defer ts.Close()
|
|
|
|
_, actor, space := createAttachmentDeleteSpace(ctx, t, ts, "current")
|
|
firstMemo, err := ts.CreateMemo(ctx, &store.Memo{
|
|
UID: "attachment-delete-current-first", CreatorID: actor.ID, Content: "first", Visibility: store.SpaceAudience, SpaceID: &space.ID,
|
|
})
|
|
require.NoError(t, err)
|
|
secondMemo, err := ts.CreateMemo(ctx, &store.Memo{
|
|
UID: "attachment-delete-current-second", CreatorID: actor.ID, Content: "second", Visibility: store.SpaceAudience, SpaceID: &space.ID,
|
|
})
|
|
require.NoError(t, err)
|
|
attachment, err := ts.CreateAttachment(ctx, &store.Attachment{
|
|
UID: "attachment-delete-current-target", CreatorID: actor.ID, Filename: "current.txt", Type: "text/plain", MemoID: &firstMemo.ID,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// The delete protocol authorizes the binding locked in its own transaction;
|
|
// it does not compare a transport snapshot of the old binding.
|
|
require.NoError(t, ts.UpdateAttachment(ctx, &store.UpdateAttachment{ID: attachment.ID, MemoID: &secondMemo.ID}))
|
|
require.NoError(t, ts.DeleteAttachmentsWithPolicy(ctx, attachmentDeletionPolicy(actor.ID, secondMemo), []int32{attachment.ID}))
|
|
|
|
stored, err := ts.GetAttachment(ctx, &store.FindAttachment{ID: &attachment.ID})
|
|
require.NoError(t, err)
|
|
require.Nil(t, stored)
|
|
}
|
|
|
|
func TestDeleteAttachmentsWithPolicyRevalidatesCurrentMemoMembership(t *testing.T) {
|
|
ctx := context.Background()
|
|
ts := NewTestingStore(ctx, t)
|
|
defer ts.Close()
|
|
|
|
owner, actor, space := createAttachmentDeleteSpace(ctx, t, ts, "membership")
|
|
memo, err := ts.CreateMemo(ctx, &store.Memo{
|
|
UID: "attachment-delete-membership-memo", CreatorID: actor.ID, Content: "memo", Visibility: store.SpaceAudience, SpaceID: &space.ID,
|
|
})
|
|
require.NoError(t, err)
|
|
attachment, err := ts.CreateAttachment(ctx, &store.Attachment{
|
|
UID: "attachment-delete-membership-target", CreatorID: actor.ID, Filename: "member.txt", Type: "text/plain", MemoID: &memo.ID,
|
|
})
|
|
require.NoError(t, err)
|
|
require.NoError(t, ts.DeleteSpaceMember(ctx, &store.DeleteSpaceMember{SpaceID: space.ID, UserID: actor.ID}, owner.ID))
|
|
|
|
err = ts.DeleteAttachmentsWithPolicy(ctx, attachmentDeletionPolicy(actor.ID, memo), []int32{attachment.ID})
|
|
require.ErrorIs(t, err, store.ErrMemoSpaceMembershipRequired)
|
|
stored, err := ts.GetAttachment(ctx, &store.FindAttachment{ID: &attachment.ID})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, stored)
|
|
}
|
|
|
|
func TestDeleteAttachmentsWithPolicyRequiresCurrentMemoAuthor(t *testing.T) {
|
|
ctx := context.Background()
|
|
ts := NewTestingStore(ctx, t)
|
|
defer ts.Close()
|
|
|
|
owner, actor, space := createAttachmentDeleteSpace(ctx, t, ts, "memo-author")
|
|
ownerMemo, err := ts.CreateMemo(ctx, &store.Memo{
|
|
UID: "attachment-delete-owner-memo", CreatorID: owner.ID, Content: "owner", Visibility: store.SpaceAudience, SpaceID: &space.ID,
|
|
})
|
|
require.NoError(t, err)
|
|
attachment, err := ts.CreateAttachment(ctx, &store.Attachment{
|
|
UID: "attachment-delete-actor-target", CreatorID: actor.ID, Filename: "actor.txt", Type: "text/plain",
|
|
})
|
|
require.NoError(t, err)
|
|
require.NoError(t, ts.UpdateAttachment(ctx, &store.UpdateAttachment{ID: attachment.ID, MemoID: &ownerMemo.ID}))
|
|
|
|
err = ts.DeleteAttachmentsWithPolicy(ctx, attachmentDeletionPolicy(actor.ID, ownerMemo), []int32{attachment.ID})
|
|
require.ErrorIs(t, err, store.ErrMemoPermissionDenied)
|
|
stored, err := ts.GetAttachment(ctx, &store.FindAttachment{ID: &attachment.ID})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, stored)
|
|
}
|
|
|
|
func TestDeleteAttachmentsWithPolicyBatchIsAtomic(t *testing.T) {
|
|
ctx := context.Background()
|
|
ts := NewTestingStore(ctx, t)
|
|
defer ts.Close()
|
|
|
|
actor, err := ts.CreateUser(ctx, &store.User{Username: "attachment-delete-batch-actor", Role: store.RoleUser, PasswordHash: "hash"})
|
|
require.NoError(t, err)
|
|
other, err := ts.CreateUser(ctx, &store.User{Username: "attachment-delete-batch-other", Role: store.RoleUser, PasswordHash: "hash"})
|
|
require.NoError(t, err)
|
|
owned, err := ts.CreateAttachment(ctx, &store.Attachment{
|
|
UID: "attachment-delete-batch-owned", CreatorID: actor.ID, Filename: "owned.txt", Type: "text/plain",
|
|
})
|
|
require.NoError(t, err)
|
|
foreign, err := ts.CreateAttachment(ctx, &store.Attachment{
|
|
UID: "attachment-delete-batch-foreign", CreatorID: other.ID, Filename: "foreign.txt", Type: "text/plain",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = ts.DeleteAttachmentsWithPolicy(ctx, attachmentDeletionPolicy(actor.ID), []int32{owned.ID, foreign.ID})
|
|
require.ErrorIs(t, err, store.ErrMemoPermissionDenied)
|
|
for _, attachment := range []*store.Attachment{owned, foreign} {
|
|
stored, getErr := ts.GetAttachment(ctx, &store.FindAttachment{ID: &attachment.ID})
|
|
require.NoError(t, getErr)
|
|
require.NotNil(t, stored)
|
|
}
|
|
|
|
err = ts.DeleteAttachmentsWithPolicy(ctx, attachmentDeletionPolicy(actor.ID), []int32{owned.ID, 2147483000})
|
|
require.ErrorIs(t, err, store.ErrMemoMutationConflict)
|
|
stored, err := ts.GetAttachment(ctx, &store.FindAttachment{ID: &owned.ID})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, stored, "a missing batch target must not partially delete existing rows")
|
|
}
|
|
|
|
func TestDeleteAttachmentsWithPolicyValidatesIDs(t *testing.T) {
|
|
ctx := context.Background()
|
|
ts := NewTestingStore(ctx, t)
|
|
defer ts.Close()
|
|
|
|
require.Error(t, ts.DeleteAttachmentsWithPolicy(ctx, nil, []int32{1}))
|
|
require.Error(t, ts.DeleteAttachmentsWithPolicy(ctx, attachmentDeletionPolicy(1), nil))
|
|
require.Error(t, ts.DeleteAttachmentsWithPolicy(ctx, attachmentDeletionPolicy(1), []int32{0}))
|
|
require.Error(t, ts.DeleteAttachmentsWithPolicy(ctx, attachmentDeletionPolicy(1), []int32{1, 1}))
|
|
}
|
|
|
|
func TestDeleteAttachmentsWithPolicyRejectsChangedMemoContent(t *testing.T) {
|
|
ctx := context.Background()
|
|
ts := NewTestingStore(ctx, t)
|
|
defer ts.Close()
|
|
|
|
actor, err := ts.CreateUser(ctx, &store.User{Username: "attachment-delete-content-actor", Role: store.RoleUser, PasswordHash: "hash"})
|
|
require.NoError(t, err)
|
|
memo, err := ts.CreateMemo(ctx, &store.Memo{
|
|
UID: "attachment-delete-content-memo", CreatorID: actor.ID, Content: "before", Visibility: store.Private,
|
|
})
|
|
require.NoError(t, err)
|
|
attachment, err := ts.CreateAttachment(ctx, &store.Attachment{
|
|
UID: "attachment-delete-content-target", CreatorID: actor.ID, Filename: "content.txt", Type: "text/plain", MemoID: &memo.ID,
|
|
})
|
|
require.NoError(t, err)
|
|
policy := attachmentDeletionPolicy(actor.ID, memo)
|
|
err = ts.DeleteAttachmentsWithPolicy(ctx, attachmentDeletionPolicy(actor.ID), []int32{attachment.ID})
|
|
require.ErrorIs(t, err, store.ErrMemoMutationConflict, "bound attachments require a memo content snapshot")
|
|
|
|
updatedContent := "after"
|
|
require.NoError(t, ts.UpdateMemo(ctx, &store.UpdateMemo{ID: memo.ID, Content: &updatedContent}))
|
|
|
|
err = ts.DeleteAttachmentsWithPolicy(ctx, policy, []int32{attachment.ID})
|
|
require.ErrorIs(t, err, store.ErrMemoMutationConflict)
|
|
stored, err := ts.GetAttachment(ctx, &store.FindAttachment{ID: &attachment.ID})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, stored)
|
|
}
|
|
|
|
func attachmentDeletionPolicy(actorUserID int32, memos ...*store.Memo) *store.AttachmentDeletionPolicy {
|
|
expectedMemoContents := make(map[int32]string, len(memos))
|
|
for _, memo := range memos {
|
|
if memo != nil {
|
|
expectedMemoContents[memo.ID] = memo.Content
|
|
}
|
|
}
|
|
return &store.AttachmentDeletionPolicy{ActorUserID: actorUserID, ExpectedMemoContents: expectedMemoContents}
|
|
}
|
|
|
|
func createAttachmentDeleteSpace(ctx context.Context, t *testing.T, ts *store.Store, suffix string) (*store.User, *store.User, *store.Space) {
|
|
t.Helper()
|
|
owner, err := ts.CreateUser(ctx, &store.User{Username: "attachment-delete-" + suffix + "-owner", Role: store.RoleUser, PasswordHash: "hash"})
|
|
require.NoError(t, err)
|
|
actor, err := ts.CreateUser(ctx, &store.User{Username: "attachment-delete-" + suffix + "-actor", Role: store.RoleUser, PasswordHash: "hash"})
|
|
require.NoError(t, err)
|
|
space, err := ts.CreateSpace(ctx, &store.Space{UID: "attachment-delete-" + suffix + "-space", Title: suffix}, owner.ID)
|
|
require.NoError(t, err)
|
|
_, err = createSpaceMemberForTest(ctx, ts, &store.SpaceMember{SpaceID: space.ID, UserID: actor.ID, Role: store.SpaceMemberRoleUser}, owner.ID)
|
|
require.NoError(t, err)
|
|
return owner, actor, space
|
|
}
|