Files
mygo/internal/repository/file_test.go
T
ld 032f716e49 feat(repo): add soft-delete with status filter to file queries
File repository now filters by status=active in all query methods (FindByID, FindByUserID, FindByParentID, FindByNameAndParent).

Delete now performs soft-delete by setting status to 'user_deleted' instead of physical row deletion.

Add ListIncludeDeleted to UserRepository for admin views of all users.

Add tests: StatusFilter, SoftDelete, DeleteIdempotent, StatusFilterCount.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-07-04 17:20:22 +08:00

316 lines
7.4 KiB
Go

package repository
import (
"context"
"testing"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"github.com/dhao2001/mygo/internal/model"
)
func setupFileRepo(t *testing.T) FileRepository {
t.Helper()
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil {
t.Fatalf("open db: %v", err)
}
if err := db.AutoMigrate(&model.File{}); err != nil {
t.Fatalf("migrate: %v", err)
}
return NewFileRepository(db)
}
func TestFileRepository_Create(t *testing.T) {
repo := setupFileRepo(t)
ctx := context.Background()
file := &model.File{
ID: "file-1",
UserID: "user-1",
Name: "test.txt",
Size: 1024,
Status: model.StatusActive,
}
if err := repo.Create(ctx, file); err != nil {
t.Fatalf("Create = %v", err)
}
}
func TestFileRepository_FindByID(t *testing.T) {
repo := setupFileRepo(t)
ctx := context.Background()
file := &model.File{
ID: "file-1",
UserID: "user-1",
Name: "test.txt",
Status: model.StatusActive,
}
if err := repo.Create(ctx, file); err != nil {
t.Fatalf("Create = %v", err)
}
found, err := repo.FindByID(ctx, "file-1")
if err != nil {
t.Fatalf("FindByID = %v", err)
}
if found.Name != "test.txt" {
t.Errorf("name = %q, want %q", found.Name, "test.txt")
}
}
func TestFileRepository_FindByIDNotFound(t *testing.T) {
repo := setupFileRepo(t)
ctx := context.Background()
_, err := repo.FindByID(ctx, "nonexistent")
if err != model.ErrNotFound {
t.Fatalf("expected ErrNotFound, got %v", err)
}
}
func TestFileRepository_FindByUserID(t *testing.T) {
repo := setupFileRepo(t)
ctx := context.Background()
files := []*model.File{
{ID: "f-1", UserID: "user-1", Name: "a.txt", Status: model.StatusActive},
{ID: "f-2", UserID: "user-1", Name: "b.txt", Status: model.StatusActive},
{ID: "f-3", UserID: "user-2", Name: "c.txt", Status: model.StatusActive},
}
for _, f := range files {
if err := repo.Create(ctx, f); err != nil {
t.Fatalf("Create = %v", err)
}
}
result, total, err := repo.FindByUserID(ctx, "user-1", 0, 10)
if err != nil {
t.Fatalf("FindByUserID = %v", err)
}
if len(result) != 2 {
t.Errorf("len(result) = %d, want 2", len(result))
}
if total != 2 {
t.Errorf("total = %d, want 2", total)
}
}
func TestFileRepository_FindByParentID(t *testing.T) {
repo := setupFileRepo(t)
ctx := context.Background()
parentID := "dir-1"
files := []*model.File{
{ID: "f-1", UserID: "user-1", ParentID: &parentID, Name: "a.txt", Status: model.StatusActive},
{ID: "f-2", UserID: "user-1", ParentID: &parentID, Name: "b.txt", Status: model.StatusActive},
{ID: "f-3", UserID: "user-1", Name: "c.txt", Status: model.StatusActive},
}
for _, f := range files {
if err := repo.Create(ctx, f); err != nil {
t.Fatalf("Create = %v", err)
}
}
children, err := repo.FindByParentID(ctx, "user-1", &parentID)
if err != nil {
t.Fatalf("FindByParentID = %v", err)
}
if len(children) != 2 {
t.Errorf("len(children) = %d, want 2", len(children))
}
}
func TestFileRepository_FindByParentIDNull(t *testing.T) {
repo := setupFileRepo(t)
ctx := context.Background()
parentID := "dir-1"
files := []*model.File{
{ID: "f-1", UserID: "user-1", ParentID: &parentID, Name: "a.txt", Status: model.StatusActive},
{ID: "f-2", UserID: "user-1", Name: "root.txt", Status: model.StatusActive},
}
for _, f := range files {
if err := repo.Create(ctx, f); err != nil {
t.Fatalf("Create = %v", err)
}
}
children, err := repo.FindByParentID(ctx, "user-1", nil)
if err != nil {
t.Fatalf("FindByParentID(nil) = %v", err)
}
if len(children) != 1 {
t.Errorf("len(children) = %d, want 1", len(children))
}
}
func TestFileRepository_Update(t *testing.T) {
repo := setupFileRepo(t)
ctx := context.Background()
file := &model.File{ID: "file-1", UserID: "user-1", Name: "original.txt", Status: model.StatusActive}
if err := repo.Create(ctx, file); err != nil {
t.Fatalf("Create = %v", err)
}
file.Name = "renamed.txt"
file.Size = 2048
if err := repo.Update(ctx, file); err != nil {
t.Fatalf("Update = %v", err)
}
found, err := repo.FindByID(ctx, "file-1")
if err != nil {
t.Fatalf("FindByID = %v", err)
}
if found.Name != "renamed.txt" {
t.Errorf("name = %q, want %q", found.Name, "renamed.txt")
}
if found.Size != 2048 {
t.Errorf("size = %d, want %d", found.Size, 2048)
}
}
func TestFileRepository_Delete(t *testing.T) {
repo := setupFileRepo(t)
ctx := context.Background()
file := &model.File{ID: "file-1", UserID: "user-1", Name: "test.txt", Status: model.StatusActive}
if err := repo.Create(ctx, file); err != nil {
t.Fatalf("Create = %v", err)
}
if err := repo.Delete(ctx, "file-1"); err != nil {
t.Fatalf("Delete = %v", err)
}
_, err := repo.FindByID(ctx, "file-1")
if err != model.ErrNotFound {
t.Fatalf("expected ErrNotFound after delete, got %v", err)
}
}
func TestFileRepository_SoftDelete(t *testing.T) {
repo := setupFileRepo(t)
ctx := context.Background()
file := &model.File{
ID: "file-1",
UserID: "user-1",
Name: "test.txt",
Status: model.StatusActive,
}
if err := repo.Create(ctx, file); err != nil {
t.Fatalf("Create = %v", err)
}
if err := repo.Delete(ctx, "file-1"); err != nil {
t.Fatalf("Delete = %v", err)
}
_, err := repo.FindByID(ctx, "file-1")
if err != model.ErrNotFound {
t.Fatalf("expected ErrNotFound after soft-delete, got %v", err)
}
}
func TestFileRepository_StatusFilter(t *testing.T) {
repo := setupFileRepo(t)
ctx := context.Background()
activeFile := &model.File{
ID: "f-1",
UserID: "user-1",
Name: "active.txt",
Status: model.StatusActive,
}
deletedFile := &model.File{
ID: "f-2",
UserID: "user-1",
Name: "deleted.txt",
Status: model.StatusUserDeleted,
}
if err := repo.Create(ctx, activeFile); err != nil {
t.Fatalf("Create active = %v", err)
}
if err := repo.Create(ctx, deletedFile); err != nil {
t.Fatalf("Create deleted = %v", err)
}
result, total, err := repo.FindByUserID(ctx, "user-1", 0, 10)
if err != nil {
t.Fatalf("FindByUserID = %v", err)
}
if len(result) != 1 {
t.Errorf("len(result) = %d, want 1", len(result))
}
if total != 1 {
t.Errorf("total = %d, want 1", total)
}
if result[0].ID != "f-1" {
t.Errorf("result[0].ID = %q, want %q", result[0].ID, "f-1")
}
}
func TestFileRepository_DeleteIdempotent(t *testing.T) {
repo := setupFileRepo(t)
ctx := context.Background()
file := &model.File{
ID: "file-1",
UserID: "user-1",
Name: "test.txt",
Status: model.StatusActive,
}
if err := repo.Create(ctx, file); err != nil {
t.Fatalf("Create = %v", err)
}
if err := repo.Delete(ctx, "file-1"); err != nil {
t.Fatalf("first Delete = %v", err)
}
if err := repo.Delete(ctx, "file-1"); err != nil {
t.Fatalf("second Delete = %v", err)
}
}
func TestFileRepository_StatusFilterCount(t *testing.T) {
repo := setupFileRepo(t)
ctx := context.Background()
activeFile := &model.File{
ID: "f-1",
UserID: "user-1",
Name: "active.txt",
Status: model.StatusActive,
}
deletedFile := &model.File{
ID: "f-2",
UserID: "user-1",
Name: "deleted.txt",
Status: model.StatusUserDeleted,
}
if err := repo.Create(ctx, activeFile); err != nil {
t.Fatalf("Create active = %v", err)
}
if err := repo.Create(ctx, deletedFile); err != nil {
t.Fatalf("Create deleted = %v", err)
}
_, total, err := repo.FindByUserID(ctx, "user-1", 0, 10)
if err != nil {
t.Fatalf("FindByUserID = %v", err)
}
if total != 1 {
t.Errorf("total = %d, want 1 (soft-deleted excluded from count)", total)
}
}