package cli //nolint:testpackage // needs access to unexported cleanStartupError import ( "errors" "os" "path/filepath" "testing" "go.uber.org/fx" "sneak.berlin/go/vaultik/internal/config" "sneak.berlin/go/vaultik/internal/pidlock" ) // TestCleanStartupError starts a real fx app whose config has a // blob_size_limit below the minimum. fx puts annotations naming the // functions it was building in front of the config error; only those // may be removed, although the config error itself contains "): ". func TestCleanStartupError(t *testing.T) { t.Parallel() path := filepath.Join(t.TempDir(), "config.yml") err := os.WriteFile(path, []byte("snapshots:\n"+ " test:\n"+ " paths: [/tmp/vaultik-test-source]\n"+ "storage_url: file:///tmp/vaultik-test-storage\n"+ "chunk_size: 10MB\n"+ "blob_size_limit: 30MB\n"), 0o600) if err != nil { t.Fatalf("write config: %v", err) } app := fx.New( fx.NopLogger, fx.Supply(config.Path(path)), config.Module, fx.Invoke(func(*config.Config) {}), ) err = app.Start(t.Context()) if err == nil { t.Fatal("app started; want a blob_size_limit error") } want := "failed to load config: invalid config: blob_size_limit must " + "be at least the largest chunk the chunker can emit (chunk_size " + "times the FastCDC size spread): need at least 40000000 bytes" got := cleanStartupError(err).Error() if got != want { t.Errorf("got %q, want %q", got, want) } } // TestCleanStartupErrorNotFromFx checks that an error fx did not wrap is // shown unchanged. func TestCleanStartupErrorNotFromFx(t *testing.T) { t.Parallel() //nolint:err113 // a one-off error standing in for a startup failure got := cleanStartupError(errors.New("plain error")).Error() if got != "plain error" { t.Errorf("got %q, want %q", got, "plain error") } } // TestLockScopedToMutatingCommands proves the partition the PID lock now // enforces: a read-only command runs while a mutator holds the lock, and // two mutating commands still mutually exclude. func TestLockScopedToMutatingCommands(t *testing.T) { t.Parallel() lockDir := filepath.Join(t.TempDir(), "vaultik") // A mutating command takes the process-wide lock. releaseMutator, err := acquireLockIfMutating(mutating, lockDir) if err != nil { t.Fatalf("mutating command could not acquire lock: %v", err) } // A read-only command runs to completion even while the lock is held. releaseReader, err := acquireLockIfMutating(readOnly, lockDir) if err != nil { t.Fatalf("read-only command was blocked by held lock: %v", err) } releaseReader() // A second mutating command is refused while the first holds the lock. _, err = acquireLockIfMutating(mutating, lockDir) if !errors.Is(err, pidlock.ErrAlreadyRunning) { t.Fatalf("second mutating command was not excluded, got: %v", err) } // Once the first mutator releases, another mutating command may run. releaseMutator() release, err := acquireLockIfMutating(mutating, lockDir) if err != nil { t.Fatalf("mutating command could not acquire released lock: %v", err) } release() }