Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
309659f
X
evg4b Jun 23, 2026
e94d8f1
refactor: use server.Server directly in UncorsApp instead of uncors.U…
evg4b Jun 23, 2026
ab3cca3
refactor: delete internal/uncors package
evg4b Jun 23, 2026
eadf076
refactor: deduplicate mappingsToTargets by moving it to di.Container.…
evg4b Jun 23, 2026
ec1f2b9
feat: enable debug logging via UNCORS_LOGGING env variable
evg4b Jun 23, 2026
78e6ac1
fix: resolve linter warnings in main, cli, and uncors_app
evg4b Jun 23, 2026
9aaf285
fix: remove self-import cycle in cli package
evg4b Jun 23, 2026
1d7e534
refactor: use cli.RunUncors in integration tests, add ctx to RunUncors
evg4b Jun 23, 2026
80178cd
fix: pass --interactive=false in bootProxy and fix proxy.go lint issues
evg4b Jun 23, 2026
6ff0197
fix: add omitempty to CacheConfig yaml fields
evg4b Jun 23, 2026
063cc0f
test: add coverage for cli, di.Targets, and main to meet SonarCloud gate
evg4b Jun 23, 2026
4202204
fix: prevent yaml.v3 round-trip failures for rewrite host and script …
evg4b Jun 23, 2026
52f70b8
test: add coverage for MarshalYAML, watcher edge cases, and cli reloa…
evg4b Jun 23, 2026
16ce76b
refactor: remove debug parameter in favor of UNCORS_LOGGING env var
evg4b Jun 23, 2026
089635b
fix: protect Server.listeners with RWMutex and add SetupLogging tests
evg4b Jun 23, 2026
54e0740
test: split cli test file to match code organization
evg4b Jun 23, 2026
912b261
refactor: split commands.go into separate modules by responsibility
evg4b Jun 23, 2026
3da2d1c
Provide version
evg4b Jun 28, 2026
fdf876b
Restore help functionality
evg4b Jun 28, 2026
04c04ec
fix: add missing blank line before return in GenerateCerts (nlreturn)
evg4b Jun 28, 2026
6febd9c
Restored help command
evg4b Jun 28, 2026
43afa20
Added version
evg4b Jun 28, 2026
e53778c
fix: replace println with fmt.Fprintln and add missing blank lines (f…
evg4b Jun 28, 2026
5180d0b
test: add coverage for --version, --help, and ErrVersionRequested paths
evg4b Jun 28, 2026
a6c7992
Extract container in first level
evg4b Jun 28, 2026
50f7c6e
Cleanup code
evg4b Jun 28, 2026
5a6e19f
fix: replace os.Exit-triggering tests with version/help flag paths
evg4b Jun 28, 2026
853b98a
fix: inject osExit to enable testing handleError without killing the …
evg4b Jun 28, 2026
e632ca9
Added integration tests
evg4b Jun 28, 2026
adeac37
Fixed tests
evg4b Jun 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ UNCORS follows a clean layered architecture with middleware composition:

**`internal/infra`** - Infrastructure services
- HTTP client with connection pooling and proxy support
- Logger setup (logs to stderr or file with debug flag)
- Logger setup (logs to stderr or file based on UNCORS_LOGGING env var)
- TLS certificate generation and handling

**`internal/tui`** - Terminal UI and logging
Expand Down Expand Up @@ -142,8 +142,7 @@ Key test flags:

**Key Config Options**
- `proxy`: Upstream proxy URL (optional)
- `interactive`: Enable TUI mode
- `debug`: Enable debug logging
- `interactive`: Enable TUI mode (default: true)
- `port`: Listen port (default: 3000)
- `mappings`: Array of request mappings (from/to hosts)

Expand Down Expand Up @@ -177,7 +176,7 @@ Key test flags:
4. Update CONTRIBUTING.md if user-facing

### Debugging
- Enable debug logs: `./uncors -d` (writes to `uncors.log`)
- Enable logging: Set `UNCORS_LOGGING=/path/to/logfile` environment variable
- Run single test: `go test -run TestName ./internal/handler/proxy/`
- Race detector: Already enabled in `make test` and `make test-cover`
- Integration tests: `make test-integration` (slower, real network)
Expand Down
28 changes: 28 additions & 0 deletions internal/cli/generate_certs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package cli

import (
"errors"

"github.com/evg4b/uncors/internal/di"
"github.com/spf13/pflag"
)

const GenerateCertsCmd = "generate-certs"

func GenerateCerts(container *di.Container) error {
cmd := container.GenerateCertsCommand()

flags := pflag.NewFlagSet(GenerateCertsCmd, pflag.ContinueOnError)
cmd.DefineFlags(flags, container.Version())

err := flags.Parse(container.Args())
if err != nil {
if !errors.Is(err, pflag.ErrHelp) {
return err
}

return nil
}

return cmd.Execute()
}
29 changes: 29 additions & 0 deletions internal/cli/generate_certs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package cli_test

import (
"testing"

"github.com/evg4b/uncors/internal/cli"
"github.com/evg4b/uncors/internal/di"
"github.com/stretchr/testify/require"
)

func TestGenerateCerts(t *testing.T) {
t.Run("returns error for unknown flag", func(t *testing.T) {
err := cli.GenerateCerts(di.NewContainer(di.WithArgs([]string{"--unknown-flag"})))
require.Error(t, err)
})

t.Run("generates CA certificate with valid args", func(t *testing.T) {
// Point HOME to a temp dir so certs go there, not ~/.config/uncors.
t.Setenv("HOME", t.TempDir())

err := cli.GenerateCerts(di.NewContainer(di.WithArgs([]string{"--validity-days=7"})))
require.NoError(t, err)
})

t.Run("returns nil for --help flag", func(t *testing.T) {
err := cli.GenerateCerts(di.NewContainer(di.WithArgs([]string{"--help"})))
require.NoError(t, err)
})
}
33 changes: 33 additions & 0 deletions internal/cli/run_ineractive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package cli

import (
"context"

tea "charm.land/bubbletea/v2"
"github.com/evg4b/uncors/internal/config"
"github.com/evg4b/uncors/internal/di"
uncor "github.com/evg4b/uncors/internal/uncors_app"
)

func runIneractive(
ctx context.Context,
container *di.Container,
cfg *config.UncorsConfig,
cfgPath string,
) error {
app := uncor.NewUncorsApp(
container,
cfgPath,
cfg,
func() *config.UncorsConfig {
reloaded, _, _ := config.LoadConfiguration(container.Fs(), container.Version(), container.Args())

return reloaded
},
)

_, err := tea.NewProgram(app, tea.WithContext(ctx)).
Run()

return err
}
124 changes: 124 additions & 0 deletions internal/cli/run_non_ineractive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package cli

import (
"context"
"log"
"os"
"os/signal"
"syscall"
"time"

"github.com/evg4b/uncors/internal/config"
"github.com/evg4b/uncors/internal/di"
"github.com/evg4b/uncors/internal/server"
"github.com/evg4b/uncors/internal/tui"
)

const shutdownTimeout = 15 * time.Second

func runNonIneractive(
ctx context.Context,
container *di.Container,
cfg *config.UncorsConfig,
cfgPath string,
) error {
output := container.CliOutput()
tui.PrintLogo(output, container.Version())
output.Print("")
output.WarnBox(tui.DisclaimerMessage)
output.Print("")
output.InfoBox(cfg.Mappings.String())
output.Print("")

targets, err := container.Targets(cfg)
if err != nil {
return err
}

srv := container.Server()

err = srv.Start(ctx, targets)
if err != nil {
return err
}

go startVersionChecker(ctx, container, cfg.Proxy)

go func() {
watcher := config.NewWatcher(cfgPath)

err := watcher.Watch(ctx, func() { reloadServer(ctx, container, srv) })
if err != nil {
output.Error(err)
}
}()

go func() { //nolint:gosec // G118: shutdown needs a fresh context because parent ctx is being cancelled
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)

defer signal.Stop(stop)

select {
case sig := <-stop:
if sig == syscall.SIGINT {
_, _ = os.Stdout.WriteString("\n")
}

log.Println("shutdown signal received")
case <-ctx.Done():
}

shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
defer cancel()

_ = srv.Shutdown(shutdownCtx)
}()

srv.Wait()
output.Info("Server was stopped")

return nil
}

func reloadServer(ctx context.Context, container *di.Container, srv *server.Server) {
output := container.CliOutput()

newUncorsConfig, _, err := config.LoadConfiguration(container.Fs(), container.Version(), container.Args())
if err != nil {
output.Error(err)

return
}

output.Info("Restarting server....")

targets, err := container.Targets(newUncorsConfig)
if err != nil {
output.Error(err)

return
}

err = srv.Restart(ctx, targets)
if err != nil {
output.Error(err)

return
}

output.InfoBox(
"Server restarted",
newUncorsConfig.Mappings.String(),
)
}

// startVersionChecker waits for a short delay then checks for a newer release.
func startVersionChecker(ctx context.Context, container *di.Container, proxy string) {
const checkDelay = 50 * time.Millisecond

time.Sleep(checkDelay)

container.VersionChecker(proxy).
CheckNewVersion(ctx)
}
42 changes: 42 additions & 0 deletions internal/cli/run_uncors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package cli

import (
"context"
"errors"
"fmt"
"os"

"github.com/evg4b/uncors/internal/config"
"github.com/evg4b/uncors/internal/di"
"github.com/spf13/pflag"
)

func RunUncors(ctx context.Context, container *di.Container) error {
uncorsConfig, path, err := config.LoadConfiguration(container.Fs(), container.Version(), container.Args())
if err != nil {
if errors.Is(err, config.ErrVersionRequested) {
fmt.Fprintln(os.Stdout, container.Version())

return nil
}

if errors.Is(err, pflag.ErrHelp) {
return nil
}

return err
}

var runError error
if uncorsConfig.Interactive {
runError = runIneractive(ctx, container, uncorsConfig, path)
} else {
runError = runNonIneractive(ctx, container, uncorsConfig, path)
}

if runError != nil && !errors.Is(runError, pflag.ErrHelp) {
return runError
}

return nil
}
Loading