package db import ( "context" "testing" "github.com/stretchr/testify/require" "github.com/tamakyi/TamaBox/internal/conf" ) func createOutboxTestUser(t *testing.T, ctx context.Context, store UsersStore, email, domain string) *User { t.Helper() require.NoError(t, store.Create(ctx, CreateUserOptions{ Name: domain, Password: "test-only-password", Email: email, Domain: domain, })) user, err := store.GetByEmail(ctx, email) require.NoError(t, err) return user } func TestPurgeUserContentEnqueuesOwnedR2Objects(t *testing.T) { setR2TestConfig(t) conn, _ := newTestDB(t) ctx := context.Background() users := NewUsersStore(conn) owner := createOutboxTestUser(t, ctx, users, "outbox-owner@example.test", "outbox-owner") other := createOutboxTestUser(t, ctx, users, "outbox-other@example.test", "outbox-other") require.NoError(t, conn.Model(owner).Updates(map[string]interface{}{ "avatar": "https://cdn.example.test/assets/profile-owned.webp", "background": "https://cdn.example.test/assets/default-background.webp", "background_image": "https://cdn.example.test/assets/profile-shared.webp", }).Error) require.NoError(t, conn.Model(other).Update("background_image", "https://cdn.example.test/assets/profile-shared.webp").Error) questions := NewQuestionsStore(conn) question, err := questions.Create(ctx, CreateQuestionOptions{UserID: owner.ID, Content: "owned"}) require.NoError(t, err) otherQuestion, err := questions.Create(ctx, CreateQuestionOptions{UserID: other.ID, Content: "other"}) require.NoError(t, err) images := NewUploadImagesStore(conn) owned, err := images.Create(ctx, CreateUploadImageOptions{QuestionID: question.ID, UploaderUserID: owner.ID, Key: "owned.webp"}) require.NoError(t, err) _, err = images.Create(ctx, CreateUploadImageOptions{QuestionID: question.ID, UploaderUserID: owner.ID, Key: "owned.webp"}) require.NoError(t, err) legacy, err := images.Create(ctx, CreateUploadImageOptions{ QuestionID: question.ID, UploaderUserID: owner.ID, Key: "https://cdn.example.test/assets/legacy.webp", }) require.NoError(t, err) external, err := images.Create(ctx, CreateUploadImageOptions{ QuestionID: question.ID, UploaderUserID: owner.ID, Key: "https://external.example.net/external.webp", }) require.NoError(t, err) shared, err := images.Create(ctx, CreateUploadImageOptions{QuestionID: question.ID, UploaderUserID: owner.ID, Key: "shared.webp"}) require.NoError(t, err) require.NoError(t, conn.WithContext(ctx).Create(&UploadImageQuestion{ UploadImageID: shared.ID, QuestionID: otherQuestion.ID, }).Error) conversation, message, err := NewConversationStore(conn).CreateWithFirstMessage(ctx, CreateConversationOptions{ OwnerID: owner.ID, AskerID: other.ID, MaxMessages: 20, }, other.ID, "private") require.NoError(t, err) require.NotNil(t, conversation) messageImages := NewMessageImageStore(conn) for _, opts := range []CreateMessageImageOptions{ {MessageID: message.ID, ObjectKey: "message-key.webp", URL: "https://cdn.example.test/assets/message-key.webp"}, {MessageID: message.ID, ObjectKey: "owned.webp", URL: "https://cdn.example.test/assets/owned.webp"}, {MessageID: message.ID, URL: "https://cdn.example.test/assets/message-legacy.webp"}, {MessageID: message.ID, URL: "https://external.example.net/message-external.webp"}, } { _, err = messageImages.Create(ctx, opts) require.NoError(t, err) } require.NoError(t, users.Deactivate(ctx, owner.ID)) require.Zero(t, uploadImageCount(t, conn, owned.ID)) require.Zero(t, uploadImageCount(t, conn, legacy.ID)) require.Zero(t, uploadImageCount(t, conn, external.ID)) require.Equal(t, int64(1), uploadImageCount(t, conn, shared.ID)) var sharedLinks int64 require.NoError(t, conn.WithContext(ctx).Model(&UploadImageQuestion{}). Where("upload_image_id = ? AND question_id = ?", shared.ID, otherQuestion.ID).Count(&sharedLinks).Error) require.Equal(t, int64(1), sharedLinks) due, err := NewR2DeleteTaskStore(conn).ListDue(ctx, 20) require.NoError(t, err) keys := make([]string, 0, len(due)) for _, task := range due { keys = append(keys, task.Key) } require.ElementsMatch(t, []string{ "owned.webp", "legacy.webp", "message-key.webp", "message-legacy.webp", "profile-owned.webp", "profile-shared.webp", }, keys) require.NotContains(t, keys, "shared.webp") require.NotContains(t, keys, "external.webp") require.NotContains(t, keys, "message-external.webp") require.NotContains(t, keys, "default-background.webp") var deactivated User require.NoError(t, conn.Unscoped().First(&deactivated, owner.ID).Error) require.Empty(t, deactivated.Avatar) require.Empty(t, deactivated.Background) require.Empty(t, deactivated.BackgroundImage) other, err = users.GetByID(ctx, other.ID) require.NoError(t, err) require.Equal(t, "https://cdn.example.test/assets/profile-shared.webp", other.BackgroundImage) referenced, err := NewR2DeleteTaskStore(conn).HasReferences(ctx, conf.Upload.ImageBucket, "profile-shared.webp") require.NoError(t, err) require.True(t, referenced) referenced, err = NewR2DeleteTaskStore(conn).HasReferences(ctx, conf.Upload.ImageBucket, "default-background.webp") require.NoError(t, err) require.True(t, referenced) } func TestPurgeUserContentOutboxFailureRollsBack(t *testing.T) { setR2TestConfig(t) conn, _ := newTestDB(t) ctx := context.Background() users := NewUsersStore(conn) owner := createOutboxTestUser(t, ctx, users, "outbox-rollback@example.test", "outbox-rollback") question, err := NewQuestionsStore(conn).Create(ctx, CreateQuestionOptions{UserID: owner.ID, Content: "rollback"}) require.NoError(t, err) image, err := NewUploadImagesStore(conn).Create(ctx, CreateUploadImageOptions{ QuestionID: question.ID, UploaderUserID: owner.ID, Key: "rollback.webp", }) require.NoError(t, err) require.NoError(t, conn.Migrator().DropTable(&R2DeleteTask{})) require.Error(t, users.Deactivate(ctx, owner.ID)) require.Equal(t, int64(1), uploadImageCount(t, conn, image.ID)) var links int64 require.NoError(t, conn.WithContext(ctx).Model(&UploadImageQuestion{}). Where("upload_image_id = ? AND question_id = ?", image.ID, question.ID).Count(&links).Error) require.Equal(t, int64(1), links) var questions int64 require.NoError(t, conn.WithContext(ctx).Unscoped().Model(&Question{}).Where("id = ?", question.ID).Count(&questions).Error) require.Equal(t, int64(1), questions) var user User require.NoError(t, conn.WithContext(ctx).First(&user, owner.ID).Error) require.False(t, user.DeletedAt.Valid) }