diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7605ebd..2818d3c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ on: branches: - main env: - VERSION_NUMBER: 'v0.7.1' + VERSION_NUMBER: 'v0.7.2' REGISTRY_NAME: digitalghostdev/poke-cli jobs: diff --git a/README.md b/README.md index e726bb6..f2e9f24 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ pokemon-logo

Pokémon CLI

version-label - docker-image-size + docker-image-size @@ -40,7 +40,7 @@ _Taskfile can build the executable for you_ _Use a Docker Image_ ```bash -docker run --rm -it digitalghostdev/poke-cli:v0.7.1 [command] [subcommand] [flag] +docker run --rm -it digitalghostdev/poke-cli:v0.7.2 [command] [subcommand] [flag] ``` ### Go Build diff --git a/cli_test.go b/cli_test.go index 22241bb..e797a2e 100644 --- a/cli_test.go +++ b/cli_test.go @@ -2,67 +2,63 @@ package main import ( "bytes" - "fmt" - "io" "os" "regexp" "testing" ) -// Strip ANSI color codes var ansiRegex = regexp.MustCompile(`\x1b\[[0-9;]*m`) -func stripANSI(input string) string { - return ansiRegex.ReplaceAllString(input, "") -} +func captureOutput(f func()) string { + var buf bytes.Buffer + stdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w -func TestMainFunction(t *testing.T) { - version := "v0.7.0" + f() - // Backup the original exit function and stdout/stderr - originalExit := exit - originalStdout := os.Stdout - originalStderr := os.Stderr - defer func() { - exit = originalExit // Restore exit - os.Stdout = originalStdout // Restore stdout - os.Stderr = originalStderr // Restore stderr - }() + _ = w.Close() + os.Stdout = stdout + _, _ = buf.ReadFrom(r) - // Replace exit with a function that captures the exit code - exitCode := 0 - exit = func(code int) { exitCode = code } + return buf.String() +} +func stripANSI(input string) string { + return ansiRegex.ReplaceAllString(input, "") +} + +func TestRunCLI(t *testing.T) { tests := []struct { + name string args []string expectedOutput string - expectedExit int + expectedCode int }{ { - args: []string{"pokemons"}, + name: "No Arguments", + args: []string{}, expectedOutput: "╭──────────────────────────────────────────────────────╮\n" + - "│Error! │\n" + - "│ 'pokemons' is not a valid command. │\n" + + "│Welcome! This tool displays data related to Pokémon! │\n" + "│ │\n" + - "│Available Commands: │\n" + + "│ USAGE: │\n" + + "│ poke-cli [flag] │\n" + + "│ poke-cli [flag] │\n" + + "│ poke-cli [flag] │\n" + + "│ │\n" + + "│ FLAGS: │\n" + + "│ -h, --help Shows the help menu │\n" + + "│ -l, --latest Prints the latest available │\n" + + "│ version of the program │\n" + + "│ │\n" + + "│ AVAILABLE COMMANDS: │\n" + "│ pokemon Get details of a specific Pokémon │\n" + "│ types Get details of a specific typing │\n" + - "│ │\n" + - "│Also run [poke-cli -h] for more info! │\n" + "╰──────────────────────────────────────────────────────╯\n", - expectedExit: 1, - }, - { - args: []string{"-l"}, - expectedOutput: fmt.Sprintf("Latest Docker image version: %s\nLatest release tag: %s\n", version, version), - expectedExit: 0, - }, - { - args: []string{"--latest"}, - expectedOutput: fmt.Sprintf("Latest Docker image version: %s\nLatest release tag: %s\n", version, version), - expectedExit: 0, + expectedCode: 0, }, { + name: "Help Flag Short", args: []string{"-h"}, expectedOutput: "╭──────────────────────────────────────────────────────╮\n" + "│Welcome! This tool displays data related to Pokémon! │\n" + @@ -81,64 +77,59 @@ func TestMainFunction(t *testing.T) { "│ pokemon Get details of a specific Pokémon │\n" + "│ types Get details of a specific typing │\n" + "╰──────────────────────────────────────────────────────╯\n", - expectedExit: 0, - }, - { - args: []string{"pokemon", "kingambit"}, - expectedOutput: "Your selected Pokémon: Kingambit\nNational Pokédex #: 983\n", - expectedExit: 0, + expectedCode: 0, }, { - args: []string{"pokemon", "cradily", "--types"}, - expectedOutput: "Your selected Pokémon: Cradily\nNational Pokédex #: 346\n──────\nTyping\nType 1: rock\nType 2: grass\n", - expectedExit: 0, + name: "Help Flag Long", + args: []string{"--help"}, + expectedOutput: "╭──────────────────────────────────────────────────────╮\n" + + "│Welcome! This tool displays data related to Pokémon! │\n" + + "│ │\n" + + "│ USAGE: │\n" + + "│ poke-cli [flag] │\n" + + "│ poke-cli [flag] │\n" + + "│ poke-cli [flag] │\n" + + "│ │\n" + + "│ FLAGS: │\n" + + "│ -h, --help Shows the help menu │\n" + + "│ -l, --latest Prints the latest available │\n" + + "│ version of the program │\n" + + "│ │\n" + + "│ AVAILABLE COMMANDS: │\n" + + "│ pokemon Get details of a specific Pokémon │\n" + + "│ types Get details of a specific typing │\n" + + "╰──────────────────────────────────────────────────────╯\n", + expectedCode: 0, }, { - args: []string{"pokemon", "giratina-altered", "--abilities"}, - expectedOutput: "Your selected Pokémon: Giratina-Altered\nNational Pokédex #: 487\n─────────\nAbilities\nAbility 1: pressure\nHidden Ability: telepathy\n", - expectedExit: 0, + name: "Invalid Command", + args: []string{"invalid"}, + expectedOutput: "Error!", + expectedCode: 1, }, { - args: []string{"pokemon", "coPPeraJAH", "-t", "-a"}, - expectedOutput: "Your selected Pokémon: Copperajah\nNational Pokédex #: 879\n──────\nTyping\nType 1: steel\n─────────\nAbilities\nAbility 1: sheer-force\nHidden Ability: heavy-metal\n", - expectedExit: 0, + name: "Latest Flag", + args: []string{"-l"}, + expectedOutput: "Latest Docker image version: v0.7.1\nLatest release tag: v0.7.1\n", + expectedCode: 0, }, } - for _, test := range tests { - // Create a pipe to capture output - r, w, _ := os.Pipe() - os.Stdout = w - os.Stderr = w - - // Set os.Args for the test case - os.Args = append([]string{"poke-cli"}, test.args...) - - // Run the main function - main() - - // Close the writer and restore stdout and stderr - err := w.Close() - if err != nil { - t.Fatalf("Error closing pipe writer: %v", err) - } - os.Stdout = originalStdout - os.Stderr = originalStderr - - // Read from the pipe - var buf bytes.Buffer - if _, err := io.Copy(&buf, r); err != nil { - t.Errorf("Error copying output: %v", err) - } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + exit = func(code int) {} + output := captureOutput(func() { + code := runCLI(tt.args) + if code != tt.expectedCode { + t.Errorf("Expected exit code %d, got %d", tt.expectedCode, code) + } + }) - // Strip ANSI color codes from the actual output - actualOutput := stripANSI(buf.String()) - if actualOutput != test.expectedOutput { - t.Errorf("Args: %v\nExpected output: %q\nGot: %q\n", test.args, test.expectedOutput, actualOutput) - } + output = stripANSI(output) - if exitCode != test.expectedExit { - t.Errorf("Args: %v\nExpected exit code: %d\nGot: %d\n", test.args, test.expectedExit, exitCode) - } + if !bytes.Contains([]byte(output), []byte(tt.expectedOutput)) { + t.Errorf("Expected output to contain %q, got %q", tt.expectedOutput, output) + } + }) } } diff --git a/cmd/pokemon_test.go b/cmd/pokemon_test.go index ca823dc..7eb5140 100644 --- a/cmd/pokemon_test.go +++ b/cmd/pokemon_test.go @@ -1,7 +1,9 @@ package cmd import ( + "bytes" "github.com/stretchr/testify/assert" + "os" "testing" ) @@ -49,3 +51,38 @@ func TestValidatePokemonArgs_TooManyArgs(t *testing.T) { assert.NotEqual(t, expectedError, err.Error()) } } + +func TestPokemonCommand(t *testing.T) { + // Capture standard output + var output bytes.Buffer + stdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + // Set up test arguments (focus only on Pokémon name and command) + os.Args = []string{"poke-cli", "pokemon", "bulbasaur"} + + // Call the function + PokemonCommand() + + // Close and restore stdout + if closeErr := w.Close(); closeErr != nil { + t.Fatalf("Failed to close pipe writer: %v", closeErr) + } + os.Stdout = stdout + + _, readErr := output.ReadFrom(r) + if readErr != nil { + t.Fatalf("Failed to read from pipe: %v", readErr) + } + + // Define expected output based on actual API response + expectedName := "Bulbasaur" + expectedID := "1" + + // Assert output contains expected Pokémon details + assert.Contains(t, output.String(), "Your selected Pokémon:", "Output should contain Pokémon details header") + assert.Contains(t, output.String(), expectedName, "Output should contain the Pokémon name") + assert.Contains(t, output.String(), "National Pokédex #:", "Output should contain Pokémon ID header") + assert.Contains(t, output.String(), expectedID, "Output should contain the Pokémon ID") +} diff --git a/cmd/styles.go b/cmd/styles.go index 3f5dffb..9435520 100644 --- a/cmd/styles.go +++ b/cmd/styles.go @@ -38,3 +38,10 @@ var ( "fairy": "#EE99EE", } ) + +// Helper function to get color for a given type name from colorMap +func getTypeColor(typeName string) string { + color := colorMap[typeName] + + return color +} diff --git a/cmd/styles_test.go b/cmd/styles_test.go new file mode 100644 index 0000000..c959eab --- /dev/null +++ b/cmd/styles_test.go @@ -0,0 +1,22 @@ +package cmd + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestGetTypeColor(t *testing.T) { + // Test known types + for typeName, expectedColor := range colorMap { + t.Run(typeName, func(t *testing.T) { + color := getTypeColor(typeName) + assert.Equal(t, expectedColor, color, "Expected color for type %s to be %s", typeName, expectedColor) + }) + } + + // Test unknown type + t.Run("unknown type", func(t *testing.T) { + color := getTypeColor("unknown") + assert.Equal(t, "", color, "Expected color for unknown type to be an empty string") + }) +} diff --git a/cmd/types.go b/cmd/types.go index b004e79..dd28069 100644 --- a/cmd/types.go +++ b/cmd/types.go @@ -51,13 +51,6 @@ func (m model) View() string { return "Select a type! Hit 'Q' or 'CTRL-C' to quit.\n" + typesTableBorder.Render(m.table.View()) + "\n" } -// Helper function to get color for a given type name from colorMap -func getTypeColor(typeName string) string { - color := colorMap[typeName] - - return color -} - // Function to display type details after a type is selected func displayTypeDetails(typesName string, endpoint string) { diff --git a/cmd/types_test.go b/cmd/types_test.go index dc2baca..7ec34a1 100644 --- a/cmd/types_test.go +++ b/cmd/types_test.go @@ -29,3 +29,27 @@ func TestValidateTypesArgs_TooManyArgs(t *testing.T) { assert.NotEqual(t, expectedError, err.Error()) } } + +func TestModelInit(t *testing.T) { + m := model{} + result := m.Init() + + assert.Nil(t, result, "Expected Init() to return nil") +} + +func TestModelView_SelectedOption(t *testing.T) { + m := model{selectedOption: "someOption"} + + output := m.View() + + assert.Equal(t, "", output, "Expected output to be an empty string when selectedOption is set") +} + +func TestModelView_DisplayTable(t *testing.T) { + m := model{selectedOption: ""} + expectedOutput := "Select a type! Hit 'Q' or 'CTRL-C' to quit.\n" + typesTableBorder.Render(m.table.View()) + "\n" + + output := m.View() + + assert.Equal(t, expectedOutput, output, "Expected View output to include table view") +} diff --git a/connections/connection.go b/connections/connection.go index d61126e..fab11e8 100644 --- a/connections/connection.go +++ b/connections/connection.go @@ -68,13 +68,12 @@ type TypesJSONStruct struct { } `json:"damage_relations"` } -var httpGet = http.Get var red = lipgloss.Color("#F2055C") var errorColor = lipgloss.NewStyle().Foreground(red) // ApiCallSetup Helper function to handle API calls and JSON unmarshalling func ApiCallSetup(url string, target interface{}) error { - res, err := httpGet(url) + res, err := http.Get(url) if err != nil { return fmt.Errorf("error making GET request: %w", err) } diff --git a/flags/pokemonflagset_test.go b/flags/pokemonflagset_test.go index 685ab23..aaa1569 100644 --- a/flags/pokemonflagset_test.go +++ b/flags/pokemonflagset_test.go @@ -1,7 +1,9 @@ package flags import ( + "bytes" "github.com/stretchr/testify/assert" + "os" "testing" ) @@ -30,3 +32,76 @@ func TestSetupPokemonFlagSet(t *testing.T) { assert.NotNil(t, shortAbilitiesFlag, "Short abilities flag should not be nil") assert.Equal(t, false, *shortAbilitiesFlag, "Short abilities flag name should be 'a'") } + +func TestAbilitiesFlag(t *testing.T) { + // Capture standard output + var output bytes.Buffer + stdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + // Call the function with a known Pokémon (e.g., bulbasaur) + err := AbilitiesFlag("pokemon", "bulbasaur") + + // Close and restore stdout + if closeErr := w.Close(); closeErr != nil { + t.Fatalf("Failed to close pipe writer: %v", closeErr) + } + os.Stdout = stdout + + _, readErr := output.ReadFrom(r) + if readErr != nil { + t.Fatalf("Failed to read from pipe: %v", readErr) + } + + // Assert no errors occurred during execution + assert.NoError(t, err) + + // Define the expected output based on the API response + expectedOutput := `───────── +Abilities +Ability 1: overgrow +Hidden Ability: chlorophyll +` + + // Assert the actual output matches the expected output + assert.Contains(t, output.String(), "Abilities", "Output should contain 'Abilities'") + assert.Contains(t, output.String(), "Ability 1: overgrow", "Output should contain 'Ability 1: overgrow'") + assert.Contains(t, output.String(), "Hidden Ability: chlorophyll", "Output should contain 'Ability 2: chlorophyll'") + assert.Equal(t, expectedOutput, output.String(), "Output does not match the expected formatting") +} + +func TestTypesFlag(t *testing.T) { + // Capture standard output + var output bytes.Buffer + stdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + // Call the TypesFlag function with a valid Pokémon + err := TypesFlag("pokemon", "bulbasaur") + + // Close and restore stdout + if closeErr := w.Close(); closeErr != nil { + t.Fatalf("Failed to close pipe writer: %v", closeErr) + } + os.Stdout = stdout + + _, readErr := output.ReadFrom(r) + if readErr != nil { + t.Fatalf("Failed to read from pipe: %v", readErr) + } + + // Assert no errors occurred + assert.NoError(t, err, "TypesFlag should not return an error for a valid Pokémon") + + // Define expected output components + expectedHeader := "Typing" + expectedType1 := "Type 1: grass" + expectedType2 := "Type 2: poison" + + // Assert output contains the expected header and typing information + assert.Contains(t, output.String(), expectedHeader, "Output should contain the 'Typing' header") + assert.Contains(t, output.String(), expectedType1, "Output should contain the Pokémon's first type") + assert.Contains(t, output.String(), expectedType2, "Output should contain the Pokémon's second type") +} diff --git a/flags/version.go b/flags/version.go index 5f4ebe0..4d20342 100644 --- a/flags/version.go +++ b/flags/version.go @@ -8,14 +8,12 @@ import ( "os/exec" ) -func latestDockerImage() { - fullCommand := `curl -s https://hub.docker.com/v2/repositories/digitalghostdev/poke-cli/tags/?page_size=1 | grep -o '"name":"[^"]*"' | cut -d '"' -f 4` - +func latestDockerImage(fullCommand string) { cmd := exec.Command("bash", "-c", fullCommand) - output, err := cmd.Output() if err != nil { - fmt.Printf("error running 'command': %v\n", err) + fmt.Printf("error running command: %v\n", err) + return } fmt.Print("Latest Docker image version: ", string(output)) @@ -55,6 +53,6 @@ func latestRelease(githubAPIURL string) { func LatestFlag() { // cmd := exec.Command("git", "describe", "--tags", "--abbrev=0") - latestDockerImage() + latestDockerImage(`curl -s https://hub.docker.com/v2/repositories/digitalghostdev/poke-cli/tags/?page_size=1 | grep -o '"name":"[^"]*"' | cut -d '"' -f 4`) latestRelease("https://api.github.com/repos/digitalghost-dev/poke-cli/releases/latest") } diff --git a/flags/version_test.go b/flags/version_test.go index fa06209..c1bb59b 100644 --- a/flags/version_test.go +++ b/flags/version_test.go @@ -22,7 +22,10 @@ func captureOutput(f func()) string { f() // Restore the original stdout and close the writer - w.Close() + err := w.Close() + if err != nil { + return fmt.Sprintf("error closing writer: %v", err) + } os.Stdout = oldStdout // Read the captured output @@ -32,10 +35,15 @@ func captureOutput(f func()) string { } func TestLatestDockerImage(t *testing.T) { - output := captureOutput(latestDockerImage) - - // Modify this assertion as needed based on expected output + // Test normal execution with a valid command + validCommand := `curl -s https://hub.docker.com/v2/repositories/digitalghostdev/poke-cli/tags/?page_size=1 | grep -o '"name":"[^"]*"' | cut -d '"' -f 4` + output := captureOutput(func() { latestDockerImage(validCommand) }) assert.Contains(t, output, "Latest Docker image version:") + + // Test command failure with an invalid command + invalidCommand := "invalidcommand" + output = captureOutput(func() { latestDockerImage(invalidCommand) }) + assert.Contains(t, output, "error running command:") } func TestLatestRelease(t *testing.T) { @@ -48,7 +56,11 @@ func TestLatestRelease(t *testing.T) { func TestLatestRelease_Success(t *testing.T) { // Create a mock server that simulates a successful response handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, `{"tag_name": "v1.0.0"}`) + _, err := fmt.Fprintln(w, `{"tag_name": "v1.0.0"}`) + if err != nil { + t.Errorf("failed to write response: %v", err) + return + } }) server := httptest.NewServer(handler) defer server.Close() @@ -61,7 +73,11 @@ func TestLatestRelease_Success(t *testing.T) { func TestLatestRelease_InvalidJSON(t *testing.T) { // Create a mock server that returns invalid JSON handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, `invalid-json`) + _, err := fmt.Fprintln(w, `invalid-json`) + if err != nil { + t.Errorf("failed to write response: %v", err) + return + } }) server := httptest.NewServer(handler) defer server.Close()