Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
73 changes: 73 additions & 0 deletions internal/cli/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package cli

import (
"fmt"

"github.com/spf13/cobra"

"github.com/kernelshard/expose/internal/config"
)

// newConfigCmd creates the 'config' command
func newConfigCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "config",
Short: "Manage expose configuration",
Long: "View and manage expose configuration values",
}

//
cmd.AddCommand(newConfigListCmd())
cmd.AddCommand(newConfigGetCmd())

return cmd
}

// newConfigListCmd creates the 'config list' command
func newConfigListCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "List all configuration values",
RunE: runConfigList,
}
return cmd
}

// newConfigGetCmd creates the 'config get' command
// e.g. expose config get <key>
func newConfigGetCmd() *cobra.Command {
return &cobra.Command{
Use: "get <key>",
Short: "Get a specific configuration value",
Args: cobra.ExactArgs(1),
RunE: runConfigGet,
}
}

// runConfigList handles the 'config list' command
func runConfigList(_ *cobra.Command, args []string) error {
cfg, err := config.Load("")
if err != nil {
return fmt.Errorf("config not found (run 'expose init' first): %w", err)
}
values := cfg.List()
for key, value := range values {
fmt.Printf("%s: %v\n", key, value)
}
return nil
}

// runConfigGet handles the 'config get <key>' command
func runConfigGet(_ *cobra.Command, args []string) error {
key := args[0]
cfg, err := config.Load("")
if err != nil {
return fmt.Errorf("config not found (run 'expose init' first): %w", err)
}
val, err := cfg.Get(key)
if err != nil {
return err
}
fmt.Println(val)
return nil
}
1 change: 1 addition & 0 deletions internal/cli/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package cli
1 change: 1 addition & 0 deletions internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ func Execute() error {
// Add commands
rootCmd.AddCommand(newInitCmd())
rootCmd.AddCommand(newTunnelCmd())
rootCmd.AddCommand(newConfigCmd())

return rootCmd.Execute()
}
2 changes: 1 addition & 1 deletion internal/cli/tunnel.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func newTunnelCmd() *cobra.Command {
}

// runTunnelCmd represents the 'tunnel' command in the CLI application.
func runTunnelCmd(cmd *cobra.Command, args []string) error {
func runTunnelCmd(cmd *cobra.Command, _ []string) error {

// Load config
cfg, err := config.Load("")
Expand Down
20 changes: 20 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,23 @@ func Init() (*Config, error) {
return cfg, nil

}

// List returns all configuration values as a map
func (c *Config) List() map[string]interface{} {
return map[string]interface{}{
"project": c.Project,
"port": c.Port,
}
}

// Get returns the value of a specific configuration key
func (c *Config) Get(key string) (interface{}, error) {
switch key {
case "project":
return c.Project, nil
case "port":
return c.Port, nil
default:
return nil, fmt.Errorf("unknown config key: %s", key)
}
}
197 changes: 197 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
package config

import (
"os"
"path/filepath"
"testing"
)

// TestLoad tests the Load function of the config package
func TestLoad(t *testing.T) {

t.Run("valid config file", func(t *testing.T) {
// create temporary config file
content := []byte("project: test-project\nport: 8080\n")

tempFile, err := os.CreateTemp("", "test-config-*.yml")
if err != nil {
t.Fatal(err)
}

defer os.Remove(tempFile.Name()) // clean up

if _, err := tempFile.Write(content); err != nil {
t.Fatal(err)
}
// close the file so it can be read, it's opened in write mode
_ = tempFile.Close()

// Test Load
cfg, err := Load(tempFile.Name())
if err != nil {
t.Fatalf("Load failed: %v", err)
}

if cfg.Project != "test-project" {
t.Errorf("Expected project 'test-project', got '%s'", cfg.Project)
}
if cfg.Port != 8080 {
t.Errorf("Expected port 8080, got %d", cfg.Port)
}

})

t.Run("load with blank path uses default", func(t *testing.T) {
tmpDir := t.TempDir()

filePath := filepath.Join(tmpDir, DefaultConfigFile)

content := []byte("project: default-project\nport: 3000\n")
tempFile, err := os.Create(filePath)
if err != nil {
t.Fatal(err)
}

// wrapping in closure to handle error
defer func(name string) {
err := os.Remove(name)
if err != nil {
t.Error(err)
}
}(tempFile.Name()) // clean up

if _, err := tempFile.Write(content); err != nil {
t.Fatal(err)
}
_ = tempFile.Close()
// change the working dir to the temp dir so Load uses the default path
err = os.Chdir(tmpDir)
if err != nil {
t.Error(err)
}

cfg, err := Load("")
if err != nil {
t.Errorf("Expected no error loading default config, got %v", err)
}

if cfg.Project != "default-project" {
t.Errorf("Expected project 'default-project', got '%s'", cfg.Project)
}

if cfg.Port != 3000 {
t.Errorf("Expected port 3000, got %d", cfg.Port)
}

})

t.Run("non existent file returns error", func(t *testing.T) {
_, err := Load("nonexistent-config.yml")
if err == nil {
t.Errorf("Expected error for missing config file, got nil")
}
})

}

// TestConfigInit tests the Init function of the config package
func TestConfigInit(t *testing.T) {
t.Run("error returned when config exists", func(t *testing.T) {
// Create a temp dir and a config file in it
tmpDir := t.TempDir()
tmpFilePath := filepath.Join(tmpDir, DefaultConfigFile)

tmpFile, err := os.Create(tmpFilePath)
if err != nil {
t.Fatal(err)
}

defer os.Remove(tmpFile.Name())

_ = tmpFile.Close()
_ = os.Chdir(tmpDir)

_, err = Init()
if err == nil {
t.Errorf("expected error that config already exists, got %v", err)
}

})
// TODO: test init might need to change code as it's not testable
t.Run("error returned when file is not yml formatted", func(t *testing.T) {

})

t.Run("creates config with default values", func(t *testing.T) {
tmpDir := t.TempDir()
_ = os.Chdir(tmpDir)

cfg, err := Init()
if err != nil {
t.Fatalf("Init() failed: %v", err)
}

if cfg.Port != 3000 {
t.Errorf("expected port 3000, got %d", cfg.Port)
}

// Verify file created
if _, err := os.Stat(DefaultConfigFile); os.IsNotExist(err) {
t.Error("config file not created")
}
})
}

// TestConfig_List tests the List method of the Config struct
func TestConfig_List(t *testing.T) {
cfg := &Config{
Project: "testProject",
Port: 3000,
}
values := cfg.List()

if values["project"] != "testProject" {
t.Errorf("expected project=testProject, got %v", values["project"])

}

if values["port"] != 3000 {
t.Errorf("expected port=3000, got %v", values["port"])
}

}

func TestGet(t *testing.T) {
cfg := &Config{
Project: "my_project",
Port: 3000,
}

tests := []struct {
key string
expected interface{}
wantErr bool
}{
{"project", "my_project", false},
{"port", 3000, false},
{"invalid", nil, true},
}

for _, tt := range tests {
t.Run(tt.key, func(t *testing.T) {
got, err := cfg.Get(tt.key)

if tt.wantErr && err == nil {
t.Error("expected error, got nil")
}

if !tt.wantErr && err != nil {
t.Errorf("unexpected error: %v", err)
}

if got != tt.expected {
t.Errorf("expected %v, got %v", tt.expected, got)
}
})
}
}