diff options
| author | Paul Buetow <paul@buetow.org> | 2025-07-20 23:10:50 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-07-20 23:10:50 +0300 |
| commit | 9c12e879c5d6833ce50f5b6d646ccce03a78db31 (patch) | |
| tree | 206906b551d595b35d00586b6cc5bf9e1f3fe7f8 /internal/cli | |
| parent | e580fb57a29ec3c3f3e180b20cfa6ec28687689b (diff) | |
test: add comprehensive test coverage for refactored packages
Add test suites for all newly created packages from the main.go refactoring:
- batch: 100% coverage - file reading, parsing, edge cases
- cli: 96.7% coverage - command setup, flags, configuration
- translation: 92% coverage - API integration, caching, errors
- phonetic: 87.5% coverage - API fetching, file operations
- models: 77.3% coverage - model listing functionality
- processor: 18% coverage - basic tests (limited by API dependencies)
Total: 1159 lines of test code across 7 new test files
🤖 Generated with [opencode](https://opencode.ai)
Co-Authored-By: opencode <noreply@opencode.ai>
Diffstat (limited to 'internal/cli')
| -rw-r--r-- | internal/cli/command_test.go | 260 | ||||
| -rw-r--r-- | internal/cli/flags_test.go | 99 |
2 files changed, 359 insertions, 0 deletions
diff --git a/internal/cli/command_test.go b/internal/cli/command_test.go new file mode 100644 index 0000000..55e60e8 --- /dev/null +++ b/internal/cli/command_test.go @@ -0,0 +1,260 @@ +package cli + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +func TestCreateRootCommand(t *testing.T) { + flags := NewFlags() + cmd := CreateRootCommand(flags) + + // Test basic command properties + if cmd.Use != "totalrecall [word]" { + t.Errorf("Expected Use to be 'totalrecall [word]', got %s", cmd.Use) + } + + if !strings.Contains(cmd.Short, "Bulgarian Anki Flashcard Generator") { + t.Errorf("Expected Short description to contain 'Bulgarian Anki Flashcard Generator'") + } + + // Test that flags are set up + flagTests := []struct { + name string + expected bool + }{ + {"config", true}, + {"output", true}, + {"format", true}, + {"image-api", true}, + {"batch", true}, + {"skip-audio", true}, + {"skip-images", true}, + {"anki", true}, + {"anki-csv", true}, + {"deck-name", true}, + {"list-models", true}, + {"all-voices", true}, + {"gui", true}, + {"openai-model", true}, + {"openai-voice", true}, + {"openai-speed", true}, + {"openai-instruction", true}, + {"openai-image-model", true}, + {"openai-image-size", true}, + {"openai-image-quality", true}, + {"openai-image-style", true}, + } + + for _, tt := range flagTests { + t.Run("flag_"+tt.name, func(t *testing.T) { + var flag *pflag.Flag + if tt.name == "config" { + flag = cmd.PersistentFlags().Lookup(tt.name) + } else { + flag = cmd.Flags().Lookup(tt.name) + } + if flag == nil && tt.expected { + t.Errorf("Expected flag %s to exist", tt.name) + } + }) + } +} + +func TestSetupFlags(t *testing.T) { + cmd := &cobra.Command{} + flags := NewFlags() + + setupFlags(cmd, flags) + + // Test default values + outputFlag := cmd.Flags().Lookup("output") + if outputFlag == nil { + t.Fatal("output flag not found") + } + + home, _ := os.UserHomeDir() + expectedDefault := filepath.Join(home, "Downloads") + if outputFlag.DefValue != expectedDefault { + t.Errorf("Expected default output dir to be %s, got %s", expectedDefault, outputFlag.DefValue) + } + + // Test audio format default + formatFlag := cmd.Flags().Lookup("format") + if formatFlag == nil { + t.Fatal("format flag not found") + } + if formatFlag.DefValue != "mp3" { + t.Errorf("Expected default format to be mp3, got %s", formatFlag.DefValue) + } +} + +func TestInitConfig(t *testing.T) { + // Save original viper state + originalConfig := viper.New() + *originalConfig = *viper.GetViper() + defer func() { + *viper.GetViper() = *originalConfig + }() + + tests := []struct { + name string + cfgFile string + setupFunc func(t *testing.T) string + cleanupFunc func(string) + }{ + { + name: "with config file", + cfgFile: "test-config.yaml", + setupFunc: func(t *testing.T) string { + tmpDir := t.TempDir() + cfgPath := filepath.Join(tmpDir, "test-config.yaml") + content := `audio: + provider: openai + openai_key: test-key +output: + directory: /test/output` + err := os.WriteFile(cfgPath, []byte(content), 0644) + if err != nil { + t.Fatalf("Failed to create test config: %v", err) + } + return cfgPath + }, + cleanupFunc: func(path string) {}, + }, + { + name: "without config file", + cfgFile: "", + setupFunc: func(t *testing.T) string { + return "" + }, + cleanupFunc: func(path string) {}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Reset viper for each test + viper.Reset() + + cfgPath := tt.setupFunc(t) + if tt.cfgFile != "" && cfgPath != "" { + tt.cfgFile = cfgPath + } + + InitConfig(tt.cfgFile) + + // Test environment variable prefix + os.Setenv("TOTALRECALL_TEST_VAR", "test-value") + defer os.Unsetenv("TOTALRECALL_TEST_VAR") + + if viper.GetString("test_var") != "test-value" { + t.Error("Environment variable not properly loaded") + } + + tt.cleanupFunc(cfgPath) + }) + } +} + +func TestGetOpenAIKey(t *testing.T) { + // Save original viper state + originalConfig := viper.New() + *originalConfig = *viper.GetViper() + defer func() { + *viper.GetViper() = *originalConfig + }() + + tests := []struct { + name string + envKey string + configKey string + expected string + }{ + { + name: "from environment", + envKey: "env-test-key", + configKey: "config-test-key", + expected: "env-test-key", + }, + { + name: "from config when no env", + envKey: "", + configKey: "config-test-key", + expected: "config-test-key", + }, + { + name: "empty when neither set", + envKey: "", + configKey: "", + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Reset viper + viper.Reset() + + // Set up environment + if tt.envKey != "" { + os.Setenv("OPENAI_API_KEY", tt.envKey) + defer os.Unsetenv("OPENAI_API_KEY") + } else { + os.Unsetenv("OPENAI_API_KEY") + } + + // Set up config + if tt.configKey != "" { + viper.Set("audio.openai_key", tt.configKey) + } + + got := GetOpenAIKey() + if got != tt.expected { + t.Errorf("GetOpenAIKey() = %v, want %v", got, tt.expected) + } + }) + } +} + +func TestBindFlagsToViper(t *testing.T) { + // Save original viper state + originalConfig := viper.New() + *originalConfig = *viper.GetViper() + defer func() { + *viper.GetViper() = *originalConfig + }() + + // Reset viper + viper.Reset() + + cmd := &cobra.Command{} + flags := NewFlags() + setupFlags(cmd, flags) + + // Set some flag values + cmd.Flags().Set("output", "/test/output") + cmd.Flags().Set("format", "wav") + cmd.Flags().Set("openai-model", "tts-1-hd") + + bindFlagsToViper(cmd) + + // Test that values are bound + if viper.GetString("output.directory") != "/test/output" { + t.Errorf("Expected output.directory to be /test/output, got %s", viper.GetString("output.directory")) + } + + if viper.GetString("audio.format") != "wav" { + t.Errorf("Expected audio.format to be wav, got %s", viper.GetString("audio.format")) + } + + if viper.GetString("audio.openai_model") != "tts-1-hd" { + t.Errorf("Expected audio.openai_model to be tts-1-hd, got %s", viper.GetString("audio.openai_model")) + } +} diff --git a/internal/cli/flags_test.go b/internal/cli/flags_test.go new file mode 100644 index 0000000..d308bb7 --- /dev/null +++ b/internal/cli/flags_test.go @@ -0,0 +1,99 @@ +package cli + +import ( + "reflect" + "testing" +) + +func TestNewFlags(t *testing.T) { + flags := NewFlags() + + // Test default values + tests := []struct { + name string + got interface{} + expected interface{} + }{ + {"AudioFormat", flags.AudioFormat, "mp3"}, + {"ImageAPI", flags.ImageAPI, "openai"}, + {"DeckName", flags.DeckName, "Bulgarian Vocabulary"}, + {"OpenAIModel", flags.OpenAIModel, "gpt-4o-mini-tts"}, + {"OpenAISpeed", flags.OpenAISpeed, 0.9}, + {"OpenAIImageModel", flags.OpenAIImageModel, "dall-e-3"}, + {"OpenAIImageSize", flags.OpenAIImageSize, "1024x1024"}, + {"OpenAIImageQuality", flags.OpenAIImageQuality, "standard"}, + {"OpenAIImageStyle", flags.OpenAIImageStyle, "natural"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if !reflect.DeepEqual(tt.got, tt.expected) { + t.Errorf("%s = %v, want %v", tt.name, tt.got, tt.expected) + } + }) + } + + // Test boolean defaults (should be false) + boolTests := []struct { + name string + value bool + }{ + {"SkipAudio", flags.SkipAudio}, + {"SkipImages", flags.SkipImages}, + {"GenerateAnki", flags.GenerateAnki}, + {"AnkiCSV", flags.AnkiCSV}, + {"ListModels", flags.ListModels}, + {"AllVoices", flags.AllVoices}, + {"GUIMode", flags.GUIMode}, + } + + for _, tt := range boolTests { + t.Run(tt.name, func(t *testing.T) { + if tt.value != false { + t.Errorf("%s = %v, want false", tt.name, tt.value) + } + }) + } + + // Test string defaults (should be empty) + stringTests := []struct { + name string + value string + }{ + {"CfgFile", flags.CfgFile}, + {"OutputDir", flags.OutputDir}, + {"BatchFile", flags.BatchFile}, + {"OpenAIVoice", flags.OpenAIVoice}, + {"OpenAIInstruction", flags.OpenAIInstruction}, + } + + for _, tt := range stringTests { + t.Run(tt.name, func(t *testing.T) { + if tt.value != "" { + t.Errorf("%s = %v, want empty string", tt.name, tt.value) + } + }) + } +} + +func TestFlagsStructure(t *testing.T) { + // Test that Flags struct has all expected fields + flags := &Flags{} + flagsType := reflect.TypeOf(*flags) + + expectedFields := []string{ + "CfgFile", "OutputDir", "AudioFormat", "ImageAPI", "BatchFile", + "SkipAudio", "SkipImages", "GenerateAnki", "AnkiCSV", "DeckName", + "ListModels", "AllVoices", "GUIMode", + "OpenAIModel", "OpenAIVoice", "OpenAISpeed", "OpenAIInstruction", + "OpenAIImageModel", "OpenAIImageSize", "OpenAIImageQuality", "OpenAIImageStyle", + } + + for _, fieldName := range expectedFields { + t.Run("has_field_"+fieldName, func(t *testing.T) { + if _, ok := flagsType.FieldByName(fieldName); !ok { + t.Errorf("Flags struct missing field: %s", fieldName) + } + }) + } +} |
