Files
gh-action-readme/internal/interfaces_test.go
Ismo Vuorinen 7f80105ff5 feat: go 1.25.5, dependency updates, renamed internal/errors (#129)
* feat: rename internal/errors to internal/apperrors

* fix(tests): clear env values before using in tests

* feat: rename internal/errors to internal/apperrors

* chore(deps): update go and all dependencies

* chore: remove renovate from pre-commit, formatting

* chore: sonarcloud fixes

* feat: consolidate constants to appconstants/constants.go

* chore: sonarcloud fixes

* feat: simplification, deduplication, test utils

* chore: sonarcloud fixes

* chore: sonarcloud fixes

* chore: sonarcloud fixes

* chore: sonarcloud fixes

* chore: clean up

* fix: config discovery, const deduplication

* chore: fixes
2026-01-01 23:17:29 +02:00

478 lines
15 KiB
Go

// Package internal provides tests for the focused interfaces and demonstrates improved testability.
package internal
import (
"os"
"strings"
"testing"
"github.com/schollz/progressbar/v3"
"github.com/ivuorinen/gh-action-readme/appconstants"
"github.com/ivuorinen/gh-action-readme/internal/apperrors"
)
// MockMessageLogger implements MessageLogger for testing.
type MockMessageLogger struct {
InfoCalls []string
SuccessCalls []string
WarningCalls []string
BoldCalls []string
PrintfCalls []string
}
func (m *MockMessageLogger) Info(format string, args ...any) {
m.InfoCalls = append(m.InfoCalls, formatMessage(format, args...))
}
func (m *MockMessageLogger) Success(format string, args ...any) {
m.SuccessCalls = append(m.SuccessCalls, formatMessage(format, args...))
}
func (m *MockMessageLogger) Warning(format string, args ...any) {
m.WarningCalls = append(m.WarningCalls, formatMessage(format, args...))
}
func (m *MockMessageLogger) Bold(format string, args ...any) {
m.BoldCalls = append(m.BoldCalls, formatMessage(format, args...))
}
func (m *MockMessageLogger) Printf(format string, args ...any) {
m.PrintfCalls = append(m.PrintfCalls, formatMessage(format, args...))
}
func (m *MockMessageLogger) Fprintf(_ *os.File, format string, args ...any) {
// For testing, just track the formatted message
m.PrintfCalls = append(m.PrintfCalls, formatMessage(format, args...))
}
// MockErrorReporter implements ErrorReporter for testing.
type MockErrorReporter struct {
ErrorCalls []string
ErrorWithSuggestionsCalls []string
ErrorWithContextCalls []string
ErrorWithSimpleFixCalls []string
}
func (m *MockErrorReporter) Error(format string, args ...any) {
m.ErrorCalls = append(m.ErrorCalls, formatMessage(format, args...))
}
func (m *MockErrorReporter) ErrorWithSuggestions(err *apperrors.ContextualError) {
if err != nil {
m.ErrorWithSuggestionsCalls = append(m.ErrorWithSuggestionsCalls, err.Error())
}
}
func (m *MockErrorReporter) ErrorWithContext(_ appconstants.ErrorCode, message string, _ map[string]string) {
m.ErrorWithContextCalls = append(m.ErrorWithContextCalls, message)
}
func (m *MockErrorReporter) ErrorWithSimpleFix(message, suggestion string) {
m.ErrorWithSimpleFixCalls = append(m.ErrorWithSimpleFixCalls, message+": "+suggestion)
}
// MockProgressReporter implements ProgressReporter for testing.
type MockProgressReporter struct {
ProgressCalls []string
}
func (m *MockProgressReporter) Progress(format string, args ...any) {
m.ProgressCalls = append(m.ProgressCalls, formatMessage(format, args...))
}
// MockOutputConfig implements OutputConfig for testing.
type MockOutputConfig struct {
QuietMode bool
}
func (m *MockOutputConfig) IsQuiet() bool {
return m.QuietMode
}
// MockProgressManager implements ProgressManager for testing.
type MockProgressManager struct {
CreateProgressBarCalls []string
CreateProgressBarForFilesCalls []string
FinishProgressBarCalls int
FinishProgressBarWithNewlineCalls int
UpdateProgressBarCalls int
ProcessWithProgressBarCalls []string
}
func (m *MockProgressManager) CreateProgressBar(description string, total int) *progressbar.ProgressBar {
m.CreateProgressBarCalls = append(m.CreateProgressBarCalls, formatMessage("%s (total: %d)", description, total))
return nil // Return nil for mock to avoid actual progress bar
}
func (m *MockProgressManager) CreateProgressBarForFiles(description string, files []string) *progressbar.ProgressBar {
m.CreateProgressBarForFilesCalls = append(
m.CreateProgressBarForFilesCalls,
formatMessage("%s (files: %d)", description, len(files)),
)
return nil // Return nil for mock to avoid actual progress bar
}
func (m *MockProgressManager) FinishProgressBar(_ *progressbar.ProgressBar) {
m.FinishProgressBarCalls++
}
func (m *MockProgressManager) FinishProgressBarWithNewline(_ *progressbar.ProgressBar) {
m.FinishProgressBarWithNewlineCalls++
}
func (m *MockProgressManager) UpdateProgressBar(_ *progressbar.ProgressBar) {
m.UpdateProgressBarCalls++
}
func (m *MockProgressManager) ProcessWithProgressBar(
description string,
items []string,
processFunc func(item string, bar *progressbar.ProgressBar),
) {
m.ProcessWithProgressBarCalls = append(
m.ProcessWithProgressBarCalls,
formatMessage("%s (items: %d)", description, len(items)),
)
// Execute the process function for each item
for _, item := range items {
processFunc(item, nil)
}
}
// Helper function to format messages consistently.
func formatMessage(format string, args ...any) string {
if len(args) == 0 {
return format
}
// Simple formatting for test purposes
result := format
for _, arg := range args {
result = strings.Replace(result, "%s", toString(arg), 1)
result = strings.Replace(result, "%d", toString(arg), 1)
result = strings.Replace(result, "%v", toString(arg), 1)
}
return result
}
func toString(v any) string {
switch val := v.(type) {
case string:
return val
case int:
return formatInt(val)
default:
return "unknown"
}
}
func formatInt(i int) string {
// Simple int to string conversion for testing
if i == 0 {
return "0"
}
result := ""
negative := i < 0
if negative {
i = -i
}
for i > 0 {
digit := i % 10
result = string(rune('0'+digit)) + result
i /= 10
}
if negative {
result = "-" + result
}
return result
}
// Test that demonstrates improved testability with focused interfaces.
func TestFocusedInterfaces_SimpleLogger(t *testing.T) {
t.Parallel()
mockLogger := &MockMessageLogger{}
simpleLogger := NewSimpleLogger(mockLogger)
// Test successful operation
simpleLogger.LogOperation("test-operation", true)
// Verify the expected calls were made
if len(mockLogger.InfoCalls) != 1 {
t.Errorf("expected 1 Info call, got %d", len(mockLogger.InfoCalls))
}
if len(mockLogger.SuccessCalls) != 1 {
t.Errorf("expected 1 Success call, got %d", len(mockLogger.SuccessCalls))
}
if len(mockLogger.WarningCalls) != 0 {
t.Errorf("expected 0 Warning calls, got %d", len(mockLogger.WarningCalls))
}
// Check message content
if !strings.Contains(mockLogger.InfoCalls[0], "test-operation") {
t.Errorf("expected Info call to contain 'test-operation', got: %s", mockLogger.InfoCalls[0])
}
if !strings.Contains(mockLogger.SuccessCalls[0], "test-operation") {
t.Errorf("expected Success call to contain 'test-operation', got: %s", mockLogger.SuccessCalls[0])
}
}
func TestFocusedInterfaces_SimpleLogger_WithFailure(t *testing.T) {
t.Parallel()
mockLogger := &MockMessageLogger{}
simpleLogger := NewSimpleLogger(mockLogger)
// Test failed operation
simpleLogger.LogOperation("failing-operation", false)
// Verify the expected calls were made
if len(mockLogger.InfoCalls) != 1 {
t.Errorf("expected 1 Info call, got %d", len(mockLogger.InfoCalls))
}
if len(mockLogger.SuccessCalls) != 0 {
t.Errorf("expected 0 Success calls, got %d", len(mockLogger.SuccessCalls))
}
if len(mockLogger.WarningCalls) != 1 {
t.Errorf("expected 1 Warning call, got %d", len(mockLogger.WarningCalls))
}
}
func TestFocusedInterfaces_ErrorManager(t *testing.T) {
t.Parallel()
mockReporter := &MockErrorReporter{}
mockFormatter := &MockErrorFormatter{}
mockManager := &mockErrorManager{
reporter: mockReporter,
formatter: mockFormatter,
}
errorManager := NewFocusedErrorManager(mockManager)
// Test validation error handling
errorManager.HandleValidationError("test-file.yml", []string{"name", "description"})
// Verify the expected calls were made
if len(mockReporter.ErrorWithContextCalls) != 1 {
t.Errorf("expected 1 ErrorWithContext call, got %d", len(mockReporter.ErrorWithContextCalls))
}
if !strings.Contains(mockReporter.ErrorWithContextCalls[0], "test-file.yml") {
t.Errorf("expected error message to contain 'test-file.yml', got: %s", mockReporter.ErrorWithContextCalls[0])
}
}
func TestFocusedInterfaces_TaskProgress(t *testing.T) {
t.Parallel()
mockReporter := &MockProgressReporter{}
taskProgress := NewTaskProgress(mockReporter)
// Test progress reporting
taskProgress.ReportProgress("compile", 3, 10)
// Verify the expected calls were made
if len(mockReporter.ProgressCalls) != 1 {
t.Errorf("expected 1 Progress call, got %d", len(mockReporter.ProgressCalls))
}
if !strings.Contains(mockReporter.ProgressCalls[0], "compile") {
t.Errorf("expected progress message to contain 'compile', got: %s", mockReporter.ProgressCalls[0])
}
}
func TestFocusedInterfaces_ConfigAwareComponent(t *testing.T) {
t.Parallel()
tests := []struct {
name string
quietMode bool
shouldShow bool
}{
{
name: "normal mode should output",
quietMode: false,
shouldShow: true,
},
{
name: "quiet mode should not output",
quietMode: true,
shouldShow: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
mockConfig := &MockOutputConfig{QuietMode: tt.quietMode}
component := NewConfigAwareComponent(mockConfig)
result := component.ShouldOutput()
if result != tt.shouldShow {
t.Errorf("expected ShouldOutput() to return %v, got %v", tt.shouldShow, result)
}
})
}
}
func TestFocusedInterfaces_CompositeOutputWriter(t *testing.T) {
t.Parallel()
// Create a composite mock that implements OutputWriter
mockLogger := &MockMessageLogger{}
mockProgress := &MockProgressReporter{}
mockConfig := &MockOutputConfig{QuietMode: false}
compositeWriter := &CompositeOutputWriter{
writer: &mockOutputWriter{
logger: mockLogger,
reporter: mockProgress,
config: mockConfig,
},
}
items := []string{"item1", "item2", "item3"}
compositeWriter.ProcessWithOutput(items)
// Verify that the composite writer uses both message logging and progress reporting
// Should have called Info and Success for overall status
if len(mockLogger.InfoCalls) != 1 {
t.Errorf("expected 1 Info call, got %d", len(mockLogger.InfoCalls))
}
if len(mockLogger.SuccessCalls) != 1 {
t.Errorf("expected 1 Success call, got %d", len(mockLogger.SuccessCalls))
}
// Should have called Progress for each item
if len(mockProgress.ProgressCalls) != 3 {
t.Errorf("expected 3 Progress calls, got %d", len(mockProgress.ProgressCalls))
}
}
func TestFocusedInterfaces_GeneratorWithDependencyInjection(t *testing.T) {
t.Parallel()
// Create focused mocks
mockOutput := &mockCompleteOutput{
logger: &MockMessageLogger{},
reporter: &MockErrorReporter{},
formatter: &MockErrorFormatter{},
progress: &MockProgressReporter{},
config: &MockOutputConfig{QuietMode: false},
}
mockProgress := &MockProgressManager{}
// Create generator with dependency injection
config := &AppConfig{
Theme: "default",
OutputFormat: "md",
OutputDir: ".",
Verbose: false,
Quiet: false,
}
generator := NewGeneratorWithDependencies(config, mockOutput, mockProgress)
// Verify the generator was created with the injected dependencies
if generator == nil {
t.Fatal("expected generator to be created")
}
if generator.Config != config {
t.Error("expected generator to have the provided config")
}
if generator.Output != mockOutput {
t.Error("expected generator to have the injected output")
}
if generator.Progress != mockProgress {
t.Error("expected generator to have the injected progress manager")
}
}
// Composite mock types to implement the composed interfaces
type mockCompleteOutput struct {
logger MessageLogger
reporter ErrorReporter
formatter ErrorFormatter
progress ProgressReporter
config OutputConfig
}
func (m *mockCompleteOutput) Info(format string, args ...any) { m.logger.Info(format, args...) }
func (m *mockCompleteOutput) Success(format string, args ...any) { m.logger.Success(format, args...) }
func (m *mockCompleteOutput) Warning(format string, args ...any) { m.logger.Warning(format, args...) }
func (m *mockCompleteOutput) Bold(format string, args ...any) { m.logger.Bold(format, args...) }
func (m *mockCompleteOutput) Printf(format string, args ...any) { m.logger.Printf(format, args...) }
func (m *mockCompleteOutput) Fprintf(w *os.File, format string, args ...any) {
m.logger.Fprintf(w, format, args...)
}
func (m *mockCompleteOutput) Error(format string, args ...any) { m.reporter.Error(format, args...) }
func (m *mockCompleteOutput) ErrorWithSuggestions(err *apperrors.ContextualError) {
m.reporter.ErrorWithSuggestions(err)
}
func (m *mockCompleteOutput) ErrorWithContext(code appconstants.ErrorCode, message string, context map[string]string) {
m.reporter.ErrorWithContext(code, message, context)
}
func (m *mockCompleteOutput) ErrorWithSimpleFix(message, suggestion string) {
m.reporter.ErrorWithSimpleFix(message, suggestion)
}
func (m *mockCompleteOutput) FormatContextualError(err *apperrors.ContextualError) string {
return m.formatter.FormatContextualError(err)
}
func (m *mockCompleteOutput) Progress(format string, args ...any) {
m.progress.Progress(format, args...)
}
func (m *mockCompleteOutput) IsQuiet() bool { return m.config.IsQuiet() }
type mockOutputWriter struct {
logger MessageLogger
reporter ProgressReporter
config OutputConfig
}
func (m *mockOutputWriter) Info(format string, args ...any) { m.logger.Info(format, args...) }
func (m *mockOutputWriter) Success(format string, args ...any) { m.logger.Success(format, args...) }
func (m *mockOutputWriter) Warning(format string, args ...any) { m.logger.Warning(format, args...) }
func (m *mockOutputWriter) Bold(format string, args ...any) { m.logger.Bold(format, args...) }
func (m *mockOutputWriter) Printf(format string, args ...any) { m.logger.Printf(format, args...) }
func (m *mockOutputWriter) Fprintf(w *os.File, format string, args ...any) {
m.logger.Fprintf(w, format, args...)
}
func (m *mockOutputWriter) Progress(format string, args ...any) { m.reporter.Progress(format, args...) }
func (m *mockOutputWriter) IsQuiet() bool { return m.config.IsQuiet() }
// MockErrorFormatter implements ErrorFormatter for testing.
type MockErrorFormatter struct {
FormatContextualErrorCalls []string
}
func (m *MockErrorFormatter) FormatContextualError(err *apperrors.ContextualError) string {
if err != nil {
formatted := err.Error()
m.FormatContextualErrorCalls = append(m.FormatContextualErrorCalls, formatted)
return formatted
}
return ""
}
// mockErrorManager implements ErrorManager for testing.
type mockErrorManager struct {
reporter ErrorReporter
formatter ErrorFormatter
}
func (m *mockErrorManager) Error(format string, args ...any) { m.reporter.Error(format, args...) }
func (m *mockErrorManager) ErrorWithSuggestions(err *apperrors.ContextualError) {
m.reporter.ErrorWithSuggestions(err)
}
func (m *mockErrorManager) ErrorWithContext(code appconstants.ErrorCode, message string, context map[string]string) {
m.reporter.ErrorWithContext(code, message, context)
}
func (m *mockErrorManager) ErrorWithSimpleFix(message, suggestion string) {
m.reporter.ErrorWithSimpleFix(message, suggestion)
}
func (m *mockErrorManager) FormatContextualError(err *apperrors.ContextualError) string {
return m.formatter.FormatContextualError(err)
}