Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add clean code example #2701

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open

Conversation

norri
Copy link

@norri norri commented Nov 22, 2024

This PR provides alternative clean code version to clean-architecture and hexagonal recipes. This includes:

  • Better code structuring for larger codebases
  • Examples of easier mocking and unit testing
  • Example of how pgx library can be used and unit tested
  • Docker support to test it locally

Summary by CodeRabbit

  • New Features

    • Introduced a RESTful API for managing books, including endpoints to retrieve and add books.
    • Added a new in-memory database implementation for book records.
    • Created a PostgreSQL database interface for book management.
  • Documentation

    • Updated README with instructions for building and starting the application using Docker, including API endpoint examples.
  • Tests

    • Added comprehensive unit tests for the books service and HTTP handlers to ensure functionality and error handling.

Copy link

welcome bot commented Nov 22, 2024

Thanks for opening this pull request! 🎉 Please check out our contributing guidelines.

Copy link

coderabbitai bot commented Nov 22, 2024

Walkthrough

The pull request introduces several significant changes to the codebase, including the addition of a .gitignore entry for the db_data directory, a new Dockerfile for local Go application setup, and the creation of various files for managing books in a Fiber web application. Key components include configuration management, database interaction (with both PostgreSQL and in-memory options), and RESTful API endpoints for book retrieval and addition. Unit tests are also added to ensure the functionality of the new features.

Changes

File Path Change Summary
clean-code/.gitignore Added entry db_data to ignore the directory in version control.
clean-code/Dockerfile-local Created a Dockerfile for local Go application with necessary setup for dependencies.
clean-code/README.md Added sections for a RESTful API example using Fiber and PostgreSQL, including build instructions and API endpoints.
clean-code/app/config.go Introduced config.go with Configuration struct and functions for managing application settings.
clean-code/app/config_test.go Added unit tests for configuration management.
clean-code/app/datasources/data_sources.go Created DataSources struct for managing data sources, including a database reference.
clean-code/app/datasources/database/db.go Introduced Book struct and Database interface with methods for managing book records.
clean-code/app/datasources/database/db_mock.go Added mock implementation of the Database interface for testing.
clean-code/app/datasources/database/db_test.go Added unit tests for the NewDatabase function.
clean-code/app/datasources/database/memory_db.go Created in-memory database implementation for book records.
clean-code/app/datasources/database/memory_db_test.go Added unit tests for the memory database functionality.
clean-code/app/datasources/database/postgres_db.go Introduced PostgreSQL database interface and implementation for book records.
clean-code/app/datasources/database/postgres_db_test.go Added unit tests for the postgresDB struct.
clean-code/app/main.go Created main.go as the application entry point, initializing context and server.
clean-code/app/server/domain/books.go Added Book and BooksResponse structs for handling book data.
clean-code/app/server/domain/errors.go Introduced ErrorResponse struct for standardized error responses.
clean-code/app/server/handlers/books.go Created handler functions for retrieving and adding books.
clean-code/app/server/handlers/books_test.go Added unit tests for book-related HTTP handlers.
clean-code/app/server/server.go Introduced server setup with defined API routes for books and a health check.
clean-code/app/server/server_test.go Added test for the server's status endpoint.
clean-code/app/server/services/books.go Created BooksService interface and its implementation for managing books.
clean-code/app/server/services/books_mock.go Added mock implementation of BooksService for testing.
clean-code/app/server/services/books_test.go Added unit tests for the BooksService.
clean-code/db/init_db.sql Added SQL table books with columns id and title.

Poem

🐰 In the land of code where rabbits play,
New features hop in, brightening the day.
With books to read and data to share,
Our app's now ready, with love and care.
So let's raise a cheer, for changes so grand,
A joyful leap in this coding land! 🎉


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai or @coderabbitai title anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 26

🧹 Outside diff range and nitpick comments (39)
clean-code/app/main.go (1)

3-11: Consider enhancing application lifecycle management

While the imports are well-organized, consider adding support for graceful shutdown and more robust error handling:

  1. Add os/signal and syscall for handling shutdown signals
  2. Consider using a more robust logging package like zerolog or zap

Example implementation:

import (
    "context"
    "log"
    "os"
    "os/signal"
    "syscall"

    "app/datasources"
    "app/datasources/database"
    "app/server"
)
clean-code/app/config.go (3)

1-3: Consider moving configuration to a dedicated package

For better separation of concerns and adherence to clean architecture principles, consider moving this configuration code to a dedicated package (e.g., config or internal/config). This would make it more reusable and maintainable, especially in larger applications.

-package main
+package config

 import "os"

5-8: Add field documentation for better clarity

Consider adding documentation comments for the struct and its fields to improve code clarity and maintainability.

+// Configuration holds the application configuration settings
 type Configuration struct {
+	// Port specifies the server port to listen on
 	Port        string
+	// DatabaseURL specifies the connection string for the database
 	DatabaseURL string
 }

17-22: Consider adding debug logging

The environment variable lookup implementation is correct, but adding debug logging could help with troubleshooting configuration issues in production.

 func getEnvOrDefault(key, defaultValue string) string {
 	if value, exists := os.LookupEnv(key); exists {
+		log.Printf("Config: %s=%s\n", key, value)
 		return value
 	}
+	log.Printf("Config: %s not set, using default: %s\n", key, defaultValue)
 	return defaultValue
 }
clean-code/app/server/server_test.go (1)

14-16: Consider improving context and resource management.

The test initializes the server with an empty context and DataSources, which might not represent real-world scenarios accurately.

Consider these improvements:

 func TestGetStatus(t *testing.T) {
-    app := NewServer(context.Background(), &datasources.DataSources{})
+    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+    defer cancel()
+    
+    ds := &datasources.DataSources{}
+    defer ds.Close() // if applicable
+    
+    app := NewServer(ctx, ds)
clean-code/app/server/services/books_mock.go (3)

11-13: Add documentation for the mock struct.

Consider adding a doc comment explaining the purpose of this mock and how it should be used in tests.

+// BooksServiceMock provides a mock implementation of the BooksService interface
+// for testing purposes. It uses testify/mock for implementing mock behavior.
 type BooksServiceMock struct {
     mock.Mock
 }

15-21: Add safe type assertion to prevent potential panics.

The current type assertion could panic if the mock is set up incorrectly. While this is a test mock, safer type assertions would make tests more maintainable.

 func (m *BooksServiceMock) GetBooks(ctx context.Context) ([]domain.Book, error) {
     args := m.Called(ctx)
     if args.Get(0) == nil {
         return nil, args.Error(1)
     }
-    return args.Get(0).([]domain.Book), args.Error(1)
+    books, ok := args.Get(0).([]domain.Book)
+    if !ok {
+        return nil, fmt.Errorf("invalid mock setup: expected []domain.Book, got %T", args.Get(0))
+    }
+    return books, args.Error(1)
 }

23-26: Consider adding validation for the newBook parameter.

While the implementation is correct, consider adding validation to ensure the mock behaves similarly to the real service when receiving invalid input.

 func (m *BooksServiceMock) SaveBook(ctx context.Context, newBook domain.Book) error {
+    if newBook.ID == "" {
+        return fmt.Errorf("mock validation: book ID cannot be empty")
+    }
     args := m.Called(ctx, newBook)
     return args.Error(0)
 }
clean-code/app/datasources/database/db_mock.go (1)

26-27: Enhance mock cleanup method

The empty implementation misses opportunities for better testing support.

Consider this enhanced implementation:

+// CloseConnections is a mock implementation that tracks connection closure.
+// In tests, you can verify if cleanup was called:
+//   mock.AssertCalled(t, "CloseConnections")
 func (m *DatabaseMock) CloseConnections() {
+    m.Called()
 }
clean-code/app/server/server.go (2)

3-11: Consider adding error handling utilities

For a production-ready clean code example, consider importing error handling utilities (like errors package or a custom error package) to properly handle and wrap errors that might occur during database operations or request processing.


15-16: Consider adding essential middleware and documenting versioning strategy

For a production-ready example, consider adding essential middleware for:

  • Request logging
  • Panic recovery
  • CORS handling
  • Request timeout
  • Authentication/Authorization

Also, document the API versioning strategy (currently using path-based v1).

Example middleware setup:

 	app := fiber.New()
+	app.Use(recover.New())
+	app.Use(logger.New())
+	app.Use(cors.New())
+	app.Use(timeout.New(timeout.Config{
+		TimeoutHandler: func(c *fiber.Ctx) error {
+			return c.Status(fiber.StatusRequestTimeout).JSON(fiber.Map{
+				"error": "Request Timeout",
+			})
+		},
+		Timeout: 5 * time.Second,
+	}))
 	apiRoutes := app.Group("/api")
clean-code/app/datasources/database/memory_db.go (2)

5-11: Consider improving documentation and visibility.

While the implementation is good, consider these improvements:

  1. Add godoc-style comments explaining the purpose and usage of this constructor
  2. Consider exporting the constructor if it's meant to be used by other packages for testing purposes
-// This is just an example and not for production use
+// NewMemoryDB returns an in-memory implementation of the Database interface.
+// It is intended for testing and example purposes only, not for production use.
+// The implementation stores books in memory and provides basic CRUD operations.
-func newMemoryDB() Database {
+func NewMemoryDB() Database {

31-32: Document the empty CloseConnections method.

Add documentation to explain why this method is empty for the in-memory implementation.

+// CloseConnections is a no-op for memory database as there are no connections to close.
+// It exists to satisfy the Database interface.
 func (db *memoryDB) CloseConnections() {
 }
clean-code/app/datasources/database/db.go (3)

9-12: Consider enhancing the Book struct with tags and documentation.

The struct could benefit from:

  1. Field tags for JSON/DB mapping
  2. GoDoc comments
  3. Title field validation

Consider applying these improvements:

+// Book represents a book entity in the system
 type Book struct {
-	ID    int
-	Title string
+	ID    int    `json:"id" db:"id"`
+	Title string `json:"title" db:"title"`
 }

+// ValidateBook ensures the book data is valid
+func (b Book) ValidateBook() error {
+	if strings.TrimSpace(b.Title) == "" {
+		return errors.New("title cannot be empty")
+	}
+	return nil
+}

14-18: Consider enhancing the Database interface with documentation and pagination.

The interface follows good practices with context usage and error handling. However, consider these improvements:

  1. Add GoDoc comments
  2. Implement pagination for LoadAllBooks to handle large datasets
  3. Return the created book from CreateBook for immediate use

Consider applying these changes:

+// Database defines the interface for database operations
 type Database interface {
-	LoadAllBooks(ctx context.Context) ([]Book, error)
+	// LoadAllBooks retrieves a paginated list of books
+	LoadAllBooks(ctx context.Context, offset, limit int) ([]Book, error)
-	CreateBook(ctx context.Context, newBook Book) error
+	// CreateBook adds a new book and returns the created entity
+	CreateBook(ctx context.Context, newBook Book) (*Book, error)
+	// CloseConnections closes all active database connections
 	CloseConnections()
 }

20-34: Consider implementing a database factory pattern with dependency injection.

The current implementation could be improved by:

  1. Using a factory pattern with registered database drivers
  2. Implementing dependency injection for better testability
  3. Adding support for database configuration beyond just the URL

Consider this alternative approach:

type DBDriver interface {
    Connect(ctx context.Context, config DBConfig) (Database, error)
}

type DBConfig struct {
    URL      string
    MaxConns int
    Timeout  time.Duration
    // Add other configuration options
}

type DatabaseFactory struct {
    drivers map[string]DBDriver
}

func (f *DatabaseFactory) Register(name string, driver DBDriver) {
    f.drivers[name] = driver
}

func (f *DatabaseFactory) Create(ctx context.Context, driverName string, config DBConfig) (Database, error) {
    driver, exists := f.drivers[driverName]
    if !exists {
        return nil, fmt.Errorf("unknown database driver: %s", driverName)
    }
    return driver.Connect(ctx, config)
}
clean-code/app/datasources/database/db_test.go (2)

11-15: Consider enhancing the memory DB test coverage.

While the type check is useful, consider adding:

  1. Verification that the database is properly initialized and operational.
  2. Interface compliance check instead of concrete type check.
 func TestNewDatabase_MemoryDB(t *testing.T) {
 	ctx := context.Background()
 	db := NewDatabase(ctx, "")
-	assert.Equal(t, "*database.memoryDB", reflect.TypeOf(db).String())
+	// Verify interface compliance
+	_, ok := db.(Database)
+	assert.True(t, ok)
+
+	// Verify database is operational
+	book := Book{Title: "Test Book"}
+	_, err := db.AddBook(ctx, book)
+	assert.NoError(t, err)
 }

31-34: Improve helper function documentation and completeness.

The helper function could be enhanced:

  1. Add documentation explaining its purpose and usage.
  2. Consider validating all Book fields.
  3. Use a more specific name indicating what's being asserted.
-func assertBook(t *testing.T, book Book, expectedID int, expected Book) {
+// assertBookEquals is a test helper that verifies if a book matches the expected values
+// by comparing all relevant fields
+func assertBookEquals(t *testing.T, actual Book, expectedID int, expected Book) {
+	t.Helper()  // Mark as test helper for better error reporting
 	assert.Equal(t, expectedID, book.ID)
 	assert.Equal(t, expected.Title, book.Title)
+	// Add other relevant field comparisons
+	assert.Equal(t, expected.Author, book.Author)
+	assert.Equal(t, expected.ISBN, book.ISBN)
 }
clean-code/app/config_test.go (2)

22-30: Consider enhancing test efficiency and coverage.

Suggestions for improvement:

  1. Enable parallel test execution using t.Parallel()
  2. Convert to table-driven tests to cover multiple scenarios
  3. Add assertions for other potential default values

Example refactor:

 func TestNewConfiguration_Defaults(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name     string
+		unsetEnv []string
+		want     Configuration
+	}{
+		{
+			name:     "all defaults",
+			unsetEnv: []string{"PORT", "DATABASE_URL"},
+			want:     Configuration{Port: "3000", DatabaseURL: ""},
+		},
+		// Add more test cases here
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			for _, env := range tt.unsetEnv {
+				os.Unsetenv(env)
+			}
+			got := NewConfiguration()
+			assert.Equal(t, tt.want, got)
+		})
+	}
-	os.Unsetenv("PORT")
-	os.Unsetenv("DATABASE_URL")
-
-	conf := NewConfiguration()
-
-	assert.Equal(t, "3000", conf.Port)
-	assert.Equal(t, "", conf.DatabaseURL)
 }

32-41: Add test cases for edge cases.

The current test covers basic scenarios, but consider adding test cases for:

  • Empty string values
  • Whitespace-only values
  • Special characters in environment variables

Example additional test cases:

func TestGetEnvOrDefault(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name         string
		envKey       string
		envValue     string
		defaultValue string
		want         string
	}{
		{
			name:         "existing env",
			envKey:       "TEST_ENV",
			envValue:     "value",
			defaultValue: "default",
			want:         "value",
		},
		{
			name:         "empty string",
			envKey:       "TEST_ENV_EMPTY",
			envValue:     "",
			defaultValue: "default",
			want:         "",
		},
		{
			name:         "whitespace only",
			envKey:       "TEST_ENV_SPACE",
			envValue:     "   ",
			defaultValue: "default",
			want:         "   ",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if tt.envValue != "" {
				os.Setenv(tt.envKey, tt.envValue)
				defer os.Unsetenv(tt.envKey)
			}
			got := getEnvOrDefault(tt.envKey, tt.defaultValue)
			assert.Equal(t, tt.want, got)
		})
	}
}
clean-code/app/server/services/books.go (2)

11-14: Add interface documentation.

Consider adding GoDoc comments to document the interface and its methods. This will improve code maintainability and help other developers understand the service's responsibilities.

+// BooksService defines the interface for book management operations
 type BooksService interface {
+    // GetBooks retrieves all books from the storage
     GetBooks(ctx context.Context) ([]domain.Book, error)
+    // SaveBook persists a new book to the storage
     SaveBook(ctx context.Context, newBook domain.Book) error
 }

16-22: Add nil check in constructor.

Consider adding validation to prevent nil database injection, which could cause runtime panics.

 func NewBooksService(db database.Database) BooksService {
+    if db == nil {
+        panic("database is required")
+    }
     return &booksService{db: db}
 }
clean-code/app/datasources/database/memory_db_test.go (2)

10-15: Enhance test coverage and naming.

While the basic happy path is covered, consider these improvements:

  1. Rename test to describe the scenario (e.g., TestMemoryDB_LoadBooks_EmptyDatabase)
  2. Add test cases for error scenarios
  3. Test context cancellation handling

Example improvement:

-func TestMemoryDB_LoadBooks(t *testing.T) {
+func TestMemoryDB_LoadBooks_EmptyDatabase(t *testing.T) {
 	db := newMemoryDB()
 	books, err := db.LoadAllBooks(context.Background())
 	assert.Nil(t, err)
 	assert.Equal(t, 0, len(books))
 }
+
+func TestMemoryDB_LoadBooks_CancelledContext(t *testing.T) {
+	db := newMemoryDB()
+	ctx, cancel := context.WithCancel(context.Background())
+	cancel()
+	books, err := db.LoadAllBooks(ctx)
+	assert.Error(t, err)
+	assert.Nil(t, books)
+}

1-44: Consider enhancing test organization for clean architecture.

For a clean code example, consider these architectural improvements:

  1. Add a test suite using suite.Suite from testify for better organization
  2. Include test fixtures or factories for book creation
  3. Add integration tests with the actual database interface
  4. Document test scenarios in BDD style (Given/When/Then)
clean-code/app/server/handlers/books.go (2)

12-24: Consider enhancing error handling and request validation.

The handler implementation is solid with good separation of concerns and proper context propagation. However, there are a few potential improvements:

  1. The error message "internal error" could be more specific while still not exposing internal details
  2. Consider adding request validation for query parameters (if any)
  3. Consider adding context timeout handling

Here's a suggested improvement:

 func GetBooks(service services.BooksService) fiber.Handler {
 	return func(c *fiber.Ctx) error {
+		// Add timeout to prevent long-running queries
+		ctx, cancel := context.WithTimeout(c.UserContext(), 5*time.Second)
+		defer cancel()
+
-		books, err := service.GetBooks(c.UserContext())
+		books, err := service.GetBooks(ctx)
 		if err != nil {
-			log.Printf("GetBooks failed: %v", err)
+			log.Printf("Failed to retrieve books: %v", err)
 			return sendError(c, fiber.StatusInternalServerError, "internal error")
 		}
 
 		return c.JSON(domain.BooksResponse{
 			Books: books,
 		})
 	}
 }

43-47: Consider enhancing error response structure.

The error helper is clean but could be more informative for API consumers.

Consider this enhancement:

-func sendError(c *fiber.Ctx, code int, message string) error {
+func sendError(c *fiber.Ctx, code int, message string) error {
+	c.Set("Content-Type", "application/json")
 	return c.Status(code).JSON(domain.ErrorResponse{
-		Error: message,
+		Error: message,
+		Code: code,
+		RequestID: c.GetRespHeader("X-Request-ID"),
 	})
 }
clean-code/app/server/services/books_test.go (5)

14-22: Enhance test coverage and documentation.

Consider the following improvements:

  1. Add a test description using t.Run() for better test organization
  2. Verify the actual content of the returned book, not just the length
  3. Validate that the context is properly passed to the database layer

Here's a suggested improvement:

 func TestGetBooks(t *testing.T) {
-    mockDB := new(database.DatabaseMock)
-    mockDB.On("LoadAllBooks", mock.Anything).Return([]database.Book{{Title: "Title"}}, nil)
-
-    service := NewBooksService(mockDB)
-    books, err := service.GetBooks(context.Background())
-    assert.Nil(t, err)
-    assert.Len(t, books, 1)
+    t.Run("successfully retrieves books from database", func(t *testing.T) {
+        expectedBook := database.Book{Title: "Title"}
+        mockDB := new(database.DatabaseMock)
+        mockDB.On("LoadAllBooks", mock.MatchedBy(func(ctx context.Context) bool {
+            return ctx != nil
+        })).Return([]database.Book{expectedBook}, nil)
+
+        service := NewBooksService(mockDB)
+        books, err := service.GetBooks(context.Background())
+
+        assert.NoError(t, err)
+        assert.Len(t, books, 1)
+        assert.Equal(t, expectedBook.Title, books[0].Title)
+        mockDB.AssertExpectations(t)
+    })

24-31: Improve error testing specificity.

The test could be more robust by:

  1. Using a specific error instead of generic assert.AnError
  2. Adding a test description
  3. Verifying the error type/message returned to ensure proper error handling

Here's a suggested improvement:

 func TestGetBooks_Fails(t *testing.T) {
-    mockDB := new(database.DatabaseMock)
-    mockDB.On("LoadAllBooks", mock.Anything).Return(nil, assert.AnError)
-
-    service := NewBooksService(mockDB)
-    _, err := service.GetBooks(context.Background())
-    assert.NotNil(t, err)
+    t.Run("returns error when database fails", func(t *testing.T) {
+        expectedErr := errors.New("database connection failed")
+        mockDB := new(database.DatabaseMock)
+        mockDB.On("LoadAllBooks", mock.Anything).Return(nil, expectedErr)
+
+        service := NewBooksService(mockDB)
+        _, err := service.GetBooks(context.Background())
+
+        assert.Error(t, err)
+        assert.Equal(t, expectedErr.Error(), err.Error())
+        mockDB.AssertExpectations(t)
+    })

33-40: Enhance save book test coverage.

Consider improving the test by:

  1. Adding test cases for different book data
  2. Verifying all book fields being passed to the database
  3. Adding proper test descriptions

Here's a suggested improvement:

 func TestSaveBook(t *testing.T) {
-    mockDB := new(database.DatabaseMock)
-    mockDB.On("CreateBook", mock.Anything, database.Book{Title: "Title"}).Return(nil)
-
-    service := NewBooksService(mockDB)
-    err := service.SaveBook(context.Background(), domain.Book{Title: "Title"})
-    assert.Nil(t, err)
+    testCases := []struct {
+        name string
+        book domain.Book
+    }{
+        {
+            name: "successfully saves book with title only",
+            book: domain.Book{Title: "Title"},
+        },
+        {
+            name: "successfully saves book with all fields",
+            book: domain.Book{Title: "Title", Author: "Author", ISBN: "1234567890"},
+        },
+    }
+
+    for _, tc := range testCases {
+        t.Run(tc.name, func(t *testing.T) {
+            mockDB := new(database.DatabaseMock)
+            mockDB.On("CreateBook", mock.Anything, mock.MatchedBy(func(b database.Book) bool {
+                return b.Title == tc.book.Title &&
+                    b.Author == tc.book.Author &&
+                    b.ISBN == tc.book.ISBN
+            })).Return(nil)
+
+            service := NewBooksService(mockDB)
+            err := service.SaveBook(context.Background(), tc.book)
+
+            assert.NoError(t, err)
+            mockDB.AssertExpectations(t)
+        })
+    }

42-49: Improve error handling test coverage.

The test could be enhanced by:

  1. Testing different error scenarios (e.g., validation errors, database constraints)
  2. Using specific errors instead of generic assert.AnError
  3. Adding proper test descriptions

Here's a suggested improvement:

 func TestSaveBook_Fails(t *testing.T) {
-    mockDB := new(database.DatabaseMock)
-    mockDB.On("CreateBook", mock.Anything, database.Book{Title: "Title"}).Return(assert.AnError)
-
-    service := NewBooksService(mockDB)
-    err := service.SaveBook(context.Background(), domain.Book{Title: "Title"})
-    assert.NotNil(t, err)
+    testCases := []struct {
+        name          string
+        book          domain.Book
+        expectedError error
+    }{
+        {
+            name:          "fails when database error occurs",
+            book:          domain.Book{Title: "Title"},
+            expectedError: errors.New("database error"),
+        },
+        {
+            name:          "fails when duplicate book exists",
+            book:          domain.Book{Title: "Title"},
+            expectedError: errors.New("duplicate book"),
+        },
+    }
+
+    for _, tc := range testCases {
+        t.Run(tc.name, func(t *testing.T) {
+            mockDB := new(database.DatabaseMock)
+            mockDB.On("CreateBook", mock.Anything, mock.MatchedBy(func(b database.Book) bool {
+                return b.Title == tc.book.Title
+            })).Return(tc.expectedError)
+
+            service := NewBooksService(mockDB)
+            err := service.SaveBook(context.Background(), tc.book)
+
+            assert.Error(t, err)
+            assert.Equal(t, tc.expectedError.Error(), err.Error())
+            mockDB.AssertExpectations(t)
+        })
+    }

1-49: Consider architectural improvements for better test coverage.

While the test structure is good, consider these architectural improvements:

  1. Add separate test cases for domain.Book to database.Book mapping
  2. Implement more thorough context handling (timeouts, cancellation)
  3. Create custom error types for better error handling and verification
  4. Consider adding integration tests alongside these unit tests

Would you like assistance in implementing any of these improvements?

clean-code/app/datasources/database/postgres_db.go (3)

12-16: Consider enhancing the interface with transaction support

While the current interface covers basic operations, consider adding transaction support for maintaining data consistency in complex operations. Also, the Close method should return an error to handle cleanup failures.

 type PostgresPool interface {
     Query(ctx context.Context, sql string, args ...interface{}) (pgx.Rows, error)
     Exec(ctx context.Context, sql string, args ...interface{}) (pgconn.CommandTag, error)
-    Close()
+    Close() error
+    Begin(ctx context.Context) (pgx.Tx, error)
 }

52-58: Add support for batch operations in CreateBook

The current implementation doesn't support batch inserts, which could be inefficient for multiple book creations.

+const queryCreateBook = "INSERT INTO books (title) VALUES ($1)"
+
 func (db *postgresDB) CreateBook(ctx context.Context, newBook Book) error {
-    _, err := db.pool.Exec(ctx, "INSERT INTO books (title) VALUES ($1)", newBook.Title)
+    _, err := db.pool.Exec(ctx, queryCreateBook, newBook.Title)
     if err != nil {
         return fmt.Errorf("failed to insert book: %w", err)
     }
     return nil
 }
+
+func (db *postgresDB) CreateBooks(ctx context.Context, books []Book) error {
+    batch := &pgx.Batch{}
+    for _, book := range books {
+        batch.Queue(queryCreateBook, book.Title)
+    }
+    
+    results := db.pool.SendBatch(ctx, batch)
+    defer results.Close()
+    
+    return results.Close()
+}

1-62: Consider implementing repository pattern more strictly

While the current implementation is clean, consider these architectural improvements:

  1. Move SQL queries to a separate constants file for better maintenance
  2. Add a proper domain model layer to separate database entities from domain entities
  3. Consider adding a transaction middleware for handling complex operations

Would you like me to provide an example of how to implement these architectural improvements?

clean-code/app/datasources/database/postgres_db_test.go (4)

15-17: Consider using parameterized queries in test expectations

While the current query works, consider using parameterized queries in the test expectations to better reflect secure coding practices that should be used in the actual implementation.

-mockPool.ExpectQuery("SELECT id, title FROM books").
+mockPool.ExpectQuery("SELECT id, title FROM books WHERE 1=$1").
+    WithArgs(1).

11-28: Consider using table-driven tests

The test could be more comprehensive by using table-driven tests to cover multiple scenarios (empty result, multiple books, etc.).

Here's a suggested refactor:

func TestPostgresDB_GetBooks(t *testing.T) {
    tests := []struct {
        name     string
        mockRows func() *pgxmock.Rows
        want     []Book
        wantErr  bool
    }{
        {
            name: "single book",
            mockRows: func() *pgxmock.Rows {
                return pgxmock.NewRows([]string{"id", "title"}).AddRow(1, "book1")
            },
            want: []Book{{ID: 1, Title: "book1"}},
        },
        {
            name: "multiple books",
            mockRows: func() *pgxmock.Rows {
                return pgxmock.NewRows([]string{"id", "title"}).
                    AddRow(1, "book1").
                    AddRow(2, "book2")
            },
            want: []Book{{ID: 1, Title: "book1"}, {ID: 2, Title: "book2"}},
        },
        {
            name: "no books",
            mockRows: func() *pgxmock.Rows {
                return pgxmock.NewRows([]string{"id", "title"})
            },
            want: []Book{},
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            mockPool, err := pgxmock.NewPool()
            assert.Nil(t, err)

            mockPool.ExpectQuery("SELECT id, title FROM books").
                WillReturnRows(tt.mockRows())

            db := postgresDB{pool: mockPool}
            result, err := db.LoadAllBooks(context.Background())
            
            if tt.wantErr {
                assert.Error(t, err)
                return
            }
            assert.Nil(t, err)
            assert.Equal(t, tt.want, result)
            assert.Nil(t, mockPool.ExpectationsWereMet())
        })
    }
}

34-35: Consider using specific database errors

Instead of using assert.AnError, consider using specific database errors (like pgx.ErrNoRows or custom errors) to make the test more realistic and meaningful. Also, the error message assertion could be more specific.

-   WillReturnError(assert.AnError)
+   WillReturnError(&pgconn.PgError{
+       Code:    "42P01",
+       Message: "relation \"books\" does not exist",
+   })

-   assert.ErrorContains(t, err, "failed to query books table")
+   assert.ErrorContains(t, err, "failed to query books table: relation \"books\" does not exist")

Also applies to: 41-42


51-53: Make SQL expectations more explicit

The current SQL expectation is too permissive. Consider making it more explicit to catch potential query changes.

-   mockPool.ExpectExec("INSERT INTO books").
+   mockPool.ExpectExec("INSERT INTO books (title) VALUES ($1)").
    WithArgs("book1").
    WillReturnResult(pgxmock.NewResult("INSERT", 1))
clean-code/app/server/handlers/books_test.go (1)

20-33: Consider adding more test cases for GetBooks.

While the happy path is tested, consider adding test cases for:

  • Multiple books in response
  • Empty books list
func TestGetBooks(t *testing.T) {
    testCases := []struct {
        name     string
        books    []domain.Book
        expected int
    }{
        {
            name:     "single book",
            books:    []domain.Book{{Title: "Title"}},
            expected: 1,
        },
        {
            name:     "multiple books",
            books:    []domain.Book{{Title: "Title1"}, {Title: "Title2"}},
            expected: 2,
        },
        {
            name:     "empty list",
            books:    []domain.Book{},
            expected: 0,
        },
    }

    for _, tc := range testCases {
        t.Run(tc.name, func(t *testing.T) {
            mockService := new(services.BooksServiceMock)
            mockService.On("GetBooks", mock.Anything).Return(tc.books, nil)

            app := fiber.New()
            app.Get(booksRoute, GetBooks(mockService))

            resp, err := app.Test(httptest.NewRequest("GET", booksRoute, nil))
            assert.Nil(t, err)
            assert.Equal(t, 200, resp.StatusCode)

            body := bodyFromResponse[domain.BooksResponse](t, resp)
            assert.Len(t, body.Books, tc.expected)
        })
    }
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 1c6adcc and 46f9dc9.

⛔ Files ignored due to path filters (3)
  • clean-code/app/go.mod is excluded by !**/*.mod
  • clean-code/app/go.sum is excluded by !**/*.sum, !**/*.sum
  • clean-code/docker-compose.yml is excluded by !**/*.yml
📒 Files selected for processing (24)
  • clean-code/.gitignore (1 hunks)
  • clean-code/Dockerfile-local (1 hunks)
  • clean-code/README.md (1 hunks)
  • clean-code/app/config.go (1 hunks)
  • clean-code/app/config_test.go (1 hunks)
  • clean-code/app/datasources/data_sources.go (1 hunks)
  • clean-code/app/datasources/database/db.go (1 hunks)
  • clean-code/app/datasources/database/db_mock.go (1 hunks)
  • clean-code/app/datasources/database/db_test.go (1 hunks)
  • clean-code/app/datasources/database/memory_db.go (1 hunks)
  • clean-code/app/datasources/database/memory_db_test.go (1 hunks)
  • clean-code/app/datasources/database/postgres_db.go (1 hunks)
  • clean-code/app/datasources/database/postgres_db_test.go (1 hunks)
  • clean-code/app/main.go (1 hunks)
  • clean-code/app/server/domain/books.go (1 hunks)
  • clean-code/app/server/domain/errors.go (1 hunks)
  • clean-code/app/server/handlers/books.go (1 hunks)
  • clean-code/app/server/handlers/books_test.go (1 hunks)
  • clean-code/app/server/server.go (1 hunks)
  • clean-code/app/server/server_test.go (1 hunks)
  • clean-code/app/server/services/books.go (1 hunks)
  • clean-code/app/server/services/books_mock.go (1 hunks)
  • clean-code/app/server/services/books_test.go (1 hunks)
  • clean-code/db/init_db.sql (1 hunks)
✅ Files skipped from review due to trivial changes (6)
  • clean-code/.gitignore
  • clean-code/Dockerfile-local
  • clean-code/app/datasources/data_sources.go
  • clean-code/app/server/domain/books.go
  • clean-code/app/server/domain/errors.go
  • clean-code/db/init_db.sql
🧰 Additional context used
🪛 LanguageTool
clean-code/README.md

[uncategorized] ~16-~16: Loose punctuation mark.
Context: ... ### Endpoints - GET /api/v1/books: Retrieves a list of all books. ```sh ...

(UNLIKELY_OPENING_PUNCTUATION)


[uncategorized] ~21-~21: Loose punctuation mark.
Context: ...i/v1/books ``` - POST /api/v1/books: Adds a new book to the collection. ``...

(UNLIKELY_OPENING_PUNCTUATION)

🪛 Markdownlint (0.35.0)
clean-code/README.md

3-3: null
Bare URL used

(MD034, no-bare-urls)

🔇 Additional comments (26)
clean-code/app/main.go (1)

14-14: Verify configuration implementation details

Let's verify the implementation of NewConfiguration() and its error handling capabilities.

✅ Verification successful

Configuration implementation is properly structured and safe

The NewConfiguration() function is well-implemented with:

  • A clear struct definition with Port and DatabaseURL fields
  • Safe environment variable handling using getEnvOrDefault
  • Default values for required configurations
  • Proper test coverage in config_test.go
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for NewConfiguration implementation
ast-grep --pattern 'func NewConfiguration() $$$'

# Check if configuration uses environment variables
rg -l "os.Getenv" clean-code/

Length of output: 418


Script:

#!/bin/bash
# Let's check the Configuration struct definition and getEnvOrDefault implementation
rg -A 5 "type Configuration struct" clean-code/app/

# Search for getEnvOrDefault implementation
ast-grep --pattern 'func getEnvOrDefault($$$) $$$'

# Check if there are any direct usages of Configuration
rg "Configuration" clean-code/app/

Length of output: 1318

clean-code/app/config.go (1)

1-22: Verify configuration usage across the application

Let's verify how this configuration is used across the application to ensure proper error handling and consistent usage.

✅ Verification successful

Let me gather more information about the database connection handling to ensure proper error handling is in place.


Configuration implementation is well-structured with proper error handling

The verification reveals a well-implemented configuration system with appropriate error handling:

  • Configuration is properly centralized with no direct environment variable access in the clean-code app
  • Database connection has proper error handling with fallback to in-memory database when URL is empty
  • Implementation includes comprehensive test coverage for both valid and invalid database configurations
  • The system gracefully handles missing environment variables with sensible defaults
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for Configuration struct usage
echo "Checking Configuration struct usage:"
rg -l "Configuration" --type go

# Search for environment variable usage to ensure centralization
echo -e "\nChecking for direct environment variable access that should use Configuration:"
rg -l "os.Getenv|os.LookupEnv" --type go | grep -v "config.go"

# Check for proper error handling around database connection
echo -e "\nChecking database connection handling:"
rg -B 2 -A 2 "DatabaseURL" --type go

Length of output: 2661


Script:

#!/bin/bash
# Check database connection implementation
echo "Checking database connection implementation:"
ast-grep --pattern 'func NewDatabase($_, $url string) $_'

# Check for error handling in database connection
echo -e "\nChecking error handling in database connection:"
rg -B 3 -A 3 "NewDatabase" --type go

# Check if there's any validation for DatabaseURL
echo -e "\nChecking for DatabaseURL validation:"
rg -B 2 -A 2 "DATABASE_URL" --type go

Length of output: 4054

clean-code/app/server/server_test.go (2)

1-12: LGTM! Package and imports are well-organized.

The package declaration and import organization follow Go best practices, with standard library imports, internal packages, and external dependencies properly grouped.


17-24: 🛠️ Refactor suggestion

Enhance test coverage with table-driven tests.

The current test only covers the happy path. Consider adding test cases for different scenarios and improving the response structure.

Here's a suggested improvement:

-func TestGetStatus(t *testing.T) {
-    app := NewServer(context.Background(), &datasources.DataSources{})
-
-    resp, err := app.Test(httptest.NewRequest("GET", "/api/status", nil))
-    assert.Nil(t, err)
-    assert.Equal(t, 200, resp.StatusCode)
-
-    body, err := io.ReadAll(resp.Body)
-    assert.Nil(t, err)
-    assert.Equal(t, "ok", string(body))
+func TestGetStatus(t *testing.T) {
+    tests := []struct {
+        name           string
+        method         string
+        expectedStatus int
+        expectedBody   string
+    }{
+        {
+            name:           "success case",
+            method:         "GET",
+            expectedStatus: http.StatusOK,
+            expectedBody:   "ok",
+        },
+        {
+            name:           "wrong method",
+            method:         "POST",
+            expectedStatus: http.StatusMethodNotAllowed,
+            expectedBody:   "",
+        },
+    }
+
+    for _, tt := range tests {
+        t.Run(tt.name, func(t *testing.T) {
+            ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+            defer cancel()
+
+            app := NewServer(ctx, &datasources.DataSources{})
+            req := httptest.NewRequest(tt.method, "/api/status", nil)
+            resp, err := app.Test(req)
+
+            assert.NoError(t, err)
+            assert.Equal(t, tt.expectedStatus, resp.StatusCode)
+
+            if tt.expectedBody != "" {
+                body, err := io.ReadAll(resp.Body)
+                assert.NoError(t, err)
+                assert.Equal(t, tt.expectedBody, string(body))
+            }
+        })
+    }
}

Additionally, consider:

  1. Using structured JSON responses instead of plain text
  2. Adding test cases for server shutdown scenarios
  3. Testing response headers

Let's verify if the status endpoint is defined in the server implementation:

clean-code/app/server/services/books_mock.go (2)

1-9: LGTM!

The package declaration and imports are well-organized and follow Go best practices.


11-26: Verify interface compliance.

Let's ensure this mock correctly implements all methods of the BooksService interface.

✅ Verification successful

Mock implementation matches the interface perfectly

The BooksService interface defines exactly two methods:

  • GetBooks(ctx context.Context) ([]domain.Book, error)
  • SaveBook(ctx context.Context, newBook domain.Book) error

The BooksServiceMock implements both methods with matching signatures, making it a complete and correct implementation of the interface.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify that BooksServiceMock implements all methods of BooksService interface

# Search for BooksService interface definition
echo "Searching for BooksService interface definition..."
ast-grep --pattern 'type BooksService interface {
  $$$
}'

# Search for any additional methods that might need to be mocked
echo "Searching for method calls on BooksService..."
rg -t go "BooksService\." -A 2

Length of output: 621

clean-code/app/datasources/database/db_mock.go (3)

1-8: LGTM!

The package name and imports are appropriate for a database mock implementation.


9-11: LGTM!

The mock struct is correctly defined with embedded mock.Mock.


1-27: Verify interface implementation completeness

Let's ensure this mock fully implements the Database interface.

✅ Verification successful

Based on the shell script results, I can see the Database interface definition in clean-code/app/datasources/database/db.go which shows the following methods:

  1. LoadAllBooks(ctx context.Context) ([]Book, error)
  2. CreateBook(ctx context.Context, newBook Book) error
  3. CloseConnections()

Comparing this with the mock implementation in db_mock.go, we can see that all methods are properly implemented:

  1. LoadAllBooks - Implemented with correct signature and mock behavior
  2. CreateBook - Implemented with correct signature and mock behavior
  3. CloseConnections - Implemented as an empty method (which is valid for a mock)

Mock implementation is complete and correct

The DatabaseMock struct fully implements all methods of the Database interface with proper signatures and appropriate mock behaviors.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify Database interface implementation
# Expected: Find Database interface definition and compare methods

# Find the interface definition
echo "Looking for Database interface definition..."
rg -p "type\s+Database\s+interface\s*{" -A 10

# Find any other methods that might need mocking
echo "Looking for interface method usages..."
ast-grep --pattern 'interface { $$$ }' 

Length of output: 17310

clean-code/app/server/server.go (1)

1-24: Verify test coverage for server setup

Let's verify the existence of tests for the server setup and endpoints.

✅ Verification successful

Test coverage is comprehensive and well-structured

The codebase has thorough test coverage across all layers:

  • Server setup and health check endpoint (/api/status) are tested in server_test.go
  • Book handlers have extensive tests in handlers/books_test.go covering success and error cases
  • Book service layer is well tested in services/books_test.go with proper mocking
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for test files related to server setup
echo "Searching for server tests..."
fd -e go -g "*server*test.go" clean-code/app/server

# Search for specific test cases
echo "Searching for specific test cases..."
rg -l "TestNewServer|TestHealthCheck|TestGetBooks|TestAddBook" clean-code/app/server

Length of output: 419


Script:

#!/bin/bash
# Let's check the content of server_test.go to verify test coverage
echo "Content of server_test.go:"
cat clean-code/app/server/server_test.go

# Check test coverage for handlers and services
echo -e "\nContent of handlers/books_test.go:"
cat clean-code/app/server/handlers/books_test.go

echo -e "\nContent of services/books_test.go:"
cat clean-code/app/server/services/books_test.go

Length of output: 5010

clean-code/app/datasources/database/memory_db.go (1)

1-32: Verify Database interface implementation.

Let's ensure that memoryDB correctly implements all methods of the Database interface.

✅ Verification successful

The output shows the Database interface but seems incomplete in showing memoryDB methods. Let me try a different pattern to ensure we capture all the methods.


Database interface implementation is complete and correct

The memoryDB type correctly implements all methods required by the Database interface:

  • LoadAllBooks(ctx context.Context) ([]Book, error)
  • CreateBook(ctx context.Context, newBook Book) error
  • CloseConnections()
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify Database interface implementation
# Expected: Find the Database interface definition and compare with memoryDB methods

# Find the Database interface definition
echo "Database interface definition:"
ast-grep --pattern 'type Database interface {
  $$$
}'

# Find all methods of memoryDB
echo -e "\nmemoryDB method implementations:"
ast-grep --pattern 'func (db *memoryDB) $$$'

Length of output: 632


Script:

#!/bin/bash
# Search for memoryDB methods with a more specific pattern
echo "memoryDB methods:"
rg "func \(db \*memoryDB\)" -A 1

# Also verify the struct definition
echo -e "\nmemoryDB struct definition:"
ast-grep --pattern 'type memoryDB struct {
  $$$
}'

Length of output: 1018

clean-code/app/datasources/database/db.go (1)

1-8: LGTM! Clean package structure and imports.

The package name matches the directory name and imports are minimal.

clean-code/app/datasources/database/db_test.go (1)

1-9: LGTM!

The package declaration and imports are appropriate for the test file.

clean-code/app/server/services/books.go (2)

1-9: LGTM! Clean package structure and imports.

The package organization follows Go best practices with clear separation between standard library and project imports.


24-38: Verify complete domain model mapping.

The current implementation only maps the Title field. Ensure this is intentional and that no other important book fields (e.g., ID, Author, ISBN) are missing from the domain model.

✅ Verification successful

The domain model is intentionally simplified to only include the Title field

Looking at the relevant models in the codebase:

  • Domain model (clean-code/app/server/domain/books.go) only defines Title field
  • Database model (clean-code/app/datasources/database/db.go) has ID and Title fields

The current implementation correctly maps all fields from the domain model, and the simplification appears to be intentional for this clean code example. The other Book structs found in the codebase are from different example projects and are not related to this implementation.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check for other potential book fields in the domain and database models
rg -A 5 "type Book struct" 

Length of output: 3738

clean-code/app/datasources/database/memory_db_test.go (2)

1-8: LGTM! Clean imports and proper package naming.

The imports are minimal and appropriate for the testing requirements.


17-27: Verify the assertBook helper function implementation.

The test uses an assertBook helper function that isn't visible in the current context. Let's verify its implementation to ensure proper assertions.

Additionally, consider enhancing the test coverage:

  1. Validate book ID assignment logic
  2. Test error scenarios (e.g., duplicate books if that's a constraint)
✅ Verification successful

assertBook helper function implementation is verified and adequate

The helper function correctly validates both the ID assignment and title matching:

  • Verifies book ID matches the expected ID value
  • Confirms book title matches the expected title
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for assertBook function implementation
ast-grep --pattern 'func assertBook($$$)'

Length of output: 402

clean-code/app/server/handlers/books.go (2)

1-10: LGTM! Clean package structure and imports.

The package organization follows clean architecture principles with clear separation of concerns.


1-47: Verify consistent error handling across handlers.

Let's ensure that the error handling pattern is consistent across all handlers in the application.

clean-code/app/server/services/books_test.go (1)

1-12: LGTM! Clean imports and proper package organization.

clean-code/app/datasources/database/postgres_db.go (2)

3-10: LGTM! Appropriate imports for PostgreSQL operations

The imports are well-organized and include the necessary packages from pgx v5 for PostgreSQL operations.


29-31: LGTM! Clean type definition

The struct definition is minimal and follows interface segregation principle.

clean-code/app/datasources/database/postgres_db_test.go (1)

68-70: Use specific database errors (similar to GetBooks_Fail)

Similar to the feedback for GetBooks_Fail, consider using specific database errors and more precise error message assertions.

Also applies to: 75-76

clean-code/app/server/handlers/books_test.go (3)

1-16: LGTM! Well-organized imports with appropriate testing dependencies.

The imports are clean and well-structured, properly separating standard library imports from external and internal dependencies.


18-18: LGTM! Good use of constant for API route.

Defining the route as a constant reduces duplication and makes maintenance easier.


1-102: Verify handler implementations exist.

Let's ensure the handlers being tested are properly implemented.

✅ Verification successful

Handler implementations are properly implemented and match test coverage

The handlers GetBooks and AddBook are correctly implemented in clean-code/app/server/handlers/books.go and their implementations align perfectly with the test scenarios:

  • GetBooks handles successful retrieval and error cases, matching test cases in TestGetBooks and TestGetBooks_ServiceFails
  • AddBook handles successful creation, invalid requests, and service errors, matching test cases in TestAddBook, TestAddBook_InvalidRequest, and TestAddBook_ServiceFails
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for GetBooks and AddBook handler implementations
ast-grep --pattern 'func GetBooks($_) fiber.Handler {
    $$$
}'

ast-grep --pattern 'func AddBook($_) fiber.Handler {
    $$$
}'

Length of output: 4835

)

func main() {
ctx := context.Background()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Replace background context with cancellable context

Using a background context without cancellation can lead to resource leaks. Consider implementing a cancellable context for proper resource management.

-   ctx := context.Background()
+   ctx, cancel := context.WithCancel(context.Background())
+   defer cancel()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ctx := context.Background()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

Comment on lines +14 to +18
conf := NewConfiguration()
dataSources := &datasources.DataSources{
DB: database.NewDatabase(ctx, conf.DatabaseURL),
}
defer dataSources.DB.CloseConnections()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add error handling for configuration and database initialization

The code doesn't handle potential errors from configuration loading and database initialization. This could lead to silent failures or unexpected behavior.

-   conf := NewConfiguration()
+   conf, err := NewConfiguration()
+   if err != nil {
+       log.Fatalf("failed to load configuration: %v", err)
+   }

    dataSources := &datasources.DataSources{
-       DB: database.NewDatabase(ctx, conf.DatabaseURL),
+       DB: database.NewDatabase(ctx, conf.DatabaseURL),
    }
+   if err := dataSources.DB.Ping(ctx); err != nil {
+       log.Fatalf("failed to connect to database: %v", err)
+   }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +20 to +22
app := server.NewServer(ctx, dataSources)
log.Fatal(app.Listen(":" + conf.Port))
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Implement graceful shutdown handling

The server lacks graceful shutdown handling, which could lead to connection leaks and incomplete request processing during shutdown.

Here's a suggested implementation:

-   app := server.NewServer(ctx, dataSources)
-   log.Fatal(app.Listen(":" + conf.Port))
+   app := server.NewServer(ctx, dataSources)
+   
+   // Server shutdown channel
+   shutdownChan := make(chan os.Signal, 1)
+   signal.Notify(shutdownChan, os.Interrupt, syscall.SIGTERM)
+   
+   // Start server in goroutine
+   go func() {
+       if err := app.Listen(":" + conf.Port); err != nil {
+           log.Printf("server error: %v", err)
+           close(shutdownChan)
+       }
+   }()
+   
+   // Wait for shutdown signal
+   <-shutdownChan
+   log.Println("shutting down server...")
+   
+   // Create shutdown context with timeout
+   shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
+   defer shutdownCancel()
+   
+   // Attempt graceful shutdown
+   if err := app.Shutdown(shutdownCtx); err != nil {
+       log.Printf("server shutdown error: %v", err)
+   }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +10 to +15
func NewConfiguration() *Configuration {
return &Configuration{
Port: getEnvOrDefault("PORT", "3000"),
DatabaseURL: getEnvOrDefault("DATABASE_URL", ""),
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add validation for DatabaseURL

The constructor should validate the DatabaseURL to ensure it's not empty in production environments. Additionally, consider using a more specific default port format (":3000") to maintain consistency with common Go web server patterns.

 func NewConfiguration() *Configuration {
+	dbURL := getEnvOrDefault("DATABASE_URL", "")
+	if dbURL == "" {
+		log.Println("Warning: DATABASE_URL is not set")
+	}
+
 	return &Configuration{
-		Port:        getEnvOrDefault("PORT", "3000"),
-		DatabaseURL: getEnvOrDefault("DATABASE_URL", ""),
+		Port:        ":" + getEnvOrDefault("PORT", "3000"),
+		DatabaseURL: dbURL,
 	}
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func NewConfiguration() *Configuration {
return &Configuration{
Port: getEnvOrDefault("PORT", "3000"),
DatabaseURL: getEnvOrDefault("DATABASE_URL", ""),
}
}
func NewConfiguration() *Configuration {
dbURL := getEnvOrDefault("DATABASE_URL", "")
if dbURL == "" {
log.Println("Warning: DATABASE_URL is not set")
}
return &Configuration{
Port: ":" + getEnvOrDefault("PORT", "3000"),
DatabaseURL: dbURL,
}
}

Comment on lines +21 to +24
func (m *DatabaseMock) CreateBook(ctx context.Context, newBook Book) error {
args := m.Called(ctx, newBook)
return args.Error(0)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add parameter validation and documentation

Similar to LoadAllBooks, this implementation needs validation and documentation.

Consider this improved implementation:

+// CreateBook is a mock implementation that simulates book creation.
+// Example usage:
+//   mock.On("CreateBook", ctx, book).Return(nil)
 func (m *DatabaseMock) CreateBook(ctx context.Context, newBook Book) error {
+    if ctx == nil {
+        return fmt.Errorf("context is required")
+    }
+    if newBook.ID == 0 {
+        return fmt.Errorf("invalid book: ID is required")
+    }
     args := m.Called(ctx, newBook)
     return args.Error(0)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (m *DatabaseMock) CreateBook(ctx context.Context, newBook Book) error {
args := m.Called(ctx, newBook)
return args.Error(0)
}
// CreateBook is a mock implementation that simulates book creation.
// Example usage:
// mock.On("CreateBook", ctx, book).Return(nil)
func (m *DatabaseMock) CreateBook(ctx context.Context, newBook Book) error {
if ctx == nil {
return fmt.Errorf("context is required")
}
if newBook.ID == 0 {
return fmt.Errorf("invalid book: ID is required")
}
args := m.Called(ctx, newBook)
return args.Error(0)
}

Comment on lines +33 to +50
func (db *postgresDB) LoadAllBooks(ctx context.Context) ([]Book, error) {
rows, err := db.pool.Query(ctx, "SELECT id, title FROM books")
if err != nil {
return nil, fmt.Errorf("failed to query books table: %w", err)
}
defer rows.Close()

var books []Book
for rows.Next() {
var record Book
err := rows.Scan(&record.ID, &record.Title)
if err != nil {
return nil, fmt.Errorf("failed to scan rows: %w", err)
}
books = append(books, record)
}
return books, nil
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve error handling and resource management in LoadAllBooks

The current implementation has several areas for improvement:

  1. Missing error check for rows.Err() after iteration
  2. Consider using pgx.CollectRows for cleaner scanning
  3. SQL query should be a constant
+const queryLoadAllBooks = "SELECT id, title FROM books"
+
 func (db *postgresDB) LoadAllBooks(ctx context.Context) ([]Book, error) {
-    rows, err := db.pool.Query(ctx, "SELECT id, title FROM books")
+    rows, err := db.pool.Query(ctx, queryLoadAllBooks)
     if err != nil {
         return nil, fmt.Errorf("failed to query books table: %w", err)
     }
     defer rows.Close()

-    var books []Book
-    for rows.Next() {
-        var record Book
-        err := rows.Scan(&record.ID, &record.Title)
-        if err != nil {
-            return nil, fmt.Errorf("failed to scan rows: %w", err)
-        }
-        books = append(books, record)
+    books, err := pgx.CollectRows(rows, pgx.RowToStructByName[Book])
+    if err != nil {
+        return nil, fmt.Errorf("failed to collect rows: %w", err)
     }
     return books, nil
 }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +18 to +27
func newPostgresDB(ctx context.Context, databaseURL string) (Database, error) {
dbpool, err := pgxpool.New(ctx, databaseURL)
if err != nil {
return nil, fmt.Errorf("unable to create connection pool: %v", err)
}

return &postgresDB{
pool: dbpool,
}, nil
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance connection pool initialization with validation and configuration

The constructor should validate the connection and allow customization of pool settings. Also, consider implementing a ping check to ensure the connection is valid.

-func newPostgresDB(ctx context.Context, databaseURL string) (Database, error) {
+func newPostgresDB(ctx context.Context, databaseURL string, opts *pgxpool.Config) (Database, error) {
+    if opts == nil {
+        config, err := pgxpool.ParseConfig(databaseURL)
+        if err != nil {
+            return nil, fmt.Errorf("invalid database URL: %w", err)
+        }
+        // Set default pool settings
+        config.MaxConns = 10
+        config.MinConns = 2
+        opts = config
+    }
-    dbpool, err := pgxpool.New(ctx, databaseURL)
+    dbpool, err := pgxpool.NewWithConfig(ctx, opts)
     if err != nil {
         return nil, fmt.Errorf("unable to create connection pool: %v", err)
     }
+    
+    // Validate connection
+    if err := dbpool.Ping(ctx); err != nil {
+        dbpool.Close()
+        return nil, fmt.Errorf("failed to ping database: %w", err)
+    }

     return &postgresDB{
         pool: dbpool,
     }, nil
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func newPostgresDB(ctx context.Context, databaseURL string) (Database, error) {
dbpool, err := pgxpool.New(ctx, databaseURL)
if err != nil {
return nil, fmt.Errorf("unable to create connection pool: %v", err)
}
return &postgresDB{
pool: dbpool,
}, nil
}
func newPostgresDB(ctx context.Context, databaseURL string, opts *pgxpool.Config) (Database, error) {
if opts == nil {
config, err := pgxpool.ParseConfig(databaseURL)
if err != nil {
return nil, fmt.Errorf("invalid database URL: %w", err)
}
// Set default pool settings
config.MaxConns = 10
config.MinConns = 2
opts = config
}
dbpool, err := pgxpool.NewWithConfig(ctx, opts)
if err != nil {
return nil, fmt.Errorf("unable to create connection pool: %v", err)
}
// Validate connection
if err := dbpool.Ping(ctx); err != nil {
dbpool.Close()
return nil, fmt.Errorf("failed to ping database: %w", err)
}
return &postgresDB{
pool: dbpool,
}, nil
}

Comment on lines +1 to +79
package database

import (
"context"
"testing"

"github.com/pashagolub/pgxmock/v4"
"github.com/stretchr/testify/assert"
)

func TestPostgresDB_GetBooks(t *testing.T) {
mockPool, err := pgxmock.NewPool()
assert.Nil(t, err)

mockPool.ExpectQuery("SELECT id, title FROM books").
WillReturnRows(pgxmock.NewRows([]string{"id", "title"}).
AddRow(1, "book1"))

db := postgresDB{
pool: mockPool,
}
result, err := db.LoadAllBooks(context.Background())
assert.Nil(t, err)
assert.Equal(t, 1, len(result))
assertBook(t, result[0], 1, Book{Title: "book1"})

assert.Nil(t, mockPool.ExpectationsWereMet())
}

func TestPostgresDB_GetBooks_Fail(t *testing.T) {
mockPool, err := pgxmock.NewPool()
assert.Nil(t, err)

mockPool.ExpectQuery("SELECT id, title FROM books").
WillReturnError(assert.AnError)

db := postgresDB{
pool: mockPool,
}
result, err := db.LoadAllBooks(context.Background())
assert.Nil(t, result)
assert.ErrorContains(t, err, "failed to query books table")

assert.Nil(t, mockPool.ExpectationsWereMet())
}

func TestPostgresDB_CreateBook(t *testing.T) {
mockPool, err := pgxmock.NewPool()
assert.Nil(t, err)

mockPool.ExpectExec("INSERT INTO books").
WithArgs("book1").
WillReturnResult(pgxmock.NewResult("INSERT", 1))

db := postgresDB{
pool: mockPool,
}
err = db.CreateBook(context.Background(), Book{Title: "book1"})
assert.Nil(t, err)

assert.Nil(t, mockPool.ExpectationsWereMet())
}

func TestPostgresDB_CreateBook_Fail(t *testing.T) {
mockPool, err := pgxmock.NewPool()
assert.Nil(t, err)

mockPool.ExpectExec("INSERT INTO books").
WithArgs("book1").
WillReturnError(assert.AnError)

db := postgresDB{
pool: mockPool,
}
err = db.CreateBook(context.Background(), Book{Title: "book1"})
assert.ErrorContains(t, err, "failed to insert book")

assert.Nil(t, mockPool.ExpectationsWereMet())
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add tests for context handling and input validation

The test suite would benefit from additional test cases:

  1. Context cancellation/timeout scenarios
  2. Input validation tests (empty title, invalid characters, etc.)

Example test for context cancellation:

func TestPostgresDB_GetBooks_ContextCancelled(t *testing.T) {
    mockPool, err := pgxmock.NewPool()
    assert.Nil(t, err)

    ctx, cancel := context.WithCancel(context.Background())
    cancel() // Cancel context immediately

    db := postgresDB{pool: mockPool}
    result, err := db.LoadAllBooks(ctx)
    
    assert.Nil(t, result)
    assert.ErrorIs(t, err, context.Canceled)
}

Comment on lines +50 to +60
func TestAddBook(t *testing.T) {
mockService := new(services.BooksServiceMock)
mockService.On("SaveBook", mock.Anything, domain.Book{Title: "Title"}).Return(nil)

app := fiber.New()
app.Post(booksRoute, AddBook(mockService))

resp, err := app.Test(postRequest(booksRoute, `{"title":"Title"}`))
assert.Nil(t, err)
assert.Equal(t, 201, resp.StatusCode)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add validation test cases for AddBook.

Consider adding test cases for input validation:

  • Empty book title
  • Very long book title
  • Special characters in title
func TestAddBook_ValidationCases(t *testing.T) {
    testCases := []struct {
        name         string
        requestBody  string
        expectedCode int
    }{
        {
            name:         "empty title",
            requestBody:  `{"title":""}`,
            expectedCode: 400,
        },
        {
            name:         "very long title",
            requestBody:  fmt.Sprintf(`{"title":"%s"}`, strings.Repeat("a", 1001)),
            expectedCode: 400,
        },
        {
            name:         "title with special chars",
            requestBody:  `{"title":"<script>alert('xss')</script>"}`,
            expectedCode: 400,
        },
    }

    for _, tc := range testCases {
        t.Run(tc.name, func(t *testing.T) {
            mockService := new(services.BooksServiceMock)
            app := fiber.New()
            app.Post(booksRoute, AddBook(mockService))

            resp, err := app.Test(postRequest(booksRoute, tc.requestBody))
            assert.Nil(t, err)
            assert.Equal(t, tc.expectedCode, resp.StatusCode)
        })
    }
}

Comment on lines +97 to +102
func bodyFromResponse[T any](t *testing.T, resp *http.Response) T {
var body T
err := json.NewDecoder(resp.Body).Decode(&body)
assert.Nil(t, err)
return body
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve error handling and add response body cleanup.

The helper function should:

  1. Close the response body to prevent resource leaks
  2. Handle JSON decoding errors more gracefully
-func bodyFromResponse[T any](t *testing.T, resp *http.Response) T {
+func bodyFromResponse[T any](t *testing.T, resp *http.Response) T {
+    defer resp.Body.Close()
     var body T
     err := json.NewDecoder(resp.Body).Decode(&body)
-    assert.Nil(t, err)
+    if err != nil {
+        t.Fatalf("failed to decode response body: %v", err)
+    }
     return body
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func bodyFromResponse[T any](t *testing.T, resp *http.Response) T {
var body T
err := json.NewDecoder(resp.Body).Decode(&body)
assert.Nil(t, err)
return body
}
func bodyFromResponse[T any](t *testing.T, resp *http.Response) T {
defer resp.Body.Close()
var body T
err := json.NewDecoder(resp.Body).Decode(&body)
if err != nil {
t.Fatalf("failed to decode response body: %v", err)
}
return body
}

@@ -0,0 +1,26 @@
## Clean code example for Fiber and PostgreSQL

This is an example of a RESTful API built using the Fiber framework (https://gofiber.io/) and PostgreSQL as the database.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please describe more about the part of the clean code here

@@ -0,0 +1,26 @@
## Clean code example for Fiber and PostgreSQL
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pls add a link in the README.md in root folder to this file

DatabaseURL string
}

func NewConfiguration() *Configuration {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image
https://github.com/Pungyeon/clean-go-article#Comments

can you add comments to all public vars/functions/structs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants