mirror of https://github.com/usememos/memos
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
490 lines
15 KiB
Go
490 lines
15 KiB
Go
package test
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
|
|
v1pb "github.com/usememos/memos/proto/gen/api/v1"
|
|
storepb "github.com/usememos/memos/proto/gen/store"
|
|
apiv1 "github.com/usememos/memos/server/router/api/v1"
|
|
"github.com/usememos/memos/store"
|
|
)
|
|
|
|
func TestDeleteUserSelfDeleteCleansAccountDataAndAuthCookies(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ts := NewTestService(t)
|
|
defer ts.Cleanup()
|
|
|
|
ctx := context.Background()
|
|
user, err := ts.CreateRegularUser(ctx, "alice")
|
|
require.NoError(t, err)
|
|
|
|
_, err = ts.Store.CreateUserIdentity(ctx, &store.UserIdentity{
|
|
UserID: user.ID,
|
|
Provider: "google",
|
|
ExternUID: "alice-google-sub",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = ts.Store.AddUserRefreshToken(ctx, user.ID, &storepb.RefreshTokensUserSetting_RefreshToken{
|
|
TokenId: "refresh-token-id",
|
|
ExpiresAt: timestamppb.New(time.Now().Add(time.Hour)),
|
|
CreatedAt: timestamppb.Now(),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
headerCtx := apiv1.WithHeaderCarrier(ctx)
|
|
authCtx := ts.CreateUserContext(headerCtx, user.ID)
|
|
_, err = ts.Service.DeleteUser(authCtx, &v1pb.DeleteUserRequest{
|
|
Name: apiv1.BuildUserName(user.Username),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
deletedUser, err := ts.Store.GetUser(ctx, &store.FindUser{ID: &user.ID})
|
|
require.NoError(t, err)
|
|
require.Nil(t, deletedUser)
|
|
|
|
identities, err := ts.Store.ListUserIdentities(ctx, &store.FindUserIdentity{UserID: &user.ID})
|
|
require.NoError(t, err)
|
|
require.Empty(t, identities)
|
|
|
|
refreshSetting, err := ts.Store.GetUserSetting(ctx, &store.FindUserSetting{
|
|
UserID: &user.ID,
|
|
Key: storepb.UserSetting_REFRESH_TOKENS,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Nil(t, refreshSetting)
|
|
|
|
carrier := apiv1.GetHeaderCarrier(authCtx)
|
|
require.NotNil(t, carrier)
|
|
require.Contains(t, strings.ToLower(carrier.Get("Set-Cookie")), "memos_refresh=")
|
|
}
|
|
|
|
func TestDeleteUserSelfDeleteRemovesOwnedResourcesAndMemoSubtrees(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ts := NewTestService(t)
|
|
defer ts.Cleanup()
|
|
|
|
ctx := context.Background()
|
|
user, err := ts.CreateRegularUser(ctx, "resource-owner")
|
|
require.NoError(t, err)
|
|
peer, err := ts.CreateRegularUser(ctx, "resource-peer")
|
|
require.NoError(t, err)
|
|
|
|
userCtx := ts.CreateUserContext(ctx, user.ID)
|
|
peerCtx := ts.CreateUserContext(ctx, peer.ID)
|
|
|
|
ownMemo, err := ts.Service.CreateMemo(userCtx, &v1pb.CreateMemoRequest{
|
|
Memo: &v1pb.Memo{
|
|
Content: "owner memo",
|
|
Visibility: v1pb.Visibility_PUBLIC,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
foreignMemo, err := ts.Service.CreateMemo(peerCtx, &v1pb.CreateMemoRequest{
|
|
Memo: &v1pb.Memo{
|
|
Content: "peer memo",
|
|
Visibility: v1pb.Visibility_PUBLIC,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
peerCommentOnOwnMemo, err := ts.Service.CreateMemoComment(peerCtx, &v1pb.CreateMemoCommentRequest{
|
|
Name: ownMemo.Name,
|
|
Comment: &v1pb.Memo{
|
|
Content: "peer comment on owner memo",
|
|
Visibility: v1pb.Visibility_PUBLIC,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
peerNestedCommentOnOwnMemo, err := ts.Service.CreateMemoComment(peerCtx, &v1pb.CreateMemoCommentRequest{
|
|
Name: peerCommentOnOwnMemo.Name,
|
|
Comment: &v1pb.Memo{
|
|
Content: "peer nested comment on owner memo",
|
|
Visibility: v1pb.Visibility_PUBLIC,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
userCommentOnForeignMemo, err := ts.Service.CreateMemoComment(userCtx, &v1pb.CreateMemoCommentRequest{
|
|
Name: foreignMemo.Name,
|
|
Comment: &v1pb.Memo{
|
|
Content: "owner comment on peer memo",
|
|
Visibility: v1pb.Visibility_PUBLIC,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
peerReplyToUserComment, err := ts.Service.CreateMemoComment(peerCtx, &v1pb.CreateMemoCommentRequest{
|
|
Name: userCommentOnForeignMemo.Name,
|
|
Comment: &v1pb.Memo{
|
|
Content: "peer reply to owner comment",
|
|
Visibility: v1pb.Visibility_PUBLIC,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
ownMemoUID, err := apiv1.ExtractMemoUIDFromName(ownMemo.Name)
|
|
require.NoError(t, err)
|
|
ownMemoStore, err := ts.Store.GetMemo(ctx, &store.FindMemo{UID: &ownMemoUID})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, ownMemoStore)
|
|
|
|
foreignMemoUID, err := apiv1.ExtractMemoUIDFromName(foreignMemo.Name)
|
|
require.NoError(t, err)
|
|
foreignMemoStore, err := ts.Store.GetMemo(ctx, &store.FindMemo{UID: &foreignMemoUID})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, foreignMemoStore)
|
|
|
|
attachedAttachment, err := ts.Store.CreateAttachment(ctx, &store.Attachment{
|
|
UID: "attach-owner-memo",
|
|
CreatorID: user.ID,
|
|
Filename: "owner.txt",
|
|
Type: "text/plain",
|
|
Size: 4,
|
|
Blob: []byte("memo"),
|
|
MemoID: &ownMemoStore.ID,
|
|
})
|
|
require.NoError(t, err)
|
|
thumbnailCachePath := filepath.Join(ts.Profile.Data, ".thumbnail_cache", attachedAttachment.UID+".jpeg")
|
|
motionCachePath := filepath.Join(ts.Profile.Data, ".motion_cache", attachedAttachment.UID+".mp4")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(thumbnailCachePath), 0o755))
|
|
require.NoError(t, os.WriteFile(thumbnailCachePath, []byte("thumb"), 0o644))
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(motionCachePath), 0o755))
|
|
require.NoError(t, os.WriteFile(motionCachePath, []byte("motion"), 0o644))
|
|
|
|
unattachedAttachment, err := ts.Store.CreateAttachment(ctx, &store.Attachment{
|
|
UID: "attach-owner-loose",
|
|
CreatorID: user.ID,
|
|
Filename: "loose.txt",
|
|
Type: "text/plain",
|
|
Size: 5,
|
|
Blob: []byte("loose"),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
peerAttachment, err := ts.Store.CreateAttachment(ctx, &store.Attachment{
|
|
UID: "attach-peer-keep",
|
|
CreatorID: peer.ID,
|
|
Filename: "peer.txt",
|
|
Type: "text/plain",
|
|
Size: 4,
|
|
Blob: []byte("peer"),
|
|
MemoID: &foreignMemoStore.ID,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = ts.Store.UpsertReaction(ctx, &store.Reaction{
|
|
CreatorID: peer.ID,
|
|
ContentID: ownMemo.Name,
|
|
ReactionType: "👍",
|
|
})
|
|
require.NoError(t, err)
|
|
_, err = ts.Store.UpsertReaction(ctx, &store.Reaction{
|
|
CreatorID: peer.ID,
|
|
ContentID: userCommentOnForeignMemo.Name,
|
|
ReactionType: "🔥",
|
|
})
|
|
require.NoError(t, err)
|
|
_, err = ts.Store.UpsertReaction(ctx, &store.Reaction{
|
|
CreatorID: user.ID,
|
|
ContentID: foreignMemo.Name,
|
|
ReactionType: "👋",
|
|
})
|
|
require.NoError(t, err)
|
|
peerReactionOnForeignMemo, err := ts.Store.UpsertReaction(ctx, &store.Reaction{
|
|
CreatorID: peer.ID,
|
|
ContentID: foreignMemo.Name,
|
|
ReactionType: "✅",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = ts.Store.CreateMemoShare(ctx, &store.MemoShare{
|
|
UID: "share-owner-ownmemo",
|
|
MemoID: ownMemoStore.ID,
|
|
CreatorID: user.ID,
|
|
})
|
|
require.NoError(t, err)
|
|
_, err = ts.Store.CreateMemoShare(ctx, &store.MemoShare{
|
|
UID: "share-owner-foreignmemo",
|
|
MemoID: foreignMemoStore.ID,
|
|
CreatorID: user.ID,
|
|
})
|
|
require.NoError(t, err)
|
|
peerShare, err := ts.Store.CreateMemoShare(ctx, &store.MemoShare{
|
|
UID: "share-peer-foreignmemo",
|
|
MemoID: foreignMemoStore.ID,
|
|
CreatorID: peer.ID,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = ts.Store.CreateUserIdentity(ctx, &store.UserIdentity{
|
|
UserID: user.ID,
|
|
Provider: "google",
|
|
ExternUID: "resource-owner-google-sub",
|
|
})
|
|
require.NoError(t, err)
|
|
err = ts.Store.AddUserPersonalAccessToken(ctx, user.ID, &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{
|
|
TokenId: "pat-owner",
|
|
TokenHash: "pat-owner-hash",
|
|
Description: "owner pat",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
headerCtx := apiv1.WithHeaderCarrier(ctx)
|
|
authCtx := ts.CreateUserContext(headerCtx, user.ID)
|
|
_, err = ts.Service.DeleteUser(authCtx, &v1pb.DeleteUserRequest{
|
|
Name: apiv1.BuildUserName(user.Username),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
deletedUser, err := ts.Store.GetUser(ctx, &store.FindUser{ID: &user.ID})
|
|
require.NoError(t, err)
|
|
require.Nil(t, deletedUser)
|
|
|
|
for _, memoName := range []string{
|
|
ownMemo.Name,
|
|
peerCommentOnOwnMemo.Name,
|
|
peerNestedCommentOnOwnMemo.Name,
|
|
userCommentOnForeignMemo.Name,
|
|
peerReplyToUserComment.Name,
|
|
} {
|
|
memoUID, extractErr := apiv1.ExtractMemoUIDFromName(memoName)
|
|
require.NoError(t, extractErr)
|
|
memo, getErr := ts.Store.GetMemo(ctx, &store.FindMemo{UID: &memoUID})
|
|
require.NoError(t, getErr)
|
|
require.Nil(t, memo, memoName)
|
|
}
|
|
|
|
foreignMemoAfterDelete, err := ts.Store.GetMemo(ctx, &store.FindMemo{UID: &foreignMemoUID})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, foreignMemoAfterDelete)
|
|
|
|
for _, attachmentID := range []int32{attachedAttachment.ID, unattachedAttachment.ID} {
|
|
attachment, getErr := ts.Store.GetAttachment(ctx, &store.FindAttachment{ID: &attachmentID})
|
|
require.NoError(t, getErr)
|
|
require.Nil(t, attachment)
|
|
}
|
|
peerAttachmentAfterDelete, err := ts.Store.GetAttachment(ctx, &store.FindAttachment{ID: &peerAttachment.ID})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, peerAttachmentAfterDelete)
|
|
_, err = os.Stat(thumbnailCachePath)
|
|
require.ErrorIs(t, err, os.ErrNotExist)
|
|
_, err = os.Stat(motionCachePath)
|
|
require.ErrorIs(t, err, os.ErrNotExist)
|
|
|
|
ownMemoReactions, err := ts.Store.ListReactions(ctx, &store.FindReaction{ContentID: &ownMemo.Name})
|
|
require.NoError(t, err)
|
|
require.Empty(t, ownMemoReactions)
|
|
|
|
userCommentReactions, err := ts.Store.ListReactions(ctx, &store.FindReaction{ContentID: &userCommentOnForeignMemo.Name})
|
|
require.NoError(t, err)
|
|
require.Empty(t, userCommentReactions)
|
|
|
|
foreignMemoReactions, err := ts.Store.ListReactions(ctx, &store.FindReaction{ContentID: &foreignMemo.Name})
|
|
require.NoError(t, err)
|
|
require.Len(t, foreignMemoReactions, 1)
|
|
require.Equal(t, peerReactionOnForeignMemo.ID, foreignMemoReactions[0].ID)
|
|
|
|
ownerShares, err := ts.Store.ListMemoShares(ctx, &store.FindMemoShare{CreatorID: &user.ID})
|
|
require.NoError(t, err)
|
|
require.Empty(t, ownerShares)
|
|
|
|
peerShares, err := ts.Store.ListMemoShares(ctx, &store.FindMemoShare{CreatorID: &peer.ID})
|
|
require.NoError(t, err)
|
|
require.Len(t, peerShares, 1)
|
|
require.Equal(t, peerShare.ID, peerShares[0].ID)
|
|
|
|
sentInboxes, err := ts.Store.ListInboxes(ctx, &store.FindInbox{SenderID: &user.ID})
|
|
require.NoError(t, err)
|
|
require.Empty(t, sentInboxes)
|
|
receivedInboxes, err := ts.Store.ListInboxes(ctx, &store.FindInbox{ReceiverID: &user.ID})
|
|
require.NoError(t, err)
|
|
require.Empty(t, receivedInboxes)
|
|
|
|
identities, err := ts.Store.ListUserIdentities(ctx, &store.FindUserIdentity{UserID: &user.ID})
|
|
require.NoError(t, err)
|
|
require.Empty(t, identities)
|
|
|
|
patSetting, err := ts.Store.GetUserSetting(ctx, &store.FindUserSetting{
|
|
UserID: &user.ID,
|
|
Key: storepb.UserSetting_PERSONAL_ACCESS_TOKENS,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Nil(t, patSetting)
|
|
}
|
|
|
|
func TestDeleteUserRollbackPreservesAllResources(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ts := NewTestService(t)
|
|
defer ts.Cleanup()
|
|
|
|
ctx := context.Background()
|
|
user, err := ts.CreateRegularUser(ctx, "rollback-owner")
|
|
require.NoError(t, err)
|
|
peer, err := ts.CreateRegularUser(ctx, "rollback-peer")
|
|
require.NoError(t, err)
|
|
|
|
userCtx := ts.CreateUserContext(ctx, user.ID)
|
|
ownMemo, err := ts.Service.CreateMemo(userCtx, &v1pb.CreateMemoRequest{
|
|
Memo: &v1pb.Memo{
|
|
Content: "rollback owner memo",
|
|
Visibility: v1pb.Visibility_PUBLIC,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
ownMemoUID, err := apiv1.ExtractMemoUIDFromName(ownMemo.Name)
|
|
require.NoError(t, err)
|
|
ownMemoStore, err := ts.Store.GetMemo(ctx, &store.FindMemo{UID: &ownMemoUID})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, ownMemoStore)
|
|
|
|
attachment, err := ts.Store.CreateAttachment(ctx, &store.Attachment{
|
|
UID: "attach-rollback-owner",
|
|
CreatorID: user.ID,
|
|
Filename: "rollback.txt",
|
|
Type: "text/plain",
|
|
Size: 8,
|
|
Blob: []byte("rollback"),
|
|
MemoID: &ownMemoStore.ID,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
reaction, err := ts.Store.UpsertReaction(ctx, &store.Reaction{
|
|
CreatorID: user.ID,
|
|
ContentID: ownMemo.Name,
|
|
ReactionType: "💥",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
share, err := ts.Store.CreateMemoShare(ctx, &store.MemoShare{
|
|
UID: "share-rollback-owner",
|
|
MemoID: ownMemoStore.ID,
|
|
CreatorID: user.ID,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
inbox, err := ts.Store.CreateInbox(ctx, &store.Inbox{
|
|
SenderID: peer.ID,
|
|
ReceiverID: user.ID,
|
|
Status: store.UNREAD,
|
|
Message: &storepb.InboxMessage{
|
|
Type: storepb.InboxMessage_MEMO_COMMENT,
|
|
Payload: &storepb.InboxMessage_MemoComment{
|
|
MemoComment: &storepb.InboxMessage_MemoCommentPayload{
|
|
MemoId: ownMemoStore.ID,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = ts.Store.CreateUserIdentity(ctx, &store.UserIdentity{
|
|
UserID: user.ID,
|
|
Provider: "google",
|
|
ExternUID: "rollback-owner-google-sub",
|
|
})
|
|
require.NoError(t, err)
|
|
err = ts.Store.AddUserPersonalAccessToken(ctx, user.ID, &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{
|
|
TokenId: "pat-rollback-owner",
|
|
TokenHash: "pat-rollback-owner-hash",
|
|
Description: "rollback pat",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
headerCtx := apiv1.WithHeaderCarrier(ctx)
|
|
failCtx := store.WithDeleteUserFailpoint(headerCtx, store.DeleteUserFailpointBeforeCommit)
|
|
authCtx := ts.CreateUserContext(failCtx, user.ID)
|
|
_, err = ts.Service.DeleteUser(authCtx, &v1pb.DeleteUserRequest{
|
|
Name: apiv1.BuildUserName(user.Username),
|
|
})
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, "delete user failpoint before commit")
|
|
|
|
userAfterRollback, err := ts.Store.GetUser(ctx, &store.FindUser{ID: &user.ID})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, userAfterRollback)
|
|
|
|
memoAfterRollback, err := ts.Store.GetMemo(ctx, &store.FindMemo{UID: &ownMemoUID})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, memoAfterRollback)
|
|
|
|
attachmentAfterRollback, err := ts.Store.GetAttachment(ctx, &store.FindAttachment{ID: &attachment.ID})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, attachmentAfterRollback)
|
|
|
|
reactionAfterRollback, err := ts.Store.GetReaction(ctx, &store.FindReaction{ID: &reaction.ID})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, reactionAfterRollback)
|
|
|
|
shareAfterRollback, err := ts.Store.GetMemoShare(ctx, &store.FindMemoShare{ID: &share.ID})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, shareAfterRollback)
|
|
|
|
inboxesAfterRollback, err := ts.Store.ListInboxes(ctx, &store.FindInbox{ID: &inbox.ID})
|
|
require.NoError(t, err)
|
|
require.Len(t, inboxesAfterRollback, 1)
|
|
|
|
identitiesAfterRollback, err := ts.Store.ListUserIdentities(ctx, &store.FindUserIdentity{UserID: &user.ID})
|
|
require.NoError(t, err)
|
|
require.Len(t, identitiesAfterRollback, 1)
|
|
|
|
patSetting, err := ts.Store.GetUserSetting(ctx, &store.FindUserSetting{
|
|
UserID: &user.ID,
|
|
Key: storepb.UserSetting_PERSONAL_ACCESS_TOKENS,
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, patSetting)
|
|
}
|
|
|
|
func TestDeleteUserReturnsErrorWhenAttachmentStorageCleanupFails(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ts := NewTestService(t)
|
|
defer ts.Cleanup()
|
|
|
|
ctx := context.Background()
|
|
user, err := ts.CreateRegularUser(ctx, "cleanup-failure-owner")
|
|
require.NoError(t, err)
|
|
|
|
_, err = ts.Store.CreateAttachment(ctx, &store.Attachment{
|
|
UID: "attach-cleanup-failure",
|
|
CreatorID: user.ID,
|
|
Filename: "failure.txt",
|
|
Type: "text/plain",
|
|
Size: 7,
|
|
Blob: []byte("failure"),
|
|
StorageType: storepb.AttachmentStorageType_LOCAL,
|
|
Reference: "cleanup-failure.txt",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
headerCtx := apiv1.WithHeaderCarrier(ctx)
|
|
failCtx := store.WithDeleteAttachmentStorageFailpoint(headerCtx)
|
|
authCtx := ts.CreateUserContext(failCtx, user.ID)
|
|
_, err = ts.Service.DeleteUser(authCtx, &v1pb.DeleteUserRequest{
|
|
Name: apiv1.BuildUserName(user.Username),
|
|
})
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, "attachment storage cleanup failed")
|
|
require.ErrorContains(t, err, "attachment_id=")
|
|
require.ErrorContains(t, err, store.ErrDeleteAttachmentStorageFailpoint.Error())
|
|
|
|
deletedUser, err := ts.Store.GetUser(ctx, &store.FindUser{ID: &user.ID})
|
|
require.NoError(t, err)
|
|
require.Nil(t, deletedUser)
|
|
}
|