test: add test fixtures and tests for project soft-delete

- Add soft-deleted project fixtures (IDs 41, 42, 43)
- Update existing delete tests to verify soft-delete behavior
- Add tests for restore, list deleted, and permanent delete
- Verify soft-deleted projects are excluded from ReadAll and permissions
This commit is contained in:
kolaente 2026-03-26 16:14:42 +01:00
parent 56f42a293c
commit 86cabee5c6
3 changed files with 222 additions and 18 deletions

View File

@ -361,3 +361,39 @@
position: 40
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
# Soft-deleted project owned by user 1
-
id: 41
title: Test41 soft-deleted
description: A soft-deleted project
identifier: test41
owner_id: 1
parent_project_id: 0
position: 41
deleted_at: 2026-03-20 10:00:00
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
# Soft-deleted parent project owned by user 1 (parent of 43)
-
id: 42
title: Test42 soft-deleted parent
description: A soft-deleted parent project
identifier: test42
owner_id: 1
parent_project_id: 0
position: 42
deleted_at: 2026-03-20 10:00:00
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
# Soft-deleted child project owned by user 1 (child of 42)
-
id: 43
title: Test43 soft-deleted child
description: A soft-deleted child project
identifier: test43
owner_id: 1
parent_project_id: 42
position: 43
deleted_at: 2026-03-20 10:00:00
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12

View File

@ -347,6 +347,31 @@ func TestProject_CreateOrUpdate(t *testing.T) {
})
}
func assertSoftDeleted(t *testing.T, projectID int64) {
t.Helper()
s := db.NewSession()
defer s.Close()
// Use Unscoped to bypass soft-delete filter
p := &Project{}
exists, err := s.Unscoped().Where("id = ?", projectID).Get(p)
require.NoError(t, err)
require.True(t, exists, "Project %d should still exist in db after soft-delete", projectID)
assert.NotNil(t, p.DeletedAt, "Project %d should have deleted_at set", projectID)
}
func assertNotSoftDeleted(t *testing.T, projectID int64) {
t.Helper()
s := db.NewSession()
defer s.Close()
p := &Project{}
exists, err := s.Unscoped().Where("id = ?", projectID).Get(p)
require.NoError(t, err)
require.True(t, exists, "Project %d should exist in db", projectID)
assert.Nil(t, p.DeletedAt, "Project %d should not have deleted_at set", projectID)
}
func TestProject_Delete(t *testing.T) {
t.Run("normal", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
@ -359,12 +384,12 @@ func TestProject_Delete(t *testing.T) {
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
db.AssertMissing(t, "projects", map[string]interface{}{
// With soft-delete, project row still exists but has deleted_at set
assertSoftDeleted(t, 1)
// Tasks should still exist (not permanently deleted)
db.AssertExists(t, "tasks", map[string]interface{}{
"id": 1,
})
db.AssertMissing(t, "tasks", map[string]interface{}{
"id": 1,
})
}, false)
})
t.Run("with background", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
@ -378,12 +403,11 @@ func TestProject_Delete(t *testing.T) {
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
db.AssertMissing(t, "projects", map[string]interface{}{
"id": 35,
})
db.AssertMissing(t, "files", map[string]interface{}{
// Project is soft-deleted, background file still exists
assertSoftDeleted(t, 35)
db.AssertExists(t, "files", map[string]interface{}{
"id": 1,
})
}, false)
})
t.Run("default project of the same user", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
@ -407,7 +431,7 @@ func TestProject_Delete(t *testing.T) {
require.Error(t, err)
assert.True(t, IsErrCannotDeleteDefaultProject(err))
})
t.Run("deletes archived parent and its child atomically", func(t *testing.T) {
t.Run("soft-deletes archived parent and its child", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
@ -420,10 +444,10 @@ func TestProject_Delete(t *testing.T) {
err = s.Commit()
require.NoError(t, err)
db.AssertMissing(t, "projects", map[string]interface{}{"id": 22})
db.AssertMissing(t, "projects", map[string]interface{}{"id": 21})
assertSoftDeleted(t, 22)
assertSoftDeleted(t, 21)
})
t.Run("deletes deeply nested child projects recursively", func(t *testing.T) {
t.Run("soft-deletes deeply nested child projects recursively", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
@ -435,10 +459,151 @@ func TestProject_Delete(t *testing.T) {
err = s.Commit()
require.NoError(t, err)
db.AssertMissing(t, "projects", map[string]interface{}{"id": 27})
db.AssertMissing(t, "projects", map[string]interface{}{"id": 12})
db.AssertMissing(t, "projects", map[string]interface{}{"id": 25})
db.AssertMissing(t, "projects", map[string]interface{}{"id": 26})
assertSoftDeleted(t, 27)
assertSoftDeleted(t, 12)
assertSoftDeleted(t, 25)
assertSoftDeleted(t, 26)
})
t.Run("soft-deleted projects are excluded from ReadAll", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
// Project 41 is soft-deleted and owned by user 1
p := &Project{}
projects, _, _, err := p.ReadAll(s, &user.User{ID: 1}, "", 1, 50)
require.NoError(t, err)
projectList := projects.([]*Project)
for _, proj := range projectList {
assert.NotEqual(t, int64(41), proj.ID, "Soft-deleted project 41 should not appear in ReadAll")
assert.NotEqual(t, int64(42), proj.ID, "Soft-deleted project 42 should not appear in ReadAll")
assert.NotEqual(t, int64(43), proj.ID, "Soft-deleted project 43 should not appear in ReadAll")
}
})
t.Run("soft-deleted projects return no permission", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
// Project 41 is soft-deleted and owned by user 1
// XORM auto-filters soft-deleted projects, so CanRead will error with "Project does not exist"
p := &Project{ID: 41}
canRead, _, err := p.CanRead(s, &user.User{ID: 1})
if err != nil {
assert.True(t, IsErrProjectDoesNotExist(err), "Expected project not found error for soft-deleted project")
} else {
assert.False(t, canRead, "Should not be able to read soft-deleted project")
}
})
}
func TestProject_Restore(t *testing.T) {
t.Run("restore soft-deleted project", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
// Project 41 is soft-deleted and owned by user 1
project, err := RestoreProject(s, 41, &user.User{ID: 1})
require.NoError(t, err)
require.NotNil(t, project)
assert.Nil(t, project.DeletedAt)
err = s.Commit()
require.NoError(t, err)
assertNotSoftDeleted(t, 41)
})
t.Run("restore soft-deleted parent restores children", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
// Project 42 is soft-deleted parent, 43 is soft-deleted child
_, err := RestoreProject(s, 42, &user.User{ID: 1})
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
assertNotSoftDeleted(t, 42)
assertNotSoftDeleted(t, 43)
})
t.Run("restore non-existent project", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
_, err := RestoreProject(s, 999, &user.User{ID: 1})
require.Error(t, err)
assert.True(t, IsErrProjectDoesNotExist(err))
})
t.Run("restore project without admin access", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
// Project 41 is owned by user 1, user 2 has no access
_, err := RestoreProject(s, 41, &user.User{ID: 2})
require.Error(t, err)
})
}
func TestProject_GetDeletedProjects(t *testing.T) {
t.Run("returns deleted projects for user", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
// User 1 owns soft-deleted projects 41, 42, 43
projects, err := GetDeletedProjects(s, &user.User{ID: 1})
require.NoError(t, err)
assert.Len(t, projects, 3)
ids := make(map[int64]bool)
for _, p := range projects {
ids[p.ID] = true
}
assert.True(t, ids[41])
assert.True(t, ids[42])
assert.True(t, ids[43])
})
t.Run("returns empty for user with no deleted projects", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
// User 2 owns no soft-deleted projects
projects, err := GetDeletedProjects(s, &user.User{ID: 2})
require.NoError(t, err)
assert.Empty(t, projects)
})
}
func TestProject_PermanentDelete(t *testing.T) {
t.Run("permanently deletes project and all related entities", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
// First soft-delete the project
project := Project{ID: 1}
err := project.Delete(s, &user.User{ID: 1})
require.NoError(t, err)
err = s.Commit()
require.NoError(t, err)
// Now permanently delete it
s2 := db.NewSession()
defer s2.Close()
p := &Project{ID: 1}
err = p.PermanentDelete(s2, &user.User{ID: 1})
require.NoError(t, err)
err = s2.Commit()
require.NoError(t, err)
db.AssertMissing(t, "projects", map[string]interface{}{"id": 1})
db.AssertMissing(t, "tasks", map[string]interface{}{"id": 1})
})
}

View File

@ -48,6 +48,9 @@ func TestProject(t *testing.T) {
assert.NotContains(t, rec.Body.String(), `Test5`)
assert.NotContains(t, rec.Body.String(), `Test21`) // Archived through parent project
assert.NotContains(t, rec.Body.String(), `Test22`) // Archived directly
assert.NotContains(t, rec.Body.String(), `Test41`) // Soft-deleted
assert.NotContains(t, rec.Body.String(), `Test42`) // Soft-deleted parent
assert.NotContains(t, rec.Body.String(), `Test43`) // Soft-deleted child
})
t.Run("Search", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"s": []string{"Test1"}}, nil)