mirror of
https://github.com/go-vikunja/vikunja.git
synced 2026-11-09 00:22:38 -06:00
1201 lines
36 KiB
Go
1201 lines
36 KiB
Go
// Vikunja is a to-do list application to facilitate your life.
|
|
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package models
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io/fs"
|
|
"net/http"
|
|
"testing"
|
|
"time"
|
|
|
|
"code.vikunja.io/api/pkg/db"
|
|
"code.vikunja.io/api/pkg/events"
|
|
"code.vikunja.io/api/pkg/files"
|
|
"code.vikunja.io/api/pkg/user"
|
|
"code.vikunja.io/api/pkg/web"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"xorm.io/builder"
|
|
)
|
|
|
|
// Fixture projects 45 and 46 (child of 45) are in user 1's trash.
|
|
|
|
func trashProjectForTest(t *testing.T, projectID int64, a web.Auth) {
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
require.NoError(t, (&Project{ID: projectID}).Delete(s, a))
|
|
require.NoError(t, s.Commit())
|
|
}
|
|
|
|
func TestTrashProject_SubProjects(t *testing.T) {
|
|
u6 := &user.User{ID: 6}
|
|
|
|
t.Run("refused while a sub-project is another user's default", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
// 27 -> 12 -> 25 -> 26
|
|
_, err := s.Table("projects").Where("id = ?", 26).Update(map[string]any{"owner_id": 2})
|
|
require.NoError(t, err)
|
|
_, err = s.Table("users").Where("id = ?", 2).Update(map[string]any{"default_project_id": 26})
|
|
require.NoError(t, err)
|
|
|
|
err = (&Project{ID: 12}).Delete(s, u6)
|
|
var cannotDelete *ErrCannotDeleteDefaultProject
|
|
require.ErrorAs(t, err, &cannotDelete)
|
|
assert.Equal(t, int64(26), cannotDelete.ProjectID)
|
|
})
|
|
t.Run("stamps the whole subtree alike", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
// 27 -> 12 -> 25 -> 26
|
|
trashProjectForTest(t, 12, u6)
|
|
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
projects := []*Project{}
|
|
require.NoError(t, s.Unscoped().In("id", 12, 25, 26).Find(&projects))
|
|
require.Len(t, projects, 3)
|
|
require.False(t, projects[0].DeletedAt.IsZero())
|
|
for _, p := range projects {
|
|
assert.Equal(t, projects[0].DeletedAt, p.DeletedAt, "project %d", p.ID)
|
|
assert.Equal(t, int64(6), p.DeletedByID, "project %d", p.ID)
|
|
}
|
|
db.AssertExists(t, "projects", map[string]interface{}{
|
|
"id": 27,
|
|
"deleted_at": nil,
|
|
}, false)
|
|
})
|
|
t.Run("one deleted event per trashed project", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
events.ClearDispatchedEvents()
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
|
|
require.NoError(t, (&Project{ID: 12}).Delete(s, u6))
|
|
require.NoError(t, s.Commit())
|
|
events.DispatchPending(context.Background(), s)
|
|
|
|
trashed := []int64{}
|
|
for _, e := range events.GetDispatchedEvents((&ProjectDeletedEvent{}).Name()) {
|
|
trashed = append(trashed, e.(*ProjectDeletedEvent).Project.ID)
|
|
}
|
|
assert.ElementsMatch(t, []int64{
|
|
12,
|
|
25,
|
|
26,
|
|
}, trashed)
|
|
})
|
|
}
|
|
|
|
func TestTrashProject_DefaultProjects(t *testing.T) {
|
|
// Project 4 belongs to user 3 and is the default of users 2 and 3.
|
|
u3 := &user.User{ID: 3}
|
|
|
|
t.Run("owner may trash their own default", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
trashProjectForTest(t, 4, u3)
|
|
db.AssertCount(t, "projects", builder.And(
|
|
builder.Eq{"id": 4},
|
|
builder.NotNull{"deleted_at"},
|
|
), 1)
|
|
})
|
|
t.Run("clears a shared default for good", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
shareProjectForTest(t, 4, 2, PermissionRead)
|
|
trashProjectForTest(t, 4, u3)
|
|
db.AssertExists(t, "users", map[string]interface{}{
|
|
"id": 2,
|
|
"default_project_id": 0,
|
|
}, false)
|
|
|
|
require.NoError(t, restoreProjectForTest(t, 4, u3))
|
|
db.AssertExists(t, "users", map[string]interface{}{
|
|
"id": 2,
|
|
"default_project_id": 0,
|
|
}, false)
|
|
})
|
|
}
|
|
|
|
func restoreProjectForTest(t *testing.T, projectID int64, a web.Auth) error {
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
project := &Project{ID: projectID}
|
|
can, err := project.CanRestore(s, a)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !can {
|
|
return ErrGenericForbidden{}
|
|
}
|
|
if err := project.Restore(s, a); err != nil {
|
|
return err
|
|
}
|
|
require.NoError(t, s.Commit())
|
|
events.DispatchPending(context.Background(), s)
|
|
return nil
|
|
}
|
|
|
|
func TestProject_Restore(t *testing.T) {
|
|
u1 := &user.User{ID: 1}
|
|
|
|
t.Run("restores the project with its sub-projects", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
require.NoError(t, restoreProjectForTest(t, 45, u1))
|
|
events.AssertDispatched(t, &ProjectRestoredEvent{})
|
|
|
|
db.AssertExists(t, "projects", map[string]interface{}{
|
|
"id": 45,
|
|
"deleted_at": nil,
|
|
"deleted_by_id": nil,
|
|
}, false)
|
|
db.AssertExists(t, "projects", map[string]interface{}{
|
|
"id": 46,
|
|
"deleted_at": nil,
|
|
}, false)
|
|
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
task := &Task{}
|
|
has, err := s.ID(53).Get(task)
|
|
require.NoError(t, err)
|
|
require.True(t, has)
|
|
// CalDAV clients only pick it up again when updated moves
|
|
assert.True(t, task.Updated.After(time.Date(2018, 12, 1, 1, 12, 4, 0, time.UTC)), "updated is %s", task.Updated)
|
|
|
|
can, _, err := (&Task{ID: 54}).CanRead(s, u1)
|
|
require.NoError(t, err)
|
|
assert.True(t, can)
|
|
})
|
|
t.Run("one restored event per project", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
events.ClearDispatchedEvents()
|
|
|
|
require.NoError(t, restoreProjectForTest(t, 45, u1))
|
|
|
|
restored := []int64{}
|
|
for _, e := range events.GetDispatchedEvents((&ProjectRestoredEvent{}).Name()) {
|
|
restored = append(restored, e.(*ProjectRestoredEvent).Project.ID)
|
|
}
|
|
assert.ElementsMatch(t, []int64{
|
|
45,
|
|
46,
|
|
}, restored)
|
|
})
|
|
t.Run("one deleted event per project when trashed again", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
require.NoError(t, restoreProjectForTest(t, 45, u1))
|
|
events.ClearDispatchedEvents()
|
|
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
require.NoError(t, (&Project{ID: 45}).Delete(s, u1))
|
|
require.NoError(t, s.Commit())
|
|
events.DispatchPending(context.Background(), s)
|
|
|
|
trashed := []int64{}
|
|
for _, e := range events.GetDispatchedEvents((&ProjectDeletedEvent{}).Name()) {
|
|
trashed = append(trashed, e.(*ProjectDeletedEvent).Project.ID)
|
|
}
|
|
assert.ElementsMatch(t, []int64{
|
|
45,
|
|
46,
|
|
}, trashed)
|
|
})
|
|
t.Run("brings back sub-projects trashed separately before", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
u6 := &user.User{ID: 6}
|
|
// 27 -> 12 -> 25 -> 26
|
|
trashProjectForTest(t, 26, u6)
|
|
trashProjectForTest(t, 12, u6)
|
|
|
|
require.NoError(t, restoreProjectForTest(t, 12, u6))
|
|
for _, id := range []int64{
|
|
12,
|
|
25,
|
|
26,
|
|
} {
|
|
db.AssertExists(t, "projects", map[string]interface{}{
|
|
"id": id,
|
|
"deleted_at": nil,
|
|
}, false)
|
|
}
|
|
})
|
|
t.Run("brings back a sub-project someone else trashed before", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
u6 := &user.User{ID: 6}
|
|
s := db.NewSession()
|
|
_, err := s.Insert(&ProjectUser{
|
|
UserID: 1,
|
|
ProjectID: 25,
|
|
Permission: PermissionAdmin,
|
|
})
|
|
require.NoError(t, err)
|
|
require.NoError(t, s.Commit())
|
|
s.Close()
|
|
// 27 -> 12 -> 25 -> 26
|
|
trashProjectForTest(t, 26, u1)
|
|
trashProjectForTest(t, 12, u6)
|
|
|
|
require.NoError(t, restoreProjectForTest(t, 12, u6))
|
|
db.AssertExists(t, "projects", map[string]interface{}{
|
|
"id": 26,
|
|
"deleted_at": nil,
|
|
"deleted_by_id": nil,
|
|
}, false)
|
|
})
|
|
t.Run("leaves separately trashed tasks in the trash", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
trashProjectForTest(t, 1, u1)
|
|
require.NoError(t, restoreProjectForTest(t, 1, u1))
|
|
db.AssertCount(t, "tasks", builder.And(
|
|
builder.Eq{"id": 51},
|
|
builder.NotNull{"deleted_at"},
|
|
), 1)
|
|
})
|
|
t.Run("refused while the parent is in the trash", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
err := restoreProjectForTest(t, 46, u1)
|
|
require.ErrorAs(t, err, &ErrParentProjectIsInTrash{})
|
|
})
|
|
t.Run("refused while the parent is archived", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
// 21 is a child of archived 22
|
|
s := db.NewSession()
|
|
_, err := s.Table("projects").Where("id = ?", 21).Update(map[string]any{"deleted_at": time.Now()})
|
|
require.NoError(t, err)
|
|
require.NoError(t, s.Commit())
|
|
s.Close()
|
|
|
|
err = restoreProjectForTest(t, 21, u1)
|
|
assert.True(t, IsErrParentProjectIsArchived(err))
|
|
})
|
|
t.Run("admin share may restore, read share may not", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
// User 1 is admin of project 11 and reads project 9, both owned by others
|
|
trashProjectForTest(t, 11, &user.User{ID: 6})
|
|
trashProjectForTest(t, 9, &user.User{ID: 6})
|
|
|
|
require.NoError(t, restoreProjectForTest(t, 11, u1))
|
|
require.ErrorIs(t, restoreProjectForTest(t, 9, u1), ErrGenericForbidden{})
|
|
})
|
|
t.Run("unrelated user does not see it", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
assert.True(t, IsErrProjectDoesNotExist(restoreProjectForTest(t, 45, &user.User{ID: 2})))
|
|
})
|
|
t.Run("live project is not in the trash", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
assert.True(t, IsErrProjectDoesNotExist(restoreProjectForTest(t, 1, u1)))
|
|
})
|
|
}
|
|
|
|
func TestProject_ReadTrashed(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
|
|
_, _, err := (&Project{ID: 45}).CanRead(s, &user.User{ID: 1})
|
|
var inTrash ErrProjectIsInTrash
|
|
require.ErrorAs(t, err, &inTrash)
|
|
require.NotNil(t, inTrash.DeletedBy)
|
|
assert.Equal(t, int64(1), inTrash.DeletedBy.ID)
|
|
assert.True(t, inTrash.CanRestore)
|
|
assert.Zero(t, inTrash.TrashedRootID)
|
|
|
|
_, _, err = (&Project{ID: 45}).CanRead(s, &user.User{ID: 2})
|
|
assert.True(t, IsErrProjectDoesNotExist(err))
|
|
|
|
_, _, err = (&Task{ID: 53}).CanRead(s, &user.User{ID: 1})
|
|
require.ErrorAs(t, err, &inTrash)
|
|
}
|
|
|
|
func shareProjectForTest(t *testing.T, projectID, userID int64, permission Permission) {
|
|
t.Helper()
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
_, err := s.Insert(&ProjectUser{
|
|
UserID: userID,
|
|
ProjectID: projectID,
|
|
Permission: permission,
|
|
})
|
|
require.NoError(t, err)
|
|
require.NoError(t, s.Commit())
|
|
}
|
|
|
|
func TestProject_ReadTrashed_CanRestore(t *testing.T) {
|
|
u1 := &user.User{ID: 1}
|
|
readInTrash := func(t *testing.T, projectID int64, a web.Auth) ErrProjectIsInTrash {
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
_, _, err := (&Project{ID: projectID}).CanRead(s, a)
|
|
var inTrash ErrProjectIsInTrash
|
|
require.ErrorAs(t, err, &inTrash)
|
|
return inTrash
|
|
}
|
|
|
|
t.Run("not while the parent is in the trash", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
inTrash := readInTrash(t, 46, u1)
|
|
assert.False(t, inTrash.CanRestore)
|
|
assert.Equal(t, int64(45), inTrash.TrashedRootID)
|
|
assert.True(t, inTrash.CanRestoreRoot)
|
|
})
|
|
t.Run("a reader of the root can't restore it", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
shareProjectForTest(t, 45, 2, PermissionRead)
|
|
|
|
inTrash := readInTrash(t, 46, &user.User{ID: 2})
|
|
assert.Equal(t, int64(45), inTrash.TrashedRootID)
|
|
assert.False(t, inTrash.CanRestoreRoot)
|
|
})
|
|
t.Run("the root can't be restored while its parent is archived", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
s := db.NewSession()
|
|
_, err := s.Table("projects").Where("id = ?", 45).Update(map[string]any{"parent_project_id": 22})
|
|
require.NoError(t, err)
|
|
require.NoError(t, s.Commit())
|
|
s.Close()
|
|
|
|
inTrash := readInTrash(t, 46, u1)
|
|
assert.Equal(t, int64(45), inTrash.TrashedRootID)
|
|
assert.False(t, inTrash.CanRestoreRoot)
|
|
})
|
|
t.Run("an unreachable root is omitted", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
shareProjectForTest(t, 46, 2, PermissionAdmin)
|
|
|
|
inTrash := readInTrash(t, 46, &user.User{ID: 2})
|
|
assert.Zero(t, inTrash.TrashedRootID)
|
|
assert.False(t, inTrash.CanRestoreRoot)
|
|
details, err := json.Marshal(inTrash)
|
|
require.NoError(t, err)
|
|
assert.NotContains(t, string(details), "trashed_root_id")
|
|
})
|
|
t.Run("points deeper sub-projects to the topmost trashed ancestor", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
u6 := &user.User{ID: 6}
|
|
// 12 -> 25 -> 26
|
|
trashProjectForTest(t, 26, u6)
|
|
trashProjectForTest(t, 12, u6)
|
|
|
|
inTrash := readInTrash(t, 26, u6)
|
|
assert.False(t, inTrash.CanRestore)
|
|
assert.Equal(t, int64(12), inTrash.TrashedRootID)
|
|
})
|
|
t.Run("not while the parent is archived", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
// 21 is a child of archived 22
|
|
s := db.NewSession()
|
|
_, err := s.Table("projects").Where("id = ?", 21).Update(map[string]any{"deleted_at": time.Now()})
|
|
require.NoError(t, err)
|
|
require.NoError(t, s.Commit())
|
|
s.Close()
|
|
|
|
inTrash := readInTrash(t, 21, u1)
|
|
assert.False(t, inTrash.CanRestore)
|
|
assert.Zero(t, inTrash.TrashedRootID)
|
|
})
|
|
t.Run("not while the parent row is gone", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
s := db.NewSession()
|
|
_, err := s.Table("projects").Where("id = ?", 46).Update(map[string]any{"parent_project_id": 99999})
|
|
require.NoError(t, err)
|
|
require.NoError(t, s.Commit())
|
|
s.Close()
|
|
|
|
assert.False(t, readInTrash(t, 46, u1).CanRestore)
|
|
err = restoreProjectForTest(t, 46, u1)
|
|
var missing ErrParentProjectDoesNotExist
|
|
require.ErrorAs(t, err, &missing)
|
|
assert.Equal(t, http.StatusPreconditionFailed, missing.HTTPError().HTTPCode)
|
|
assert.Equal(t, ErrCodeParentProjectDoesNotExist, missing.HTTPError().Code)
|
|
})
|
|
}
|
|
|
|
func TestProject_ReadTrashed_Shares(t *testing.T) {
|
|
t.Run("read share sees the trash but can't restore", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
// User 1 reads project 9 of user 6
|
|
trashProjectForTest(t, 9, &user.User{ID: 6})
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
|
|
_, _, err := (&Project{ID: 9}).CanRead(s, &user.User{ID: 1})
|
|
var inTrash ErrProjectIsInTrash
|
|
require.ErrorAs(t, err, &inTrash)
|
|
assert.False(t, inTrash.CanRestore)
|
|
})
|
|
t.Run("link share of the project gets a plain not found", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
|
|
share := &LinkSharing{
|
|
ID: 3,
|
|
ProjectID: 45,
|
|
Permission: PermissionAdmin,
|
|
}
|
|
_, _, err := (&Project{ID: 45}).CanRead(s, share)
|
|
assert.True(t, IsErrProjectDoesNotExist(err))
|
|
assert.NotErrorAs(t, err, &ErrProjectIsInTrash{})
|
|
})
|
|
}
|
|
|
|
func TestProject_ReadTrashed_InstanceAdmin(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
admin := setUpInstanceAdmin(t)
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
|
|
_, _, err := (&Project{ID: 45}).CanRead(s, admin)
|
|
var inTrash ErrProjectIsInTrash
|
|
require.ErrorAs(t, err, &inTrash)
|
|
assert.True(t, inTrash.CanRestore)
|
|
}
|
|
|
|
func TestProject_TrashHidesContents(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
u1 := &user.User{ID: 1}
|
|
|
|
projects, _, _, err := (&Project{}).ReadAll(s, u1, "", 1, 500)
|
|
require.NoError(t, err)
|
|
projectIDs := []int64{}
|
|
for _, p := range projects.([]*Project) {
|
|
projectIDs = append(projectIDs, p.ID)
|
|
}
|
|
assert.Contains(t, projectIDs, int64(1))
|
|
assert.NotContains(t, projectIDs, int64(45))
|
|
assert.NotContains(t, projectIDs, int64(46))
|
|
|
|
tasks, _, _, err := (&TaskCollection{}).ReadAll(s, u1, "", 1, 500)
|
|
require.NoError(t, err)
|
|
taskIDs := []int64{}
|
|
for _, task := range tasks.([]*Task) {
|
|
taskIDs = append(taskIDs, task.ID)
|
|
}
|
|
assert.Contains(t, taskIDs, int64(1))
|
|
assert.NotContains(t, taskIDs, int64(53))
|
|
assert.NotContains(t, taskIDs, int64(54))
|
|
}
|
|
|
|
func TestGetTrashedProjects(t *testing.T) {
|
|
t.Run("topmost trashed projects with their sub-project count", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
|
|
projects, total, err := GetTrashedProjects(s, &user.User{ID: 1}, 1, 50)
|
|
require.NoError(t, err)
|
|
require.Len(t, projects, 1)
|
|
assert.Equal(t, int64(1), total)
|
|
assert.Equal(t, int64(45), projects[0].ID)
|
|
require.Len(t, projects[0].TrashedSubProjects, 1)
|
|
assert.Equal(t, &TrashedSubProject{
|
|
ID: 46,
|
|
Title: "Test46 trashed with its parent",
|
|
ParentProjectID: 45,
|
|
Depth: 1,
|
|
}, projects[0].TrashedSubProjects[0])
|
|
require.NotNil(t, projects[0].DeletedBy)
|
|
assert.Equal(t, int64(1), projects[0].DeletedBy.ID)
|
|
})
|
|
t.Run("separately trashed sub-projects fold into their ancestor", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
u6 := &user.User{ID: 6}
|
|
trashProjectForTest(t, 26, u6)
|
|
trashProjectForTest(t, 12, u6)
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
|
|
projects, _, err := GetTrashedProjects(s, u6, 1, 50)
|
|
require.NoError(t, err)
|
|
require.Len(t, projects, 1)
|
|
assert.Equal(t, int64(12), projects[0].ID)
|
|
// 12 -> 25 -> 26, shallowest first
|
|
require.Len(t, projects[0].TrashedSubProjects, 2)
|
|
assert.Equal(t, int64(25), projects[0].TrashedSubProjects[0].ID)
|
|
assert.Equal(t, int64(26), projects[0].TrashedSubProjects[1].ID)
|
|
assert.Equal(t, int64(25), projects[0].TrashedSubProjects[1].ParentProjectID)
|
|
})
|
|
t.Run("other users see nothing", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
|
|
projects, _, err := GetTrashedProjects(s, &user.User{ID: 2}, 1, 50)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, projects)
|
|
})
|
|
t.Run("link shares have no trash", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
|
|
_, _, err := GetTrashedProjects(s, &LinkSharing{
|
|
ID: 3,
|
|
ProjectID: 3,
|
|
Permission: PermissionAdmin,
|
|
}, 1, 50)
|
|
require.ErrorIs(t, err, ErrGenericForbidden{})
|
|
})
|
|
}
|
|
|
|
func purgedProjectDoers(t *testing.T) map[int64]*user.User {
|
|
t.Helper()
|
|
doers := map[int64]*user.User{}
|
|
for _, e := range events.GetDispatchedEvents((&ProjectPurgedEvent{}).Name()) {
|
|
purged, ok := e.(*ProjectPurgedEvent)
|
|
require.True(t, ok)
|
|
doers[purged.Project.ID] = purged.Doer
|
|
}
|
|
return doers
|
|
}
|
|
|
|
func TestWebhookListener_ParentReceivesChildPurged(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
u6 := &user.User{ID: 6}
|
|
// 12 -> 25 -> 26, all user6's; user15 only reads 26.
|
|
trashProjectForTest(t, 26, u6)
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
hook := func(createdByID int64) *Webhook {
|
|
w := &Webhook{
|
|
TargetURL: "https://example.com/purged",
|
|
Events: []string{"project.purged"},
|
|
ProjectID: 25,
|
|
CreatedByID: createdByID,
|
|
}
|
|
_, err := s.Insert(w)
|
|
require.NoError(t, err)
|
|
return w
|
|
}
|
|
owners := hook(6)
|
|
strangers := hook(15)
|
|
require.NoError(t, s.Commit())
|
|
|
|
events.ClearDispatchedEvents()
|
|
require.NoError(t, (&Project{ID: 26}).DeletePermanently(s, u6))
|
|
require.NoError(t, s.Commit())
|
|
events.DispatchPending(context.Background(), s)
|
|
purged := events.GetDispatchedEvents((&ProjectPurgedEvent{}).Name())
|
|
require.Len(t, purged, 1)
|
|
|
|
events.ClearDispatchedEvents()
|
|
events.TestListener(t, purged[0], &WebhookListener{EventName: "project.purged"})
|
|
|
|
delivered := webhookDeliveriesByID(t)
|
|
require.Contains(t, delivered, owners.ID)
|
|
assert.NotContains(t, delivered, strangers.ID, "creator can't read the webhook's project")
|
|
assert.NotContains(t, delivered[owners.ID].Payload.Data, "ancestor_ids")
|
|
}
|
|
|
|
func TestProject_DeletePermanently_SnapshotsAncestors(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
u6 := &user.User{ID: 6}
|
|
// 27 -> 12 -> 25 -> 26
|
|
trashProjectForTest(t, 12, u6)
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
|
|
events.ClearDispatchedEvents()
|
|
require.NoError(t, (&Project{ID: 12}).DeletePermanently(s, u6))
|
|
require.NoError(t, s.Commit())
|
|
events.DispatchPending(context.Background(), s)
|
|
|
|
ancestors := map[int64][]int64{}
|
|
for _, e := range events.GetDispatchedEvents((&ProjectPurgedEvent{}).Name()) {
|
|
purged := e.(*ProjectPurgedEvent)
|
|
ancestors[purged.Project.ID] = purged.AncestorIDs
|
|
}
|
|
assert.ElementsMatch(t, []int64{27}, ancestors[12])
|
|
assert.ElementsMatch(t, []int64{
|
|
27,
|
|
12,
|
|
}, ancestors[25])
|
|
assert.ElementsMatch(t, []int64{
|
|
27,
|
|
12,
|
|
25,
|
|
}, ancestors[26])
|
|
}
|
|
|
|
func TestProject_DeletePermanently(t *testing.T) {
|
|
purge := func(t *testing.T, projectID int64, a web.Auth) error {
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
project := &Project{ID: projectID}
|
|
can, err := project.CanDeletePermanently(s, a)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !can {
|
|
return ErrGenericForbidden{}
|
|
}
|
|
require.NoError(t, project.DeletePermanently(s, a))
|
|
if err := s.Commit(); err != nil {
|
|
return err
|
|
}
|
|
events.DispatchPending(context.Background(), s)
|
|
return nil
|
|
}
|
|
|
|
t.Run("admin purges a trashed project", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
events.ClearDispatchedEvents()
|
|
require.NoError(t, purge(t, 45, &user.User{ID: 1}))
|
|
db.AssertMissing(t, "projects", map[string]interface{}{"id": 45})
|
|
db.AssertMissing(t, "projects", map[string]interface{}{"id": 46})
|
|
|
|
doers := purgedProjectDoers(t)
|
|
require.Len(t, doers, 2)
|
|
for _, id := range []int64{
|
|
45,
|
|
46,
|
|
} {
|
|
require.NotNil(t, doers[id], "project %d", id)
|
|
assert.Equal(t, int64(1), doers[id].ID, "project %d", id)
|
|
}
|
|
})
|
|
t.Run("unrelated user does not see it", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
assert.True(t, IsErrProjectDoesNotExist(purge(t, 45, &user.User{ID: 2})))
|
|
db.AssertExists(t, "projects", map[string]interface{}{"id": 45}, false)
|
|
})
|
|
t.Run("live project is not in the trash", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
assert.True(t, IsErrProjectDoesNotExist(purge(t, 1, &user.User{ID: 1})))
|
|
})
|
|
t.Run("read share is denied", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
// User 1 reads project 9 of user 6
|
|
trashProjectForTest(t, 9, &user.User{ID: 6})
|
|
require.ErrorIs(t, purge(t, 9, &user.User{ID: 1}), ErrGenericForbidden{})
|
|
db.AssertExists(t, "projects", map[string]interface{}{"id": 9}, false)
|
|
})
|
|
t.Run("link share does not see it", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
share := &LinkSharing{
|
|
ID: 3,
|
|
ProjectID: 45,
|
|
Permission: PermissionAdmin,
|
|
}
|
|
assert.True(t, IsErrProjectDoesNotExist(purge(t, 45, share)))
|
|
db.AssertExists(t, "projects", map[string]interface{}{"id": 45}, false)
|
|
})
|
|
t.Run("takes the background along", func(t *testing.T) {
|
|
u6 := &user.User{ID: 6}
|
|
db.LoadAndAssertFixtures(t)
|
|
files.InitTestFileFixtures(t)
|
|
trashProjectForTest(t, 35, u6)
|
|
|
|
require.NoError(t, purge(t, 35, u6))
|
|
_, err := files.FileStat(&files.File{ID: 1})
|
|
require.ErrorIs(t, err, fs.ErrNotExist)
|
|
db.AssertMissing(t, "files", map[string]interface{}{"id": 1})
|
|
})
|
|
}
|
|
|
|
func TestEmptyProjectTrash(t *testing.T) {
|
|
t.Run("purges the auth's trash", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
events.ClearDispatchedEvents()
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
|
|
purged, err := EmptyProjectTrash(s, &user.User{ID: 1}, true)
|
|
require.NoError(t, err)
|
|
require.NoError(t, s.Commit())
|
|
events.DispatchPending(context.Background(), s)
|
|
assert.Equal(t, 1, purged)
|
|
db.AssertMissing(t, "projects", map[string]interface{}{"id": 45})
|
|
db.AssertMissing(t, "projects", map[string]interface{}{"id": 46})
|
|
|
|
doers := purgedProjectDoers(t)
|
|
require.Len(t, doers, 2)
|
|
for _, id := range []int64{
|
|
45,
|
|
46,
|
|
} {
|
|
require.NotNil(t, doers[id], "project %d", id)
|
|
assert.Equal(t, int64(1), doers[id].ID, "project %d", id)
|
|
}
|
|
})
|
|
t.Run("leaves other users' trash alone", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
|
|
purged, err := EmptyProjectTrash(s, &user.User{ID: 2}, true)
|
|
require.NoError(t, err)
|
|
require.NoError(t, s.Commit())
|
|
assert.Equal(t, 0, purged)
|
|
db.AssertExists(t, "projects", map[string]interface{}{"id": 45}, false)
|
|
db.AssertExists(t, "projects", map[string]interface{}{"id": 46}, false)
|
|
})
|
|
}
|
|
|
|
// User 1 only reads 12 through 27, so its admin share on 25 doesn't put 25 in their trash.
|
|
func TestProjectTrash_AdminOnlyOnTrashedChild(t *testing.T) {
|
|
u1 := &user.User{ID: 1}
|
|
setup := func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
s := db.NewSession()
|
|
_, err := s.Insert(&ProjectUser{
|
|
UserID: 1,
|
|
ProjectID: 25,
|
|
Permission: PermissionAdmin,
|
|
})
|
|
require.NoError(t, err)
|
|
require.NoError(t, s.Commit())
|
|
s.Close()
|
|
// 27 -> 12 -> 25 -> 26
|
|
trashProjectForTest(t, 12, &user.User{ID: 6})
|
|
}
|
|
|
|
t.Run("hidden from the trash list", func(t *testing.T) {
|
|
setup(t)
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
|
|
projects, total, err := GetTrashedProjects(s, u1, 1, 50)
|
|
require.NoError(t, err)
|
|
// Only the fixture trash
|
|
require.Len(t, projects, 1)
|
|
assert.Equal(t, int64(45), projects[0].ID)
|
|
assert.Equal(t, int64(1), total)
|
|
})
|
|
t.Run("reading tells it is in the trash, not restorable", func(t *testing.T) {
|
|
setup(t)
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
|
|
_, _, err := (&Project{ID: 25}).CanRead(s, u1)
|
|
var inTrash ErrProjectIsInTrash
|
|
require.ErrorAs(t, err, &inTrash)
|
|
assert.False(t, inTrash.CanRestore)
|
|
})
|
|
t.Run("restore is refused for the trashed parent", func(t *testing.T) {
|
|
setup(t)
|
|
require.ErrorAs(t, restoreProjectForTest(t, 25, u1), &ErrParentProjectIsInTrash{})
|
|
})
|
|
t.Run("emptying the trash leaves it", func(t *testing.T) {
|
|
setup(t)
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
|
|
purged, err := EmptyProjectTrash(s, u1, true)
|
|
require.NoError(t, err)
|
|
require.NoError(t, s.Commit())
|
|
assert.Equal(t, 1, purged)
|
|
db.AssertMissing(t, "projects", map[string]interface{}{"id": 45})
|
|
db.AssertExists(t, "projects", map[string]interface{}{"id": 25}, false)
|
|
})
|
|
t.Run("may still be purged directly", func(t *testing.T) {
|
|
setup(t)
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
|
|
project := &Project{ID: 25}
|
|
can, err := project.CanDeletePermanently(s, u1)
|
|
require.NoError(t, err)
|
|
require.True(t, can)
|
|
require.NoError(t, project.DeletePermanently(s, u1))
|
|
require.NoError(t, s.Commit())
|
|
db.AssertMissing(t, "projects", map[string]interface{}{"id": 25})
|
|
db.AssertMissing(t, "projects", map[string]interface{}{"id": 26})
|
|
db.AssertExists(t, "projects", map[string]interface{}{"id": 12}, false)
|
|
})
|
|
}
|
|
|
|
func TestDeleteExpiredProjects(t *testing.T) {
|
|
trashedAt := time.Date(2018, 12, 3, 10, 0, 0, 0, time.UTC)
|
|
|
|
t.Run("older than the retention period", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
events.ClearDispatchedEvents()
|
|
deleteExpiredProjects(trashedAt.Add(TrashRetention + time.Hour))
|
|
db.AssertMissing(t, "projects", map[string]interface{}{"id": 45})
|
|
db.AssertMissing(t, "projects", map[string]interface{}{"id": 46})
|
|
db.AssertExists(t, "projects", map[string]interface{}{"id": 1}, false)
|
|
|
|
doers := purgedProjectDoers(t)
|
|
require.Len(t, doers, 2)
|
|
assert.Contains(t, doers, int64(45))
|
|
assert.Nil(t, doers[45], "the system purges")
|
|
})
|
|
t.Run("within the retention period", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
deleteExpiredProjects(trashedAt.Add(TrashRetention - time.Hour))
|
|
db.AssertExists(t, "projects", map[string]interface{}{"id": 45}, false)
|
|
})
|
|
t.Run("sub-project trashed earlier expires before its parent", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
u6 := &user.User{ID: 6}
|
|
now := time.Now()
|
|
// 27 -> 12 -> 25 -> 26
|
|
trashProjectForTest(t, 26, u6)
|
|
s := db.NewSession()
|
|
_, err := s.Table("projects").Where("id = ?", 26).Update(map[string]any{"deleted_at": now.Add(-TrashRetention - time.Hour)})
|
|
require.NoError(t, err)
|
|
require.NoError(t, s.Commit())
|
|
s.Close()
|
|
trashProjectForTest(t, 12, u6)
|
|
|
|
deleteExpiredProjects(now)
|
|
db.AssertMissing(t, "projects", map[string]interface{}{"id": 26})
|
|
db.AssertCount(t, "projects", builder.And(
|
|
builder.In("id", 12, 25),
|
|
builder.NotNull{"deleted_at"},
|
|
), 2)
|
|
})
|
|
}
|
|
|
|
func TestGetProjectCount(t *testing.T) {
|
|
t.Run("counts the project and its sub-projects", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
|
|
tasks, err := s.Where("project_id = ?", 1).Count(&Task{})
|
|
require.NoError(t, err)
|
|
|
|
count, err := GetProjectCount(s, &user.User{ID: 1}, 1)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(0), count.SubProjects)
|
|
assert.Equal(t, tasks, count.Tasks)
|
|
// Entries 1, 2 and 4, all by user 1
|
|
assert.Equal(t, int64(3), count.TimeEntries)
|
|
assert.Equal(t, int64(1), count.TimeEntryAuthors)
|
|
|
|
count, err = GetProjectCount(s, &user.User{ID: 6}, 27)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(3), count.SubProjects)
|
|
|
|
_, err = GetProjectCount(s, &user.User{ID: 2}, 1)
|
|
require.ErrorIs(t, err, ErrGenericForbidden{})
|
|
})
|
|
t.Run("skips time entries on trashed tasks", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
|
|
// Task 51 in project 1 is in the trash
|
|
_, err := s.Insert(&TimeEntry{
|
|
UserID: 3,
|
|
TaskID: 51,
|
|
StartTime: time.Now(),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
count, err := GetProjectCount(s, &user.User{ID: 1}, 1)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(3), count.TimeEntries)
|
|
assert.Equal(t, int64(1), count.TimeEntryAuthors)
|
|
})
|
|
t.Run("instance admin gets no count for trashed or missing projects", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
admin := setUpInstanceAdmin(t)
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
|
|
_, err := GetProjectCount(s, admin, 45)
|
|
require.ErrorAs(t, err, &ErrProjectIsInTrash{})
|
|
|
|
_, err = GetProjectCount(s, admin, 99999)
|
|
assert.True(t, IsErrProjectDoesNotExist(err))
|
|
})
|
|
t.Run("admin link share is forbidden", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
|
|
share := &LinkSharing{
|
|
ID: 3,
|
|
ProjectID: 3,
|
|
Permission: PermissionAdmin,
|
|
}
|
|
_, err := GetProjectCount(s, share, 3)
|
|
require.ErrorIs(t, err, ErrGenericForbidden{})
|
|
})
|
|
}
|
|
|
|
func TestTimeEntry_TrashedProject(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
u3 := &user.User{ID: 3}
|
|
|
|
result, _, _, err := (&TimeEntry{}).ReadAll(s, u3, "", 1, 50)
|
|
require.NoError(t, err)
|
|
inTrash := map[int64]bool{}
|
|
for _, e := range result.([]*TimeEntry) {
|
|
inTrash[e.ID] = e.InTrash
|
|
}
|
|
assert.True(t, inTrash[5])
|
|
assert.True(t, inTrash[6])
|
|
|
|
result, _, _, err = (&TimeEntry{}).ReadAll(s, &user.User{ID: 1}, "", 1, 50)
|
|
require.NoError(t, err)
|
|
entryIDs := []int64{}
|
|
for _, e := range result.([]*TimeEntry) {
|
|
entryIDs = append(entryIDs, e.ID)
|
|
}
|
|
assert.Contains(t, entryIDs, int64(1))
|
|
assert.NotContains(t, entryIDs, int64(5))
|
|
assert.NotContains(t, entryIDs, int64(6))
|
|
|
|
read := &TimeEntry{ID: 6}
|
|
can, _, err := read.CanRead(s, u3)
|
|
require.NoError(t, err)
|
|
assert.True(t, can)
|
|
assert.True(t, read.InTrash)
|
|
|
|
can, err = (&TimeEntry{ID: 5}).CanDelete(s, u3)
|
|
require.NoError(t, err)
|
|
assert.False(t, can)
|
|
|
|
db.AssertCount(t, "time_entries", builder.Eq{"project_id": 45}, 1)
|
|
}
|
|
|
|
func TestBuildOverview_SkipsTrashedProjects(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
|
|
overview, err := BuildOverview(s)
|
|
require.NoError(t, err)
|
|
// 46 fixtures, 45 and 46 trashed
|
|
assert.Equal(t, int64(44), overview.Projects)
|
|
}
|
|
|
|
func TestCleanupAfterProjectAccessLoss_KeepsTrashedProjectsWithAccess(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
|
|
_, err := s.Insert(&TaskAssginee{
|
|
TaskID: 54,
|
|
UserID: 1,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// User 1 still owns trashed project 45 and its child 46
|
|
require.NoError(t, cleanupAfterProjectAccessLossForUsers(s, []int64{1}, []int64{45}))
|
|
require.NoError(t, s.Commit())
|
|
|
|
db.AssertExists(t, "task_assignees", map[string]interface{}{
|
|
"task_id": 54,
|
|
"user_id": 1,
|
|
}, false)
|
|
}
|
|
|
|
func TestProject_TrashedIdentifierStaysReserved(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
|
|
_, err := s.Table("projects").Where("id = ?", 45).Update(map[string]any{"identifier": "TRASHED"})
|
|
require.NoError(t, err)
|
|
|
|
err = CreateProject(s, &Project{
|
|
Title: "new",
|
|
Identifier: "trashed",
|
|
}, &user.User{ID: 1}, false, false)
|
|
assert.True(t, IsErrProjectIdentifierIsNotUnique(err))
|
|
}
|
|
|
|
func TestRepairOrphanedProjects_Trashed(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
|
|
_, err := s.ID(45).Unscoped().Delete(&Project{})
|
|
require.NoError(t, err)
|
|
|
|
_, err = RepairOrphanedProjects(s, false)
|
|
require.NoError(t, err)
|
|
require.NoError(t, s.Commit())
|
|
|
|
s2 := db.NewSession()
|
|
defer s2.Close()
|
|
repaired := &Project{}
|
|
has, err := s2.Unscoped().ID(46).Get(repaired)
|
|
require.NoError(t, err)
|
|
require.True(t, has)
|
|
assert.Equal(t, int64(0), repaired.parentID())
|
|
}
|
|
|
|
// Project.Delete trashes whole subtrees, so the project access query only has to
|
|
// drop trashed targets. Every path that adds a live project under a trashed one breaks that.
|
|
func assertNoLiveDescendantOfTrashedProject(t *testing.T) {
|
|
t.Helper()
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
|
|
violations := []*ProjectAncestor{}
|
|
err := s.Table("project_ancestors").
|
|
Select("project_ancestors.*").
|
|
Join("INNER", []string{"projects", "ancestor"}, "ancestor.id = project_ancestors.ancestor_id").
|
|
Join("INNER", []string{"projects", "descendant"}, "descendant.id = project_ancestors.project_id").
|
|
Where("ancestor.deleted_at IS NOT NULL AND descendant.deleted_at IS NULL").
|
|
Find(&violations)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, violations, "live projects below a trashed one")
|
|
}
|
|
|
|
func TestTrashedProjectNeverHasLiveDescendant(t *testing.T) {
|
|
u1 := &user.User{ID: 1}
|
|
u6 := &user.User{ID: 6}
|
|
|
|
t.Run("fixtures", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
assertNoLiveDescendantOfTrashedProject(t)
|
|
})
|
|
t.Run("trashing a project trashes its whole subtree", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
// 27 -> 12 -> 25 -> 26
|
|
trashProjectForTest(t, 12, u6)
|
|
assertNoLiveDescendantOfTrashedProject(t)
|
|
})
|
|
t.Run("restoring a sub-project of a trashed project is refused", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
require.Error(t, restoreProjectForTest(t, 46, u1))
|
|
assertNoLiveDescendantOfTrashedProject(t)
|
|
})
|
|
t.Run("restoring after a sub-project was trashed first", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
trashProjectForTest(t, 26, u6)
|
|
trashProjectForTest(t, 12, u6)
|
|
require.Error(t, restoreProjectForTest(t, 26, u6))
|
|
assertNoLiveDescendantOfTrashedProject(t)
|
|
|
|
require.NoError(t, restoreProjectForTest(t, 12, u6))
|
|
assertNoLiveDescendantOfTrashedProject(t)
|
|
})
|
|
doers := []struct {
|
|
name string
|
|
auth func(t *testing.T) web.Auth
|
|
}{
|
|
{"owner", func(*testing.T) web.Auth { return u1 }},
|
|
{"instance admin", func(t *testing.T) web.Auth { return setUpInstanceAdmin(t) }},
|
|
}
|
|
for _, doer := range doers {
|
|
t.Run("creating a project below a trashed one is refused for the "+doer.name, func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
a := doer.auth(t)
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
|
|
err := CreateProject(s, &Project{Title: "below the trash", ParentProjectID: Ptr(int64(45))}, a, false, false)
|
|
require.Error(t, err)
|
|
require.NoError(t, s.Commit())
|
|
assertNoLiveDescendantOfTrashedProject(t)
|
|
})
|
|
t.Run("moving a project below a trashed one is refused for the "+doer.name, func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
a := doer.auth(t)
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
|
|
project, err := GetProjectSimpleByID(s, 1)
|
|
require.NoError(t, err)
|
|
project.ParentProjectID = Ptr(int64(45))
|
|
require.Error(t, UpdateProject(s, project, a, false))
|
|
require.NoError(t, s.Commit())
|
|
assertNoLiveDescendantOfTrashedProject(t)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEmptyProjectTrash_OwnDeletionsOnly(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
u1 := &user.User{ID: 1}
|
|
s := db.NewSession()
|
|
_, err := s.Table("projects").Where("id = ?", 45).Update(map[string]any{"deleted_by_id": 2})
|
|
require.NoError(t, err)
|
|
require.NoError(t, s.Commit())
|
|
s.Close()
|
|
trashProjectForTest(t, 1, u1)
|
|
|
|
cs := db.NewSession()
|
|
count, err := CountProjectTrash(cs, u1)
|
|
cs.Close()
|
|
require.NoError(t, err)
|
|
// 45 (user 2, with child 46) and 1 (user 1)
|
|
assert.Equal(t, &TrashCount{Total: 2, ByOthers: 1}, count)
|
|
|
|
es := db.NewSession()
|
|
defer es.Close()
|
|
purged, err := EmptyProjectTrash(es, u1, false)
|
|
require.NoError(t, err)
|
|
require.NoError(t, es.Commit())
|
|
assert.Equal(t, 1, purged)
|
|
db.AssertMissing(t, "projects", map[string]interface{}{"id": 1})
|
|
db.AssertExists(t, "projects", map[string]interface{}{"id": 45}, false)
|
|
db.AssertExists(t, "projects", map[string]interface{}{"id": 46}, false)
|
|
}
|
|
|
|
func TestPurgeProject_KeepsTimeEntries(t *testing.T) {
|
|
u1 := &user.User{ID: 1}
|
|
// 5 is on project 45, 6 on its task 53
|
|
assertDetached := func(t *testing.T) {
|
|
t.Helper()
|
|
db.AssertMissing(t, "projects", map[string]interface{}{"id": 45})
|
|
for _, id := range []int64{
|
|
5,
|
|
6,
|
|
} {
|
|
db.AssertExists(t, "time_entries", map[string]interface{}{
|
|
"id": id,
|
|
"task_id": 0,
|
|
"project_id": 0,
|
|
}, false)
|
|
}
|
|
}
|
|
|
|
t.Run("purged directly", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
project := &Project{ID: 45}
|
|
can, err := project.CanDeletePermanently(s, u1)
|
|
require.NoError(t, err)
|
|
require.True(t, can)
|
|
require.NoError(t, project.DeletePermanently(s, u1))
|
|
require.NoError(t, s.Commit())
|
|
assertDetached(t)
|
|
})
|
|
t.Run("emptying the trash", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
_, err := EmptyProjectTrash(s, u1, true)
|
|
require.NoError(t, err)
|
|
require.NoError(t, s.Commit())
|
|
assertDetached(t)
|
|
})
|
|
t.Run("expired", func(t *testing.T) {
|
|
db.LoadAndAssertFixtures(t)
|
|
deleteExpiredProjects(time.Now())
|
|
assertDetached(t)
|
|
})
|
|
}
|