e03b18ea21
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
71 lines
3.1 KiB
Go
71 lines
3.1 KiB
Go
package offsite
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-hub/internal/hetznerapi"
|
|
)
|
|
|
|
// orderAPI records when the sub-account is deleted relative to the purge.
|
|
type orderAPI struct {
|
|
*hetznerapi.Fake
|
|
log *[]string
|
|
}
|
|
|
|
func (o *orderAPI) DeleteSubaccount(ctx context.Context, boxID, subID int64) (hetznerapi.Action, error) {
|
|
*o.log = append(*o.log, "delete-subaccount")
|
|
return o.Fake.DeleteSubaccount(ctx, boxID, subID)
|
|
}
|
|
|
|
// R-32 option A (`09` §3 decision 167) — RESET's shared-tier teardown purges the folder through the sub-account's
|
|
// own login BEFORE the sub-account is deleted; a sub-account is a login, its folder survives its deletion.
|
|
//
|
|
// COMPANION RED-PROOF (observed): remove the PurgeShared call from Deprovision → this fails with
|
|
// "the folder must be purged before the sub-account is deleted; order [delete-subaccount]". Restored.
|
|
func TestDeprovision_R32_PurgesFolderBeforeDeletingSubaccount(t *testing.T) {
|
|
p, fake, _ := newTestProvisioner(t)
|
|
if _, err := p.ProvisionOffsite(context.Background(), "cust-r32", Input{Enabled: true, Type: "shared", QuotaGB: 10}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var order []string
|
|
p.API = &orderAPI{Fake: fake, log: &order}
|
|
p.PurgeShared = func(_ context.Context, id string) error { order = append(order, "purge:"+id); return nil }
|
|
if err := p.Deprovision(context.Background(), "cust-r32", "shared"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(order) != 2 || order[0] != "purge:cust-r32" || order[1] != "delete-subaccount" {
|
|
t.Fatalf("the folder must be purged before the sub-account is deleted; order %v", order)
|
|
}
|
|
}
|
|
|
|
// A failed purge keeps the sub-account (after its deletion only the main account reaches the folder), and the
|
|
// error reaches RESET (its hetzner leg reads "failed" and a re-run resumes).
|
|
// COMPANION RED-PROOF (observed): ignore PurgeShared's error → this fails with "a failed purge must keep the
|
|
// sub-account; deleted=1". Restored.
|
|
func TestDeprovision_R32_FailedPurgeKeepsSubaccount(t *testing.T) {
|
|
p, fake, _ := newTestProvisioner(t)
|
|
if _, err := p.ProvisionOffsite(context.Background(), "cust-r32b", Input{Enabled: true, Type: "shared", QuotaGB: 10}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
p.PurgeShared = func(context.Context, string) error { return errors.New("permission denied") }
|
|
if err := p.Deprovision(context.Background(), "cust-r32b", "shared"); err == nil {
|
|
t.Fatal("a failed purge must fail Deprovision")
|
|
}
|
|
if fake.DeletedSubaccounts != 0 {
|
|
t.Fatalf("a failed purge must keep the sub-account; deleted=%d", fake.DeletedSubaccounts)
|
|
}
|
|
}
|
|
|
|
// No purge route wired → refuse; never a silent delete that strands the folder.
|
|
func TestDeprovision_R32_NoPurgeRouteRefuses(t *testing.T) {
|
|
p, fake, _ := newTestProvisioner(t)
|
|
if _, err := p.ProvisionOffsite(context.Background(), "cust-r32c", Input{Enabled: true, Type: "shared", QuotaGB: 10}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := p.Deprovision(context.Background(), "cust-r32c", "shared"); err == nil || fake.DeletedSubaccounts != 0 {
|
|
t.Fatalf("no purge route must refuse and keep the sub-account; err=%v deleted=%d", err, fake.DeletedSubaccounts)
|
|
}
|
|
}
|