mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
feat: enable soft delete for organizations (#16584)
- Add deleted column to organizations table - Add trigger to check for existing workspaces, templates, groups and members in a org before allowing the soft delete --------- Co-authored-by: Steven Masley <stevenmasley@gmail.com> Co-authored-by: Steven Masley <Emyrk@users.noreply.github.com>
This commit is contained in:
@ -21,6 +21,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/database/dbfake"
|
||||
"github.com/coder/coder/v2/coderd/database/dbgen"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||
@ -2916,6 +2917,136 @@ func TestGetUserStatusCounts(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrganizationDeleteTrigger(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if !dbtestutil.WillUsePostgres() {
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
t.Run("WorkspaceExists", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
|
||||
orgA := dbfake.Organization(t, db).Do()
|
||||
|
||||
user := dbgen.User(t, db, database.User{})
|
||||
|
||||
dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
|
||||
OrganizationID: orgA.Org.ID,
|
||||
OwnerID: user.ID,
|
||||
}).Do()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
err := db.UpdateOrganizationDeletedByID(ctx, database.UpdateOrganizationDeletedByIDParams{
|
||||
UpdatedAt: dbtime.Now(),
|
||||
ID: orgA.Org.ID,
|
||||
})
|
||||
require.Error(t, err)
|
||||
// cannot delete organization: organization has 1 workspaces and 1 templates that must be deleted first
|
||||
require.ErrorContains(t, err, "cannot delete organization")
|
||||
require.ErrorContains(t, err, "has 1 workspaces")
|
||||
require.ErrorContains(t, err, "1 templates")
|
||||
})
|
||||
|
||||
t.Run("TemplateExists", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
|
||||
orgA := dbfake.Organization(t, db).Do()
|
||||
|
||||
user := dbgen.User(t, db, database.User{})
|
||||
|
||||
dbgen.Template(t, db, database.Template{
|
||||
OrganizationID: orgA.Org.ID,
|
||||
CreatedBy: user.ID,
|
||||
})
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
err := db.UpdateOrganizationDeletedByID(ctx, database.UpdateOrganizationDeletedByIDParams{
|
||||
UpdatedAt: dbtime.Now(),
|
||||
ID: orgA.Org.ID,
|
||||
})
|
||||
require.Error(t, err)
|
||||
// cannot delete organization: organization has 0 workspaces and 1 templates that must be deleted first
|
||||
require.ErrorContains(t, err, "cannot delete organization")
|
||||
require.ErrorContains(t, err, "has 0 workspaces")
|
||||
require.ErrorContains(t, err, "1 templates")
|
||||
})
|
||||
|
||||
t.Run("ProvisionerKeyExists", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
|
||||
orgA := dbfake.Organization(t, db).Do()
|
||||
|
||||
dbgen.ProvisionerKey(t, db, database.ProvisionerKey{
|
||||
OrganizationID: orgA.Org.ID,
|
||||
})
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
err := db.UpdateOrganizationDeletedByID(ctx, database.UpdateOrganizationDeletedByIDParams{
|
||||
UpdatedAt: dbtime.Now(),
|
||||
ID: orgA.Org.ID,
|
||||
})
|
||||
require.Error(t, err)
|
||||
// cannot delete organization: organization has 1 provisioner keys that must be deleted first
|
||||
require.ErrorContains(t, err, "cannot delete organization")
|
||||
require.ErrorContains(t, err, "1 provisioner keys")
|
||||
})
|
||||
|
||||
t.Run("GroupExists", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
|
||||
orgA := dbfake.Organization(t, db).Do()
|
||||
|
||||
dbgen.Group(t, db, database.Group{
|
||||
OrganizationID: orgA.Org.ID,
|
||||
})
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
err := db.UpdateOrganizationDeletedByID(ctx, database.UpdateOrganizationDeletedByIDParams{
|
||||
UpdatedAt: dbtime.Now(),
|
||||
ID: orgA.Org.ID,
|
||||
})
|
||||
require.Error(t, err)
|
||||
// cannot delete organization: organization has 1 groups that must be deleted first
|
||||
require.ErrorContains(t, err, "cannot delete organization")
|
||||
require.ErrorContains(t, err, "has 1 groups")
|
||||
})
|
||||
|
||||
t.Run("MemberExists", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
|
||||
orgA := dbfake.Organization(t, db).Do()
|
||||
|
||||
userA := dbgen.User(t, db, database.User{})
|
||||
userB := dbgen.User(t, db, database.User{})
|
||||
|
||||
dbgen.OrganizationMember(t, db, database.OrganizationMember{
|
||||
OrganizationID: orgA.Org.ID,
|
||||
UserID: userA.ID,
|
||||
})
|
||||
|
||||
dbgen.OrganizationMember(t, db, database.OrganizationMember{
|
||||
OrganizationID: orgA.Org.ID,
|
||||
UserID: userB.ID,
|
||||
})
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
err := db.UpdateOrganizationDeletedByID(ctx, database.UpdateOrganizationDeletedByIDParams{
|
||||
UpdatedAt: dbtime.Now(),
|
||||
ID: orgA.Org.ID,
|
||||
})
|
||||
require.Error(t, err)
|
||||
// cannot delete organization: organization has 1 members that must be deleted first
|
||||
require.ErrorContains(t, err, "cannot delete organization")
|
||||
require.ErrorContains(t, err, "has 1 members")
|
||||
})
|
||||
}
|
||||
|
||||
func requireUsersMatch(t testing.TB, expected []database.User, found []database.GetUsersRow, msg string) {
|
||||
t.Helper()
|
||||
require.ElementsMatch(t, expected, database.ConvertUserRows(found), msg)
|
||||
|
Reference in New Issue
Block a user