可靠性: 完善 R2 备份恢复与对象生命周期
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
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, "[email protected]", "outbox-owner")
|
||||
other := createOutboxTestUser(t, ctx, users, "[email protected]", "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, "[email protected]", "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)
|
||||
}
|
||||
Reference in New Issue
Block a user