diff options
| -rw-r--r-- | AGENTS.md | 9 | ||||
| -rw-r--r-- | internal/cli/cli.go | 114 | ||||
| -rw-r--r-- | internal/cli/cli_test.go | 34 | ||||
| -rw-r--r-- | internal/config/config.go | 18 | ||||
| -rw-r--r-- | internal/namespace/namespace_test.go | 14 | ||||
| -rw-r--r-- | internal/wireguard/wireguard_test.go | 8 | ||||
| -rw-r--r-- | pkg/wgconf/wgconf_test.go | 8 | ||||
| -rw-r--r-- | tests/e2e/e2e_test.go | 22 |
8 files changed, 196 insertions, 31 deletions
@@ -36,7 +36,14 @@ No piece of code is considered "done" until it has passed the full verification If any of these tools report an error or warning, the code must be corrected before the task is marked as complete. -### 2. Testing Strategy +### 2. Testing & Stubbing Conventions +To maintain a high-velocity development cycle without sacrificing correctness, we follow these rules for incomplete code: + +- **Code Stubs**: Any unimplemented logic path must be explicitly marked with a `// TODO` comment and return a descriptive error (e.g., `fmt.Errorf("feature X not yet implemented")`). +- **Test Stubs**: Any test that is planned but not yet implementable must use `t.Skip("not implemented")` and include a comment describing the specific scenario the test is intended to verify. +- **Hermetic Configuration**: Tests involving profiles, settings, or filesystem state must not touch the actual user home directory. Use the `ConfigDir` injection pattern in the `App` struct combined with `t.TempDir()` to create isolated, temporary test environments. + +### 3. Testing Strategy We employ a three-tier testing approach to balance speed and reliability: | Tier | Location | Type | Scope | Requirement | diff --git a/internal/cli/cli.go b/internal/cli/cli.go new file mode 100644 index 0000000..cb95202 --- /dev/null +++ b/internal/cli/cli.go @@ -0,0 +1,114 @@ +package cli + +import ( + "flag" + "fmt" + + "git.theodohertyfamily.com/tools/wg-wrap/internal/config" +) + +type App struct { + Args []string + ConfigDir string // Optional override for profile storage location +} + +func NewApp(args []string) *App { + return &App{Args: args} +} + +func (a *App) Run() error { + // Handle subcommands first (profile list, import, configure, delete, stop) + if len(a.Args) > 1 && a.Args[1] == "profile" { + return a.handleProfileCmd() + } + + cfg := &config.Config{} + + fs := flag.NewFlagSet("wg-wrap", flag.ExitOnError) + fs.StringVar(&cfg.Profile, "profile", "", "WireGuard profile to use (filename without extension in ~/.config/wg-wrap/profiles/)") + fs.StringVar(&cfg.DNSServer, "dns-server", "", "Override DNS server to use") + + args := a.Args[1:] + sepIdx := -1 + for i, arg := range args { + if arg == "--" { + sepIdx = i + break + } + } + + var flagsToParse []string + if sepIdx != -1 { + flagsToParse = args[:sepIdx] + cfg.Command = args[sepIdx+1:] + } else { + flagsToParse = args + } + + err := fs.Parse(flagsToParse) + if err != nil { + return fmt.Errorf("error parsing flags: %w", err) + } + + if sepIdx == -1 { + cfg.Command = fs.Args() + } + + if len(cfg.Command) == 0 { + return fmt.Errorf("no command provided. use --help for usage") + } + + if cfg.Profile == "" { + cfg.Profile = "default" + } + + profilesDir := a.ConfigDir + if profilesDir == "" { + profilesDir = config.GetDefaultProfilesDir() + } + + fmt.Printf("Profile: %s\n", cfg.Profile) + fmt.Printf("Profiles Directory: %s\n", profilesDir) + fmt.Printf("DNS Server: %s\n", cfg.DNSServer) + fmt.Printf("Command: %v\n", cfg.Command) + return nil +} + +func (a *App) handleProfileCmd() error { + if len(a.Args) < 3 { + return fmt.Errorf("usage: wg-wrap profile <list|import|configure|delete|stop> [args]") + } + + subCmd := a.Args[2] + switch subCmd { + case "list": + fmt.Println("Listing profiles...") + return fmt.Errorf("profile list not yet implemented") + case "import": + if len(a.Args) < 4 { + return fmt.Errorf("usage: wg-wrap profile import <path>") + } + fmt.Printf("Importing profile from %s...\n", a.Args[3]) + return fmt.Errorf("profile import not yet implemented") + case "configure": + if len(a.Args) < 4 { + return fmt.Errorf("usage: wg-wrap profile configure <name>") + } + fmt.Printf("Configuring profile %s...\n", a.Args[3]) + return fmt.Errorf("profile configure not yet implemented") + case "delete": + if len(a.Args) < 4 { + return fmt.Errorf("usage: wg-wrap profile delete <name>") + } + fmt.Printf("Deleting profile %s...\n", a.Args[3]) + return fmt.Errorf("profile delete not yet implemented") + case "stop": + if len(a.Args) < 4 { + return fmt.Errorf("usage: wg-wrap profile stop <name>") + } + fmt.Printf("Stopping profile %s and unpinning namespace...\n", a.Args[3]) + return fmt.Errorf("profile stop not yet implemented") + default: + return fmt.Errorf("unknown profile subcommand: %s", subCmd) + } +} diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go new file mode 100644 index 0000000..71ff6cb --- /dev/null +++ b/internal/cli/cli_test.go @@ -0,0 +1,34 @@ +package cli + +import ( + "testing" +) + +func TestAppRun_ProfileDirInjection(t *testing.T) { + // Set up a temporary directory to simulate XDG_CONFIG_HOME/wg-wrap/profiles + tmpDir := t.TempDir() + + tests := []struct { + name string + args []string + wantErr bool + }{ + { + name: "valid profile with injected dir", + args: []string{"wg-wrap", "--profile", "test-vpn", "curl", "google.com"}, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + app := NewApp(tt.args) + app.ConfigDir = tmpDir // Inject temporary directory + + err := app.Run() + if (err != nil) != tt.wantErr { + t.Errorf("App.Run() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/config/config.go b/internal/config/config.go index 5aa8462..d81a1f6 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,7 +1,25 @@ package config +import ( + "os" + "path/filepath" +) + type Config struct { Profile string DNSServer string Command []string } + +// GetDefaultProfilesDir returns the standard XDG path for wg-wrap profiles. +func GetDefaultProfilesDir() string { + configHome := os.Getenv("XDG_CONFIG_HOME") + if configHome == "" { + home, err := os.UserHomeDir() + if err != nil { + return "etc/wg-wrap/profiles" // Fallback + } + configHome = filepath.Join(home, ".config") + } + return filepath.Join(configHome, "wg-wrap", "profiles") +} diff --git a/internal/namespace/namespace_test.go b/internal/namespace/namespace_test.go index cfa0e9b..e39710d 100644 --- a/internal/namespace/namespace_test.go +++ b/internal/namespace/namespace_test.go @@ -7,18 +7,16 @@ import ( ) func TestNamespaceCreation(t *testing.T) { - t.Log("Integration Test: Verifying CLONE_NEWUSER and CLONE_NEWNET syscalls") - // TODO: Verify that unshare creates a new network namespace - // TODO: Verify that the process has root privileges inside the namespace + // Test that CLONE_NEWUSER and CLONE_NEWNET are called in the correct sequence and a netns is created. + t.Skip("not implemented") } func TestNamespacePinning(t *testing.T) { - t.Log("Integration Test: Verifying bind-mount of namespace to /run/user/$UID/wg-wrap/") - // TODO: Verify that the namespace survives after the process exits - // TODO: Verify that we can re-join the namespace via setns + // Test that the network namespace is bind-mounted to /run/user/$UID/wg-wrap/ and persists after process exit. + t.Skip("not implemented") } func TestRoutingSetup(t *testing.T) { - t.Log("Integration Test: Verifying TUN device creation and IP routing table setup") - // TODO: Mock 'ip' command or use netlink to verify route exists + // Test that the TUN device is created and the routing table is configured with the correct VPN subnet. + t.Skip("not implemented") } diff --git a/internal/wireguard/wireguard_test.go b/internal/wireguard/wireguard_test.go index 05e0fb7..9bbd24c 100644 --- a/internal/wireguard/wireguard_test.go +++ b/internal/wireguard/wireguard_test.go @@ -7,11 +7,11 @@ import ( ) func TestWireGuardDeviceBinding(t *testing.T) { - t.Log("Integration Test: Verifying binding of userspace WG device to TUN device") - // TODO: Initialize a wg-go device and link it to a mock TUN + // Test that the userspace WireGuard device is correctly bound to the Linux TUN device. + t.Skip("not implemented") } func TestIpcSetConfiguration(t *testing.T) { - t.Log("Integration Test: Verifying IpcSet applies keys and endpoints correctly") - // TODO: Verify that configuration updates are reflected in the device state + // Test that IpcSet correctly updates the WireGuard device keys and endpoints. + t.Skip("not implemented") } diff --git a/pkg/wgconf/wgconf_test.go b/pkg/wgconf/wgconf_test.go index ccd3960..d0bcb0b 100644 --- a/pkg/wgconf/wgconf_test.go +++ b/pkg/wgconf/wgconf_test.go @@ -5,11 +5,11 @@ import ( ) func TestParseConfig(t *testing.T) { - t.Log("Unit Test: Verifying WireGuard .conf parsing logic") - // TODO: Implement test cases for valid/invalid configs, MTU, and DNS + // Test that valid .conf files are parsed correctly and invalid ones return errors. + t.Skip("not implemented") } func TestValidateProfile(t *testing.T) { - t.Log("Unit Test: Verifying profile validation and path resolution") - // TODO: Implement test cases for ~/.config/wg-wrap/profiles/ resolution + // Test that profile names are resolved correctly to ~/.config/wg-wrap/profiles/*.conf. + t.Skip("not implemented") } diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index 5966659..888aeb6 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -5,27 +5,21 @@ import ( ) func TestDataPlaneConnectivity(t *testing.T) { - t.Log("E2E Test: Virtual Peer connectivity check") - // 1. Spin up a Virtual Peer (GVisor-based userspace stack) - // 2. Generate a matching .conf profile - // 3. Run `wg-wrap --profile test curl <virtual-peer-ip>` - // 4. Verify HTTP response is received + // Test full data path: start virtual peer -> run wg-wrap -> curl peer internal IP -> verify HTTP 200. + t.Skip("not implemented") } func TestNetworkIsolation(t *testing.T) { - t.Log("E2E Test: Verifying host isolation") - // 1. Ensure host cannot ping the Virtual Peer's internal IP - // 2. Ensure wrapped process CAN ping the Virtual Peer's internal IP + // Test that host cannot reach peer internal IP, but wrapped process can. + t.Skip("not implemented") } func TestDNSLeakage(t *testing.T) { - t.Log("E2E Test: Verifying DNS is routed via VPN") - // 1. Run `wg-wrap --profile test dig <domain>` - // 2. Verify that the DNS query goes to the VPN DNS server, not host resolver + // Test that DNS queries are routed through the VPN and not the host's resolver. + t.Skip("not implemented") } func TestMTUFragmentation(t *testing.T) { - t.Log("E2E Test: Verifying MTU 1420 prevents packet drop") - // 1. Send large pings (-s 1400) through the tunnel - // 2. Verify packets are received without fragmentation errors + // Test that packets of size ~1400 are transmitted without fragmentation errors. + t.Skip("not implemented") } |
