You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

164 lines
6.0 KiB

package mock
import (
"context"
"errors"
"os"
"path/filepath"
"strings"
"tera-clone/services/tera-api/internal/domain"
"tera-clone/services/tera-api/internal/supportbundle"
"tera-clone/services/tera-api/internal/tlscert"
"testing"
)
type resetRebooter struct {
calls int
check func()
}
func (r *resetRebooter) Reboot(context.Context) error {
if r.check != nil {
r.check()
}
r.calls++
return nil
}
func TestFactoryResetErasesPersistedDataAndRestoresOnlyTerauser(t *testing.T) {
root := t.TempDir()
certOptions := tlscert.Options{Root: filepath.Join(root, "tls"), Clock: fixedClock}
certs, err := tlscert.Open(certOptions)
if err != nil {
t.Fatal(err)
}
bundles, err := supportbundle.NewManager(supportbundle.Options{Directory: filepath.Join(root, "bundles")})
if err != nil {
t.Fatal(err)
}
reboot := &resetRebooter{}
options := StoreOptions{ConfigPath: filepath.Join(root, "config.json"), AuthPath: filepath.Join(root, "auth.json"), SessionPath: filepath.Join(root, "sessions.json"), LoginFailurePath: filepath.Join(root, "failures.json"), SecretPath: filepath.Join(root, "secrets.json"), OperationPath: filepath.Join(root, "operations.json"), AuditPath: filepath.Join(root, "audit.jsonl"), SystemLogPath: filepath.Join(root, "system.jsonl"), PortLogPath: filepath.Join(root, "port.jsonl"), LinkLogPath: filepath.Join(root, "link.jsonl"), TLSCertificateStore: certs, SupportBundleService: bundles, SystemController: reboot}
store, err := NewStoreWithOptions(fixedClock, options)
if err != nil {
t.Fatal(err)
}
defer store.Close()
oldSession, err := store.Login(context.Background(), domain.AuthLoginRequest{Username: "admin", Password: "mock-admin-secret"})
if err != nil {
t.Fatal(err)
}
if err := store.secrets.Set("customer/secret", []byte("customer-secret-marker")); err != nil {
t.Fatal(err)
}
store.systemLogEvents = []domain.SystemLogEvent{{Message: "old-system-marker"}}
store.portLogEvents = []domain.PortLogEvent{{Message: "old-port-marker"}}
store.linkLogEvents = []domain.LinkLogEvent{{Message: "old-link-marker"}}
store.authGroupOverrides["customer"] = authGroupRecord{Name: "Customer", Role: "port_operator"}
store.addressFilters = domain.AddressFilterSettings{Enabled: true, Mode: "whitelist"}
store.loginFailures["customer"] = loginFailureRecord{}
if _, err := certs.Generate("customer", tlscert.Subject{CommonName: "customer.example"}); err != nil {
t.Fatal(err)
}
if _, err := bundles.Create(context.Background(), supportbundle.CreateRequest{BundleID: "support_bundle_20260908_000001", GeneratedAt: fixedClock(), Sections: []supportbundle.Section{{Name: "config.json", Value: "old-bundle-marker"}}}); err != nil {
t.Fatal(err)
}
store.runningManagement.CaptchaEnabled = false
store.persistentManagement.CaptchaEnabled = false
reboot.check = func() {
content, err := os.ReadFile(options.AuthPath)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(content), `"admin"`) || !strings.Contains(string(content), `"terauser"`) {
t.Fatal("reboot requested before account reset was persisted")
}
}
if _, err := store.FactoryReset(context.Background()); err != nil {
t.Fatal(err)
}
if !store.runningManagement.CaptchaEnabled || !store.persistentManagement.CaptchaEnabled {
t.Fatal("factory reset disabled CAPTCHA")
}
if reboot.calls != 1 {
t.Fatal("reset did not request reboot")
}
if _, err := store.ActorBySession(context.Background(), oldSession.SessionToken); err == nil {
t.Fatal("old session survived")
}
if len(store.authUsers) != 1 || len(store.authGroupOverrides) != 0 || len(store.authTokens) != 0 || len(store.loginFailures) != 0 || store.addressFilters.Enabled {
t.Fatal("security state survived reset")
}
if store.secrets.Has("customer/secret") {
t.Fatal("stored credential survived")
}
if len(certs.List("factory-default")) != 1 {
t.Fatal("customer certificate survived")
}
if _, err := bundles.Read(context.Background(), "support_bundle_20260908_000001"); err == nil {
t.Fatal("support bundle survived")
}
for _, path := range []string{options.SystemLogPath, options.PortLogPath, options.LinkLogPath, options.AuditPath} {
content, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(content), "old-") {
t.Fatalf("old log survived in %s", path)
}
}
options.RemoveLegacyAuthUsers = true
options.TLSCertificateStore, err = tlscert.Open(certOptions)
if err != nil {
t.Fatal(err)
}
reopened, err := NewStoreWithOptions(fixedClock, options)
if err != nil {
t.Fatal(err)
}
defer reopened.Close()
if !reopened.runningManagement.CaptchaEnabled {
t.Fatal("CAPTCHA disabled after reboot")
}
if len(reopened.authUsers) != 1 {
t.Fatalf("bootstrap accounts resurrected: %d", len(reopened.authUsers))
}
if _, err := reopened.Login(context.Background(), domain.AuthLoginRequest{Username: "terauser", Password: "terauser"}); err != nil {
t.Fatal(err)
}
if _, err := reopened.Login(context.Background(), domain.AuthLoginRequest{Username: "admin", Password: "mock-admin-secret"}); err == nil {
t.Fatal("old account works after restart")
}
}
func TestFactoryResetCleanupFailureDoesNotRebootOrClearAccounts(t *testing.T) {
reboot := &resetRebooter{}
store, err := NewStoreWithOptions(fixedClock, StoreOptions{SystemController: reboot, FactoryResetCleanup: func() error { return errors.New("cleanup failed") }})
if err != nil {
t.Fatal(err)
}
defer store.Close()
if _, err := store.FactoryReset(context.Background()); err == nil {
t.Fatal("reset unexpectedly succeeded")
}
if reboot.calls != 0 || len(store.authUsers) != 3 {
t.Fatal("failed cleanup reported success or changed accounts")
}
}
func TestFactoryResetBlocksFurtherMutations(t *testing.T) {
s := NewStore(fixedClock)
defer s.Close()
release, ok := s.BeginMutation()
if !ok {
t.Fatal("new store rejected writes")
}
release()
if _, err := s.FactoryReset(context.Background()); err != nil {
t.Fatal(err)
}
release, ok = s.BeginMutation()
defer release()
if ok {
t.Fatal("writes allowed after reset before reboot")
}
}