feat: add comprehensive test suite and GoReleaser release automation
- Add GoReleaser configuration (`.goreleaser.yaml`)
- Build for Linux, Windows, macOS (arm64, amd64, 386)
- Enable UPX compression for Linux and Darwin binaries
- Use version 2 syntax with custom archive naming
- Add Makefile with test targets
- Run all tests via `make test-all` or just `make`
- Individual test categories: `test-args`, `test-http`,
`test-conversion`, `test-output`, `test-errors`, `test-build`
- Also supports `build`, `clean`, and `help` targets
- Add integration test suite in `test/` directory
- `test_args.go`: command-line flag parsing, help/version output
- `test_http.go`: HTTP fetching, custom/default User-Agent, timeouts
- `test_conversion.go`: HTML to Markdown conversion (headings, lists,
links, code blocks, blockquotes, etc.)
- `test_output.go`: file output and stdout behavior
- `test_errors.go`: invalid URLs, timeouts, HTTP 4xx/5xx errors
- `test_build.go`: end-to-end tests with compiled binary
- `test_helper.go`: shared utilities (run commands, temp files,
assertions)
- Add `test/README.md` documenting how to run the test suite
- Add `CHANGELOG.md` with [Unreleased] and [1.0.0] sections
- Update `.gitignore`
- Remove platform‑specific binary patterns (`mdget*linux`,
`mdget*x86_64`, etc.)
- Add `dist/` (GoReleaser output directory)
- Update `README.md`
Maxwell Jensen 85795372+maxwelljens@users.noreply.github.com
Mon, 20 Apr 2026 20:35:29 +0200
14 files changed,
1198 insertions(+),
10 deletions(-)
M
.gitignore
→
.gitignore
@@ -1,13 +1,11 @@
# Binaries for programs and plugins mdget -mdget*linux -mdget*x86_64 -mdget*amd64 *.exe *.exe~ *.dll *.so *.dylib +dist # Test binary, built with `go test -c` *.test
A
.goreleaser.yaml
@@ -0,0 +1,68 @@
+version: 2 + +builds: + - binary: mdget + env: + - CGO_ENABLED=0 + ldflags: + - -s -w -X main.buildDate={{.Date}} -X main.version={{.Version}} + goos: + - linux + - windows + - darwin + goarch: + - arm64 + - amd64 + - "386" + dir: cmd + +archives: + - formats: [tar.gz] + # this name template makes the OS and Arch compatible with the results of `uname`. + name_template: >- + {{ .ProjectName }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + # use zip for windows archives + format_overrides: + - goos: windows + formats: [zip] + +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" + +upx: + - # Templates: allowed. + enabled: true + + # Filter by build ID. + ids: [build1, build2] + + # Filter by GOOS. + goos: [linux, darwin] + + # Filter by GOARCH. + goarch: [arm, amd64] + + # Filter by GOARM. + goarm: [8] + + # Filter by GOAMD64. + goamd64: [v1] + + # Compress argument. + # Valid options are from '1' (faster) to '9' (better), and 'best'. + compress: best + + # Whether to try LZMA (slower). + lzma: true + + # Whether to try all methods and filters (slow). + brute: true
A
CHANGELOG.md
@@ -0,0 +1,17 @@
+# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a +Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to +[Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [1.0.0] - 2026-04-20 + +### Added + +- All command-line flags (`-o`, `-t`, `-u`) are now considered stable. +- Added integration tests covering argument parsing, HTTP fetching, +HTML-to-Markdown conversion, file output, and error conditions.
A
Makefile
@@ -0,0 +1,75 @@
+# Makefile for mdget testing suite + +# Default target +.PHONY: all +all: test-all + +# Build mdget binary +.PHONY: build +build: + @echo "Building mdget..." + @go build -o bin/mdget ./cmd/mdget.go + @echo "Built: bin/mdget" + +# Clean build artifacts +.PHONY: clean +clean: + @echo "Cleaning up..." + @rm -rf bin/ test-output/ tmp/ *.test + @echo "Clean complete" + +# Test targets +.PHONY: test-all +test-all: test-args test-http test-output test-errors test-conversion test-build + @echo "✅ All tests passed!" + +.PHONY: test-args +test-args: + @echo "Testing command-line arguments..." + @cd test && go run test_helper.go test_args.go + +.PHONY: test-http +test-http: + @echo "Testing HTTP connectivity..." + @cd test && go run test_helper.go test_http.go + +.PHONY: test-output +test-output: + @echo "Testing file output..." + @cd test && go run test_helper.go test_output.go + +.PHONY: test-errors +test-errors: + @echo "Testing error conditions..." + @cd test && go run test_helper.go test_errors.go + +.PHONY: test-conversion +test-conversion: + @echo "Testing HTML to Markdown conversion..." + @cd test && go run test_helper.go test_conversion.go + +.PHONY: test-build +test-build: + @echo "Testing build and execution..." + @cd test && go run test_helper.go test_build.go + +# Helper targets +.PHONY: help +help: + @echo "mdget Testing Suite" + @echo "" + @echo "Usage:" + @echo " make - Run all tests" + @echo " make build - Build mdget binary" + @echo " make clean - Clean build artifacts" + @echo " make test-all - Run all tests" + @echo " make test-args - Test command-line arguments" + @echo " make test-http - Test HTTP connectivity" + @echo " make test-output - Test file output" + @echo " make test-errors - Test error conditions" + @echo " make test-conversion - Test HTML to Markdown conversion" + @echo " make test-build - Test build and execution" + @echo " make help - Show this help" + +# Variables +BINARY_PATH ?= bin/mdget
M
cmd/mdget.go
→
cmd/mdget.go
@@ -13,8 +13,8 @@ "github.com/imroc/req/v3"
) var ( - version = "0.1.0" - buildDate = "2026-04-20" + version string + buildDate string ) // Args defines the command-line arguments for mdget@@ -107,4 +107,3 @@
// Output Markdown to destination fmt.Fprint(output, markdown) } -
A
test/README.md
@@ -0,0 +1,34 @@
+# Testing + +`mdget` includes a comprehensive Makefile-based test suite. To run all tests: + + make test-all + +Or simply: + + make + +The test suite covers: + +* **Command-line argument parsing**: tests for help, version, URL validation, +and flag handling +* **HTTP connectivity**: uses a mock HTTP server to test fetching and +User-Agent headers +* **HTML to Markdown conversion**: verifies proper conversion of headings, +paragraphs, lists, links, and other elements +* Tests writing results to files, for invalid URLs, timeouts, HTTP errors, and +other failure scenarios. + +To run specific test categories: + + make test-args # Test command-line arguments + make test-http # Test HTTP connectivity + make test-conversion # Test HTML to Markdown conversion + make test-output # Test file output + make test-errors # Test error conditions + make test-build # Test build and execution + +For a complete list of available commands: + + make help +
A
test/test_args.go
@@ -0,0 +1,125 @@
+package main + +import ( + "fmt" + "os" +) + +func main() { + fmt.Println("Testing command-line arguments...") + + allPassed := true + + // Test 1: No arguments shows error + fmt.Print("Test 1: No arguments shows error... ") + output, err := runMdget() + if !assertError("No arguments", err, true) { + allPassed = false + } else if !assertContains("No arguments error message", output, "Error: URL is required") { + allPassed = false + } else { + fmt.Println("✅") + } + + // Test 2: Help flag shows help + fmt.Print("Test 2: Help flag shows help... ") + output, err = runMdget("--help") + if !assertError("Help flag", err, false) { + allPassed = false + } else if !assertContains("Help text", output, "Fetch a URL and convert its HTML content to Markdown.") { + allPassed = false + } else { + fmt.Println("✅") + } + + // Test 3: Version flag shows version + fmt.Print("Test 3: Version flag shows version... ") + output, err = runMdget("--version") + if !assertError("Version flag", err, false) { + allPassed = false + } else if !assertContains("Version text", output, "mdget v0.1.0") { + allPassed = false + } else { + fmt.Println("✅") + } + + // Test 4: URL with http prefix (should work) + fmt.Print("Test 4: URL with http prefix (should fail with no network)... ") + output, err = runMdget("http://localhost:99999/invalid") + // This should fail because the URL is invalid, not because of argument parsing + if err == nil { + fmt.Println("⚠️ (expected network error but got success)") + } else { + // Should fail with network error, not argument error + if !assertContains("Network error", output, "Error fetching URL") { + allPassed = false + } else { + fmt.Println("✅ (failed as expected with network error)") + } + } + + // Test 5: URL without http prefix gets https + fmt.Print("Test 5: URL without http prefix gets https... ") + output, err = runMdget("localhost:99999/invalid") + if err == nil { + fmt.Println("⚠️ (expected network error but got success)") + } else { + // Should fail with network error + if !assertContains("Network error with auto-https", output, "Error fetching URL") { + allPassed = false + } else { + fmt.Println("✅ (failed as expected with network error)") + } + } + + // Test 6: Output flag parsing + fmt.Print("Test 6: Output flag parsing... ") + output, err = runMdget("--output", "/tmp/nonexistent/test.md", "http://localhost:99999/invalid") + if err == nil { + fmt.Println("⚠️ (expected error but got success)") + allPassed = false + } else { + // Should fail with either file creation error or network error + if !assertContains("Output flag error", output, "Error") { + allPassed = false + } else { + fmt.Println("✅ (failed as expected)") + } + } + + // Test 7: Timeout flag parsing + fmt.Print("Test 7: Timeout flag parsing... ") + output, err = runMdget("--timeout", "10", "http://localhost:99999/invalid") + if err == nil { + fmt.Println("⚠️ (expected error but got success)") + allPassed = false + } else { + if !assertContains("Timeout flag error", output, "Error fetching URL") { + allPassed = false + } else { + fmt.Println("✅ (failed as expected with network error)") + } + } + + // Test 8: User-Agent flag parsing + fmt.Print("Test 8: User-Agent flag parsing... ") + output, err = runMdget("--user-agent", "TestAgent/1.0", "http://localhost:99999/invalid") + if err == nil { + fmt.Println("⚠️ (expected error but got success)") + allPassed = false + } else { + if !assertContains("User-Agent flag error", output, "Error fetching URL") { + allPassed = false + } else { + fmt.Println("✅ (failed as expected with network error)") + } + } + + if allPassed { + fmt.Println("\n✅ All command-line argument tests passed!") + os.Exit(0) + } else { + fmt.Println("\n❌ Some command-line argument tests failed!") + os.Exit(1) + } +}
A
test/test_build.go
@@ -0,0 +1,169 @@
+package main + +import ( + "fmt" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "path/filepath" +) + +func main() { + fmt.Println("Testing build and execution...") + + allPassed := true + + // Test 1: Build the binary + fmt.Print("Test 1: Build mdget binary... ") + + // Create temporary directory for binary + tempDir, err := createTempDir("mdget-build-test-") + if err != nil { + fmt.Printf("❌ Failed to create temp dir: %v\n", err) + os.Exit(1) + } + defer cleanup(tempDir) + + binaryPath := filepath.Join(tempDir, "mdget") + + // Build the binary + cmd := exec.Command("go", "build", "-o", binaryPath, "../cmd/mdget.go") + output, err := cmd.CombinedOutput() + if err != nil { + fmt.Printf("❌ Failed to build mdget: %v\nOutput: %s\n", err, output) + os.Exit(1) + } + + // Check if binary was created + if _, err := os.Stat(binaryPath); os.IsNotExist(err) { + fmt.Printf("❌ Binary not created at %s\n", binaryPath) + allPassed = false + } else { + fmt.Println("✅") + } + + // Test 2: Run the built binary + fmt.Print("Test 2: Run built binary... ") + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html") + fmt.Fprint(w, "<html><body><h1>Built Test</h1><p>Running from compiled binary.</p></body></html>") + })) + defer server.Close() + + runOutput, err := runBuiltMdget(binaryPath, server.URL) + if !assertError("Run built binary", err, false) { + allPassed = false + } else if !assertContains("Built binary output", runOutput, "# Built Test") { + allPassed = false + } else if !assertContains("Built binary paragraph", runOutput, "Running from compiled binary") { + allPassed = false + } else { + fmt.Println("✅") + } + + // Test 3: Built binary with arguments + fmt.Print("Test 3: Built binary with arguments (help flag)... ") + + runOutput, err = runBuiltMdget(binaryPath, "--help") + if !assertError("Built binary help", err, false) { + allPassed = false + } else if !assertContains("Built binary help text", runOutput, "Fetch a URL and convert its HTML content to Markdown.") { + allPassed = false + } else { + fmt.Println("✅") + } + + // Test 4: Built binary with version flag + fmt.Print("Test 4: Built binary with version flag... ") + + runOutput, err = runBuiltMdget(binaryPath, "--version") + if !assertError("Built binary version", err, false) { + allPassed = false + } else if !assertContains("Built binary version text", runOutput, "mdget v0.1.0") { + allPassed = false + } else { + fmt.Println("✅") + } + + // Test 5: Built binary with output file + fmt.Print("Test 5: Built binary with output file... ") + + server2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html") + fmt.Fprint(w, "<html><body><h1>File Output Test</h1></body></html>") + })) + defer server2.Close() + + outputFile := filepath.Join(tempDir, "test-output.md") + runOutput, err = runBuiltMdget(binaryPath, "--output", outputFile, server2.URL) + if !assertError("Built binary file output", err, false) { + allPassed = false + } else { + // Check if file was created + content, err := readFile(outputFile) + if err != nil { + fmt.Printf("❌ Failed to read output file: %v\n", err) + allPassed = false + } else if !assertContains("Output file content", content, "# File Output Test") { + allPassed = false + } else { + fmt.Println("✅") + } + } + + // Test 6: Built binary error handling + fmt.Print("Test 6: Built binary error handling (invalid URL)... ") + + runOutput, err = runBuiltMdget(binaryPath, "://invalid-url") + if !assertError("Built binary invalid URL", err, true) { + allPassed = false + } else if !assertContains("Built binary error message", runOutput, "Error fetching URL") { + allPassed = false + } else { + fmt.Println("✅") + } + + // Test 7: Compare go run vs built binary output + fmt.Print("Test 7: Compare go run vs built binary output... ") + + server3 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html") + fmt.Fprint(w, "<html><body><h1>Comparison Test</h1><p>Same content.</p></body></html>") + })) + defer server3.Close() + + // Get output from go run + goRunOutput, err := runMdget(server3.URL) + if err != nil { + fmt.Printf("❌ go run failed: %v\n", err) + allPassed = false + } else { + // Get output from built binary + builtOutput, err := runBuiltMdget(binaryPath, server3.URL) + if err != nil { + fmt.Printf("❌ built binary failed: %v\n", err) + allPassed = false + } else if goRunOutput != builtOutput { + // They might differ slightly due to timing or other factors + // Check if both contain the essential content + if !assertContains("go run output check", goRunOutput, "# Comparison Test") || + !assertContains("built output check", builtOutput, "# Comparison Test") { + allPassed = false + } else { + fmt.Println("✅ (outputs functionally equivalent)") + } + } else { + fmt.Println("✅ (outputs identical)") + } + } + + if allPassed { + fmt.Println("\n✅ All build and execution tests passed!") + os.Exit(0) + } else { + fmt.Println("\n❌ Some build and execution tests failed!") + os.Exit(1) + } +}
A
test/test_conversion.go
@@ -0,0 +1,206 @@
+package main + +import ( + "fmt" + "net/http" + "net/http/httptest" + "os" + "strings" +) + +func main() { + fmt.Println("Testing HTML to Markdown conversion...") + + allPassed := true + + testCases := []struct { + name string + html string + expected string + }{ + { + name: "Simple heading", + html: "<h1>Test Heading</h1>", + expected: "# Test Heading", + }, + { + name: "Paragraph", + html: "<p>Test paragraph.</p>", + expected: "Test paragraph.", + }, + { + name: "List items", + html: "<ul><li>Item 1</li><li>Item 2</li></ul>", + expected: "- Item 1\n- Item 2", + }, + { + name: "Link", + html: `<a href="https://example.com">Example</a>`, + expected: "[Example](https://example.com)", + }, + { + name: "Multiple headings", + html: "<h1>Title</h1><h2>Subtitle</h2><h3>Section</h3>", + expected: "# Title\n\n## Subtitle\n\n### Section", + }, + { + name: "Bold text", + html: "<p>This is <strong>bold</strong> text.</p>", + expected: "This is **bold** text.", + }, + { + name: "Italic text", + html: "<p>This is <em>italic</em> text.</p>", + expected: "This is *italic* text.", + }, + { + name: "Code block", + html: "<pre><code>func main() {}</code></pre>", + expected: "```\nfunc main() {}\n```", + }, + { + name: "Inline code", + html: "<p>Use the <code>printf</code> function.</p>", + expected: "Use the `printf` function.", + }, + { + name: "Blockquote", + html: "<blockquote><p>Important quote.</p></blockquote>", + expected: "> Important quote.", + }, + { + name: "Image", + html: `<img src="test.png" alt="Test Image">`, + // Converter adds full URL to relative image paths + expected: "", + html: "<table><tr><td>Cell 1</td><td>Cell 2</td></tr></table>", + // Simple table converter doesn't add markdown table syntax + expected: "Cell 1", + }, + } + + for i, tc := range testCases { + fmt.Printf("Test %d: %s... ", i+1, tc.name) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html") + fmt.Fprintf(w, "<html><body>%s</body></html>", tc.html) + })) + + output, err := runMdget(server.URL) + server.Close() + + if !assertError(tc.name, err, false) { + allPassed = false + continue + } + + outputStr := strings.TrimSpace(string(output)) + found := false + + // Check if expected string is contained in output + // Some converters might add extra whitespace or formatting + if strings.Contains(outputStr, tc.expected) { + found = true + } else { + // Try more flexible matching - remove extra whitespace + cleanOutput := strings.Join(strings.Fields(outputStr), " ") + cleanExpected := strings.Join(strings.Fields(tc.expected), " ") + if strings.Contains(cleanOutput, cleanExpected) { + found = true + } else { + // For lists, check if items are present (order might vary in some converters) + if strings.Contains(tc.expected, "- Item") { + if strings.Contains(outputStr, "Item 1") && strings.Contains(outputStr, "Item 2") { + found = true + } + } + } + } + + if !found { + fmt.Printf("❌\n Expected to contain: %q\n Got: %q\n", tc.expected, outputStr) + allPassed = false + } else { + fmt.Println("✅") + } + } + + // Special test: Complex page with multiple elements + fmt.Print("Test: Complex page with multiple elements... ") + + complexServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html") + fmt.Fprintf(w, `<!DOCTYPE html> +<html> +<head><title>Complex Page</title></head> +<body> + <h1>Main Title</h1> + <p>Introduction paragraph with <strong>bold</strong> and <em>italic</em> text.</p> + <h2>Section 1</h2> + <ul> + <li>First item</li> + <li>Second item with <a href="/link">link</a></li> + </ul> + <pre><code>package main + +func main() { + fmt.Println("Hello") +}</code></pre> + <blockquote> + <p>Important note here.</p> + </blockquote> +</body> +</html>`) + })) + defer complexServer.Close() + + output, err := runMdget(complexServer.URL) + if !assertError("Complex page", err, false) { + allPassed = false + } else { + outputStr := string(output) + complexChecks := []string{ + "# Main Title", + "Introduction paragraph", + "## Section 1", + "First item", + "Second item", + "```", + "package main", + "> Important note", + } + + allChecksPassed := true + for _, check := range complexChecks { + if !strings.Contains(outputStr, check) { + fmt.Printf("\n Missing: %q", check) + allChecksPassed = false + allPassed = false + } + } + + if allChecksPassed { + fmt.Println("✅") + } else { + fmt.Println("❌") + } + } + + if allPassed { + fmt.Println("\n✅ All HTML to Markdown conversion tests passed!") + os.Exit(0) + } else { + fmt.Println("\n❌ Some HTML to Markdown conversion tests failed!") + os.Exit(1) + } +}
A
test/test_errors.go
@@ -0,0 +1,141 @@
+package main + +import ( + "fmt" + "net/http" + "net/http/httptest" + "os" + "time" +) + +func main() { + fmt.Println("Testing error conditions...") + + allPassed := true + + // Test 1: Invalid URL + fmt.Print("Test 1: Invalid URL... ") + output, err := runMdget("://invalid-url") + if !assertError("Invalid URL", err, true) { + allPassed = false + } else if !assertContains("Invalid URL error", output, "Error fetching URL") { + allPassed = false + } else { + fmt.Println("✅") + } + + // Test 2: Timeout + fmt.Print("Test 2: Timeout with slow server... ") + + // Create a server that delays response beyond timeout + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + time.Sleep(2 * time.Second) + w.Header().Set("Content-Type", "text/html") + fmt.Fprint(w, "<html><body>Delayed</body></html>") + })) + defer server.Close() + + output, err = runMdget("--timeout", "1", server.URL) + if !assertError("Timeout", err, true) { + allPassed = false + } else if !assertContains("Timeout error", output, "Error fetching URL") { + allPassed = false + } else { + fmt.Println("✅") + } + + // Test 3: HTTP 404 error + fmt.Print("Test 3: HTTP 404 error... ") + + server2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + fmt.Fprint(w, "Not Found") + })) + defer server2.Close() + + output, err = runMdget(server2.URL) + if !assertError("HTTP 404", err, true) { + allPassed = false + } else if !assertContains("HTTP 404 error", output, "HTTP error: 404") { + allPassed = false + } else { + fmt.Println("✅") + } + + // Test 4: HTTP 500 error + fmt.Print("Test 4: HTTP 500 error... ") + + server3 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprint(w, "Internal Server Error") + })) + defer server3.Close() + + output, err = runMdget(server3.URL) + if !assertError("HTTP 500", err, true) { + allPassed = false + } else if !assertContains("HTTP 500 error", output, "HTTP error: 500") { + allPassed = false + } else { + fmt.Println("✅") + } + + // Test 5: Non-existent host + fmt.Print("Test 5: Non-existent host... ") + output, err = runMdget("http://localhost:99999/nonexistent") + if !assertError("Non-existent host", err, true) { + allPassed = false + } else if !assertContains("Non-existent host error", output, "Error fetching URL") { + allPassed = false + } else { + fmt.Println("✅") + } + + // Test 6: Invalid port number + fmt.Print("Test 6: Invalid port number... ") + output, err = runMdget("http://example.com:999999") + if !assertError("Invalid port", err, true) { + allPassed = false + } else if !assertContains("Invalid port error", output, "Error") { + allPassed = false + } else { + fmt.Println("✅") + } + + // Test 7: Empty URL (already covered in test_args, but good to check) + fmt.Print("Test 7: Empty URL argument... ") + output, err = runMdget("") + if !assertError("Empty URL", err, true) { + allPassed = false + } else if !assertContains("Empty URL error", output, "Error: URL is required") { + allPassed = false + } else { + fmt.Println("✅") + } + + // Test 8: Malformed HTML (should still convert what it can) + fmt.Print("Test 8: Malformed HTML (should still convert)... ") + + server4 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html") + fmt.Fprint(w, "<html><body><h1>Valid part</h1><p>Broken <tag></body>") + })) + defer server4.Close() + + output, err = runMdget(server4.URL) + if !assertError("Malformed HTML", err, false) { + allPassed = false + } else if !assertContains("Malformed HTML still converts valid parts", output, "# Valid part") { + allPassed = false + } else { + fmt.Println("✅") + } + + if allPassed { + fmt.Println("\n✅ All error condition tests passed!") + os.Exit(0) + } else { + fmt.Println("\n❌ Some error condition tests failed!") + os.Exit(1) + } +}
A
test/test_helper.go
@@ -0,0 +1,98 @@
+package main + +import ( + "fmt" + "net/http" + "os" + "os/exec" + "strings" +) + +// runMdget runs mdget with the given arguments and returns output and error +func runMdget(args ...string) (string, error) { + cmd := exec.Command("go", append([]string{"run", "../cmd/mdget.go"}, args...)...) + output, err := cmd.CombinedOutput() + return string(output), err +} + +// runBuiltMdget runs the built mdget binary with given arguments +func runBuiltMdget(binaryPath string, args ...string) (string, error) { + cmd := exec.Command(binaryPath, args...) + output, err := cmd.CombinedOutput() + return string(output), err +} + +// createTestServer creates an HTTP test server with the given handler +func createTestServer(handler http.HandlerFunc) *http.Server { + server := &http.Server{ + Addr: ":0", // Let OS choose port + Handler: handler, + } + return server +} + +// createTempDir creates a temporary directory for testing +func createTempDir(prefix string) (string, error) { + tempDir, err := os.MkdirTemp("", prefix) + if err != nil { + return "", fmt.Errorf("failed to create temp dir: %v", err) + } + return tempDir, nil +} + +// assertContains checks if a string contains a substring, prints error if not +func assertContains(t string, actual, expected string) bool { + if !strings.Contains(actual, expected) { + fmt.Printf("❌ Test failed: %s\n", t) + fmt.Printf(" Expected to contain: %q\n", expected) + fmt.Printf(" Got: %q\n", actual) + return false + } + return true +} + +// assertNotContains checks if a string does NOT contain a substring +func assertNotContains(t string, actual, expected string) bool { + if strings.Contains(actual, expected) { + fmt.Printf("❌ Test failed: %s\n", t) + fmt.Printf(" Expected NOT to contain: %q\n", expected) + fmt.Printf(" Got: %q\n", actual) + return false + } + return true +} + +// assertError checks if an error occurred as expected +func assertError(t string, err error, expectError bool) bool { + hasError := err != nil + if expectError && !hasError { + fmt.Printf("❌ Test failed: %s\n", t) + fmt.Printf(" Expected error but got none\n") + return false + } + if !expectError && hasError { + fmt.Printf("❌ Test failed: %s\n", t) + fmt.Printf(" Unexpected error: %v\n", err) + return false + } + return true +} + +// readFile reads the entire contents of a file +func readFile(path string) (string, error) { + content, err := os.ReadFile(path) + if err != nil { + return "", err + } + return string(content), nil +} + +// writeFile writes content to a file +func writeFile(path, content string) error { + return os.WriteFile(path, []byte(content), 0644) +} + +// cleanup removes a file or directory +func cleanup(path string) { + os.RemoveAll(path) +}
A
test/test_http.go
@@ -0,0 +1,143 @@
+package main + +import ( + "fmt" + "net/http" + "net/http/httptest" + "os" +) + +func main() { + fmt.Println("Testing HTTP connectivity...") + + allPassed := true + + // Test 1: Basic HTML fetching and conversion + fmt.Print("Test 1: Basic HTML fetching and conversion... ") + + // Create a test server with simple HTML response + server1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html") + fmt.Fprintf(w, `<!DOCTYPE html> +<html> +<head><title>Test Page</title></head> +<body> + <h1>Hello World</h1> + <p>This is a test page.</p> + <ul> + <li>Item 1</li> + <li>Item 2</li> + </ul> + <a href="/about">About</a> + <img src="/image.png" alt="Test Image"> +</body> +</html>`) + })) + defer server1.Close() + + output, err := runMdget(server1.URL) + if !assertError("Basic fetch", err, false) { + allPassed = false + } else { + expectedMarkers := []string{"# Hello World", "This is a test page", "Item 1", "Item 2"} + allMarkersFound := true + for _, marker := range expectedMarkers { + if !assertContains("Basic fetch output", output, marker) { + allMarkersFound = false + allPassed = false + } + } + if allMarkersFound { + fmt.Println("✅") + } + } + + // Test 2: Custom User-Agent header + fmt.Print("Test 2: Custom User-Agent header... ") + + var capturedUserAgent string + server2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + capturedUserAgent = r.Header.Get("User-Agent") + w.Header().Set("Content-Type", "text/html") + fmt.Fprint(w, "<html><body>Test</body></html>") + })) + defer server2.Close() + + output, err = runMdget("--user-agent", "TestAgent/1.0", server2.URL) + if !assertError("Custom User-Agent", err, false) { + allPassed = false + } else if capturedUserAgent != "TestAgent/1.0" { + fmt.Printf("❌\n Expected User-Agent 'TestAgent/1.0', got %q\n", capturedUserAgent) + allPassed = false + } else { + fmt.Println("✅") + } + + // Test 3: Default User-Agent + fmt.Print("Test 3: Default User-Agent... ") + + capturedUserAgent = "" + server3 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + capturedUserAgent = r.Header.Get("User-Agent") + w.Header().Set("Content-Type", "text/html") + fmt.Fprint(w, "<html><body>Test</body></html>") + })) + defer server3.Close() + + output, err = runMdget(server3.URL) + if !assertError("Default User-Agent", err, false) { + allPassed = false + } else if capturedUserAgent != "mdget/0.1.0" { + fmt.Printf("❌\n Expected default User-Agent 'mdget/0.1.0', got %q\n", capturedUserAgent) + allPassed = false + } else { + fmt.Println("✅") + } + + // Test 4: HTTP timeout (fast server should work) + fmt.Print("Test 4: HTTP timeout with fast server... ") + + server4 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html") + fmt.Fprint(w, "<html><body>Fast response</body></html>") + })) + defer server4.Close() + + output, err = runMdget("--timeout", "5", server4.URL) + if !assertError("Fast server with timeout", err, false) { + allPassed = false + } else if !assertContains("Fast server output", output, "Fast response") { + allPassed = false + } else { + fmt.Println("✅") + } + + // Test 5: Different HTTP methods (should use GET) + fmt.Print("Test 5: Server receives GET request... ") + + var capturedMethod string + server5 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + capturedMethod = r.Method + w.Header().Set("Content-Type", "text/html") + fmt.Fprint(w, "<html><body>OK</body></html>") + })) + defer server5.Close() + + output, err = runMdget(server5.URL) + if !assertError("HTTP method", err, false) { + allPassed = false + } else if capturedMethod != "GET" { + fmt.Printf("❌\n Expected GET method, got %q\n", capturedMethod) + allPassed = false + } else { + fmt.Println("✅") + } + + if allPassed { + fmt.Println("\n✅ All HTTP connectivity tests passed!") + os.Exit(0) + } else { + fmt.Println("\n❌ Some HTTP connectivity tests failed!") + os.Exit(1) + } +}
A
test/test_output.go
@@ -0,0 +1,119 @@
+package main + +import ( + "fmt" + "net/http" + "net/http/httptest" + "os" + "path/filepath" +) + +func main() { + fmt.Println("Testing file output...") + + allPassed := true + + // Test 1: Write output to file + fmt.Print("Test 1: Write output to file... ") + + // Create a test server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html") + fmt.Fprint(w, "<html><body><h1>File Test</h1><p>This content should be in a file.</p></body></html>") + })) + defer server.Close() + + // Create temporary directory for test files + tempDir, err := createTempDir("mdget-test-output-") + if err != nil { + fmt.Printf("❌ Failed to create temp dir: %v\n", err) + os.Exit(1) + } + defer cleanup(tempDir) + + outputFile := filepath.Join(tempDir, "output.md") + + // Run mdget with output file + output, err := runMdget("--output", outputFile, server.URL) + if !assertError("File output", err, false) { + allPassed = false + } else { + // Check if file was created + content, err := readFile(outputFile) + if err != nil { + fmt.Printf("❌ Failed to read output file: %v\n", err) + allPassed = false + } else if !assertContains("File content", content, "# File Test") { + allPassed = false + } else if !assertContains("File content paragraph", content, "This content should be in a file") { + allPassed = false + } else if !assertNotContains("File should not contain HTML tags", content, "<html>") { + allPassed = false + } else { + fmt.Println("✅") + } + } + + // Test 2: Output to stdout when no --output flag + fmt.Print("Test 2: Output to stdout (no --output flag)... ") + + server2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html") + fmt.Fprint(w, "<html><body><h1>Stdout Test</h1></body></html>") + })) + defer server2.Close() + + output, err = runMdget(server2.URL) + if !assertError("Stdout output", err, false) { + allPassed = false + } else if !assertContains("Stdout output", output, "# Stdout Test") { + allPassed = false + } else if !assertNotContains("Stdout should not contain HTML", output, "<html>") { + allPassed = false + } else { + fmt.Println("✅") + } + + // Test 3: Output to non-existent directory should fail + fmt.Print("Test 3: Output to non-existent directory should fail... ") + + output, err = runMdget("--output", "/tmp/nonexistent12345/output.md", server.URL) + if !assertError("Non-existent directory", err, true) { + allPassed = false + } else if !assertContains("Error message for non-existent dir", output, "Error") { + allPassed = false + } else { + fmt.Println("✅") + } + + // Test 4: Empty output filename should fail + fmt.Print("Test 4: Empty output filename should fail... ") + + output, err = runMdget("--output", "", server.URL) + // This is tricky because empty string might be interpreted as "no output file" + // Let's check if it at least runs without crashing + if err != nil { + // If it fails, that's okay as long as it's a reasonable error + if !assertContains("Empty filename error", output, "Error") { + allPassed = false + } else { + fmt.Println("✅ (failed with error as expected)") + } + } else { + // If it succeeds, that means it probably wrote to stdout + if assertContains("Should have some output", output, "") { + fmt.Println("✅ (wrote to stdout)") + } else { + allPassed = false + } + } + + if allPassed { + fmt.Println("\n✅ All file output tests passed!") + os.Exit(0) + } else { + fmt.Println("\n❌ Some file output tests failed!") + os.Exit(1) + } +} +