package main import ( "bytes" "context" "os" "os/exec" "path/filepath" "runtime" "strings" "testing" "github.com/ungluedlabs/go-jdenticon/jdenticon" ) // testBinaryName returns the correct test binary name for the current OS. // On Windows, executables need the .exe extension. func testBinaryName() string { if runtime.GOOS == "windows" { return "jdenticon-test.exe" } return "jdenticon-test" } // TestCLIVsLibraryOutputIdentical verifies that CLI generates identical output to the library API func TestCLIVsLibraryOutputIdentical(t *testing.T) { // Build the CLI binary first tempDir, err := os.MkdirTemp("", "jdenticon-integration-test") if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } defer os.RemoveAll(tempDir) cliBinary := filepath.Join(tempDir, testBinaryName()) cmd := exec.Command("go", "build", "-o", cliBinary, ".") if err := cmd.Run(); err != nil { t.Fatalf("Failed to build CLI binary: %v", err) } testCases := []struct { name string input string size int cliArgs []string configFunc func() jdenticon.Config }{ { name: "basic SVG generation", input: "test@example.com", size: 200, cliArgs: []string{"generate", "test@example.com"}, configFunc: func() jdenticon.Config { return jdenticon.DefaultConfig() }, }, { name: "custom size", input: "test@example.com", size: 128, cliArgs: []string{"generate", "--size", "128", "test@example.com"}, configFunc: func() jdenticon.Config { return jdenticon.DefaultConfig() }, }, { name: "custom padding", input: "test@example.com", size: 200, cliArgs: []string{"generate", "--padding", "0.15", "test@example.com"}, configFunc: func() jdenticon.Config { config := jdenticon.DefaultConfig() config.Padding = 0.15 return config }, }, { name: "custom color saturation", input: "test@example.com", size: 200, cliArgs: []string{"generate", "--color-saturation", "0.8", "test@example.com"}, configFunc: func() jdenticon.Config { config := jdenticon.DefaultConfig() config.ColorSaturation = 0.8 return config }, }, { name: "background color", input: "test@example.com", size: 200, cliArgs: []string{"generate", "--bg-color", "#ffffff", "test@example.com"}, configFunc: func() jdenticon.Config { config := jdenticon.DefaultConfig() config.BackgroundColor = "#ffffff" return config }, }, { name: "grayscale saturation", input: "test@example.com", size: 200, cliArgs: []string{"generate", "--grayscale-saturation", "0.1", "test@example.com"}, configFunc: func() jdenticon.Config { config := jdenticon.DefaultConfig() config.GrayscaleSaturation = 0.1 return config }, }, { name: "custom lightness ranges", input: "test@example.com", size: 200, cliArgs: []string{"generate", "--color-lightness", "0.3,0.7", "--grayscale-lightness", "0.2,0.8", "test@example.com"}, configFunc: func() jdenticon.Config { config := jdenticon.DefaultConfig() config.ColorLightnessRange = [2]float64{0.3, 0.7} config.GrayscaleLightnessRange = [2]float64{0.2, 0.8} return config }, }, { name: "hue restrictions", input: "test@example.com", size: 200, cliArgs: []string{"generate", "--hue-restrictions", "0,120,240", "test@example.com"}, configFunc: func() jdenticon.Config { config := jdenticon.DefaultConfig() config.HueRestrictions = []float64{0, 120, 240} return config }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Generate using library API config := tc.configFunc() librarySVG, err := jdenticon.ToSVGWithConfig(context.Background(), tc.input, tc.size, config) if err != nil { t.Fatalf("Library generation failed: %v", err) } // Generate using CLI cmd := exec.Command(cliBinary, tc.cliArgs...) var cliOutput bytes.Buffer cmd.Stdout = &cliOutput cmd.Stderr = &cliOutput if err := cmd.Run(); err != nil { t.Fatalf("CLI command failed: %v, output: %s", err, cliOutput.String()) } cliSVG := cliOutput.String() // Compare outputs if cliSVG != librarySVG { t.Errorf("CLI and library outputs differ") t.Logf("Library output length: %d", len(librarySVG)) t.Logf("CLI output length: %d", len(cliSVG)) // Find the first difference minLen := len(librarySVG) if len(cliSVG) < minLen { minLen = len(cliSVG) } for i := 0; i < minLen; i++ { if librarySVG[i] != cliSVG[i] { start := i - 20 if start < 0 { start = 0 } end := i + 20 if end > minLen { end = minLen } t.Logf("First difference at position %d:", i) t.Logf("Library: %q", librarySVG[start:end]) t.Logf("CLI: %q", cliSVG[start:end]) break } } } }) } } // TestCLIPNGVsLibraryOutputIdentical verifies PNG output consistency func TestCLIPNGVsLibraryOutputIdentical(t *testing.T) { // Build the CLI binary first tempDir, err := os.MkdirTemp("", "jdenticon-png-integration-test") if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } defer os.RemoveAll(tempDir) cliBinary := filepath.Join(tempDir, testBinaryName()) cmd := exec.Command("go", "build", "-o", cliBinary, ".") if err := cmd.Run(); err != nil { t.Fatalf("Failed to build CLI binary: %v", err) } testCases := []struct { name string input string size int cliArgs []string configFunc func() jdenticon.Config }{ { name: "basic PNG generation", input: "test@example.com", size: 64, cliArgs: []string{"generate", "--format", "png", "--size", "64", "test@example.com"}, configFunc: func() jdenticon.Config { return jdenticon.DefaultConfig() }, }, { name: "PNG with background", input: "test@example.com", size: 64, cliArgs: []string{"generate", "--format", "png", "--size", "64", "--bg-color", "#ff0000", "test@example.com"}, configFunc: func() jdenticon.Config { config := jdenticon.DefaultConfig() config.BackgroundColor = "#ff0000" return config }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Generate using library API config := tc.configFunc() libraryPNG, err := jdenticon.ToPNGWithConfig(context.Background(), tc.input, tc.size, config) if err != nil { t.Fatalf("Library PNG generation failed: %v", err) } // Generate using CLI cmd := exec.Command(cliBinary, tc.cliArgs...) var cliOutput bytes.Buffer cmd.Stdout = &cliOutput if err := cmd.Run(); err != nil { t.Fatalf("CLI command failed: %v", err) } cliPNG := cliOutput.Bytes() // Compare PNG outputs - they should be identical if !bytes.Equal(cliPNG, libraryPNG) { t.Errorf("CLI and library PNG outputs differ") t.Logf("Library PNG size: %d bytes", len(libraryPNG)) t.Logf("CLI PNG size: %d bytes", len(cliPNG)) // Check PNG headers if len(libraryPNG) >= 8 && len(cliPNG) >= 8 { if !bytes.Equal(libraryPNG[:8], cliPNG[:8]) { t.Logf("PNG headers differ") t.Logf("Library: %v", libraryPNG[:8]) t.Logf("CLI: %v", cliPNG[:8]) } } } }) } } // TestCLIBatchIntegration tests batch processing consistency func TestCLIBatchIntegration(t *testing.T) { // Build the CLI binary first tempDir, err := os.MkdirTemp("", "jdenticon-batch-integration-test") if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } defer os.RemoveAll(tempDir) cliBinary := filepath.Join(tempDir, testBinaryName()) cmd := exec.Command("go", "build", "-o", cliBinary, ".") if err := cmd.Run(); err != nil { t.Fatalf("Failed to build CLI binary: %v", err) } // Create input file inputFile := filepath.Join(tempDir, "inputs.txt") inputs := []string{ "user1@example.com", "user2@example.com", "test-user", } inputContent := strings.Join(inputs, "\n") if err := os.WriteFile(inputFile, []byte(inputContent), 0644); err != nil { t.Fatalf("Failed to create input file: %v", err) } outputDir := filepath.Join(tempDir, "batch-output") // Run batch command cmd = exec.Command(cliBinary, "batch", inputFile, "--output-dir", outputDir) if err := cmd.Run(); err != nil { t.Fatalf("Batch command failed: %v", err) } // Verify each generated file matches library output config := jdenticon.DefaultConfig() for _, input := range inputs { filename := sanitizeFilename(input) + ".svg" filepath := filepath.Join(outputDir, filename) // Read CLI-generated file cliContent, err := os.ReadFile(filepath) if err != nil { t.Errorf("Failed to read CLI-generated file for %s: %v", input, err) continue } // Generate using library librarySVG, err := jdenticon.ToSVGWithConfig(context.Background(), input, 200, config) if err != nil { t.Errorf("Library generation failed for %s: %v", input, err) continue } // Compare if string(cliContent) != librarySVG { t.Errorf("Batch file for %s differs from library output", input) } } } // TestCLIErrorHandling tests that CLI properly handles error cases func TestCLIErrorHandling(t *testing.T) { // Build the CLI binary first tempDir, err := os.MkdirTemp("", "jdenticon-error-test") if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } defer os.RemoveAll(tempDir) cliBinary := filepath.Join(tempDir, testBinaryName()) cmd := exec.Command("go", "build", "-o", cliBinary, ".") if err := cmd.Run(); err != nil { t.Fatalf("Failed to build CLI binary: %v", err) } errorCases := []struct { name string args []string expectError bool }{ { name: "no arguments", args: []string{}, expectError: false, // Should show help, not error }, { name: "invalid format", args: []string{"generate", "--format", "invalid", "test"}, expectError: true, }, { name: "negative size", args: []string{"generate", "--size", "-1", "test"}, expectError: true, }, { name: "generate no arguments", args: []string{"generate"}, expectError: true, }, { name: "batch missing output dir", args: []string{"batch", "somefile.txt"}, expectError: true, }, { name: "batch missing input file", args: []string{"batch", "nonexistent.txt", "--output-dir", "/tmp"}, expectError: true, }, } for _, tc := range errorCases { t.Run(tc.name, func(t *testing.T) { cmd := exec.Command(cliBinary, tc.args...) var output bytes.Buffer cmd.Stdout = &output cmd.Stderr = &output err := cmd.Run() if tc.expectError && err == nil { t.Errorf("Expected error but command succeeded. Output: %s", output.String()) } else if !tc.expectError && err != nil { t.Errorf("Expected success but command failed: %v. Output: %s", err, output.String()) } }) } }