From 38951a21044e0e06e75648c48e02e147b1875d9f Mon Sep 17 00:00:00 2001 From: bug-author Date: Tue, 18 Nov 2025 02:57:08 +0500 Subject: [PATCH 1/3] feat: add get contact by id endpoint --- cmd/camp/main.go | 1 + internal/contact/handler.go | 30 +++++++++++++++ internal/contact/repository.go | 70 ++++++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+) diff --git a/cmd/camp/main.go b/cmd/camp/main.go index 8c02566..3ef796d 100644 --- a/cmd/camp/main.go +++ b/cmd/camp/main.go @@ -24,6 +24,7 @@ func main() { contactHandler := contact.NewHandler(contactRepository) http.HandleFunc("POST /api/contacts", contactHandler.Create) + http.HandleFunc("GET /api/contacts/{id}", contactHandler.GetByID) log.Fatal(http.ListenAndServe(":8080", nil)) } diff --git a/internal/contact/handler.go b/internal/contact/handler.go index 7aa31c9..abea4a7 100644 --- a/internal/contact/handler.go +++ b/internal/contact/handler.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "net/http" + "strconv" ) type Handler struct { @@ -43,3 +44,32 @@ func (h *Handler) Create(w http.ResponseWriter, r *http.Request) { } json.NewEncoder(w).Encode(resp) } + +func (h *Handler) GetByID(w http.ResponseWriter, r *http.Request) { + idStr := r.PathValue("id") + if idStr == "" { + http.Error(w, "missing contact id", http.StatusBadRequest) + return + } + + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + http.Error(w, "invalid contact id", http.StatusBadRequest) + return + } + + contact, err := h.repo.GetByID(id) + if err != nil { + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + if contact == nil { + http.Error(w, "contact not found", http.StatusNotFound) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(contact) +} diff --git a/internal/contact/repository.go b/internal/contact/repository.go index cc0b3d8..d0ee615 100644 --- a/internal/contact/repository.go +++ b/internal/contact/repository.go @@ -221,3 +221,73 @@ func createContact(txn *sql.Tx, c *Contact) (int64, error) { return lastId, nil } + +func (r *Repository) GetByID(id int64) (*Contact, error) { + query := ` + SELECT + id, + fname, + lname, + email, + phone, + created_at, + updated_at + FROM contacts + WHERE id = ? + LIMIT 1 +` + + var contact Contact + err := r.db.QueryRow(query, id).Scan( + &contact.ID, + &contact.FirstName, + &contact.LastName, + &contact.Email, + &contact.Phone, + &contact.CreatedAt, + &contact.UpdatedAt, + ) + + if err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + return nil, fmt.Errorf("failed to get contact: %w", err) + } + + tagsQuery := ` + SELECT + t.id, + t.text, + t.created_at, + t.updated_at + FROM tags t + INNER JOIN contact_tag ct + ON t.id = ct.tag_id + WHERE ct.contact_id = ? +` + + rows, err := r.db.Query(tagsQuery, id) + + if err != nil { + return nil, fmt.Errorf("failed to get tags: %w", err) + } + defer rows.Close() + + var tags []Tag + for rows.Next() { + var tag Tag + if err := rows.Scan(&tag.ID, &tag.Text, &tag.CreatedAt, &tag.UpdatedAt); err != nil { + return nil, fmt.Errorf("failed to scan tag: %w", err) + } + tags = append(tags, tag) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("error iterating tags: %w", err) + } + + contact.Tags = tags + + return &contact, nil +} From 86d27d50b265fbe17a39d98dce202879876a9e1f Mon Sep 17 00:00:00 2001 From: bug-author Date: Wed, 19 Nov 2025 03:35:02 +0500 Subject: [PATCH 2/3] feat: add tests for getContactByID --- internal/contact/repository_test.go | 88 +++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/internal/contact/repository_test.go b/internal/contact/repository_test.go index 7dd5f79..a2a88cd 100644 --- a/internal/contact/repository_test.go +++ b/internal/contact/repository_test.go @@ -162,6 +162,94 @@ func TestContactRepositoryThrowErrorIfTagsNotSent(t *testing.T) { } } +func TestContactRepositoryGetByID(t *testing.T) { + testDB = setupDB(t) + repo := NewRepository(testDB) + + t.Run("get existing contact with all fields", func(t *testing.T) { + contact := &Contact{ + FirstName: "John", + LastName: "Doe", + Email: "john@codersgyan.com", + Tags: []Tag{{Text: "Tag1"}, {Text: "Tag2"}}, + Phone: "000-1234", + } + + id, err := repo.CreateContactOrUpsertTags(contact) + if err != nil { + t.Fatalf("Failed to create contact: %v", err) + } + + retrievedContact, err := repo.GetByID(id) + if err != nil { + t.Fatalf("Failed to get contact by ID: %v", err) + } + + if retrievedContact == nil { + t.Fatal("Expected contact, got nil") + } + + if retrievedContact.ID != id { + t.Errorf("Expected ID %d, got %d", id, retrievedContact.ID) + } + if retrievedContact.FirstName != contact.FirstName { + t.Errorf("Expected FirstName '%s', got '%s'", contact.FirstName, retrievedContact.FirstName) + } + if retrievedContact.LastName != contact.LastName { + t.Errorf("Expected LastName '%s', got '%s'", contact.LastName, retrievedContact.LastName) + } + if retrievedContact.Email != contact.Email { + t.Errorf("Expected Email '%s', got '%s'", contact.Email, retrievedContact.Email) + } + if retrievedContact.Phone != contact.Phone { + t.Errorf("Expected Phone '%s', got '%s'", contact.Phone, retrievedContact.Phone) + } + if len(retrievedContact.Tags) != len(contact.Tags) { + t.Fatalf("Expected %d tags, got %d", len(contact.Tags), len(retrievedContact.Tags)) + } + + tagTexts := make(map[string]bool) + for _, tag := range retrievedContact.Tags { + tagTexts[tag.Text] = true + } + for _, expectedTag := range contact.Tags { + if !tagTexts[expectedTag.Text] { + t.Errorf("Expected tag '%s' not found in retrieved tags", expectedTag.Text) + } + } + }) + + t.Run("non-existing contact with negative ID", func(t *testing.T) { + notFound, err := repo.GetByID(-1) + if err != nil { + t.Fatalf("Expected no error for non-existing contact, got: %v", err) + } + if notFound != nil { + t.Error("Expected nil for non-existing contact") + } + }) + + t.Run("non-existing contact with zero ID", func(t *testing.T) { + notFound, err := repo.GetByID(0) + if err != nil { + t.Fatalf("Expected no error for non-existing contact, got: %v", err) + } + if notFound != nil { + t.Error("Expected nil for non-existing contact") + } + }) + + t.Run("non-existing contact with large ID", func(t *testing.T) { + notFound, err := repo.GetByID(999999) + if err != nil { + t.Fatalf("Expected no error for non-existing contact, got: %v", err) + } + if notFound != nil { + t.Error("Expected nil for non-existing contact") + } + }) +} + func setupDB(t *testing.T) *sql.DB { db, err := sql.Open("sqlite3", ":memory:") if err != nil { From ec35bc179e3611d67ce0f90432046dd38a52ab2d Mon Sep 17 00:00:00 2001 From: bug-author Date: Wed, 19 Nov 2025 03:39:23 +0500 Subject: [PATCH 3/3] refactor: rename GetByID -> GetContactByID --- internal/contact/handler.go | 2 +- internal/contact/repository.go | 2 +- internal/contact/repository_test.go | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/contact/handler.go b/internal/contact/handler.go index abea4a7..f605088 100644 --- a/internal/contact/handler.go +++ b/internal/contact/handler.go @@ -58,7 +58,7 @@ func (h *Handler) GetByID(w http.ResponseWriter, r *http.Request) { return } - contact, err := h.repo.GetByID(id) + contact, err := h.repo.GetContactByID(id) if err != nil { http.Error(w, "Internal server error", http.StatusInternalServerError) return diff --git a/internal/contact/repository.go b/internal/contact/repository.go index d0ee615..e358835 100644 --- a/internal/contact/repository.go +++ b/internal/contact/repository.go @@ -222,7 +222,7 @@ func createContact(txn *sql.Tx, c *Contact) (int64, error) { return lastId, nil } -func (r *Repository) GetByID(id int64) (*Contact, error) { +func (r *Repository) GetContactByID(id int64) (*Contact, error) { query := ` SELECT id, diff --git a/internal/contact/repository_test.go b/internal/contact/repository_test.go index a2a88cd..82c51b1 100644 --- a/internal/contact/repository_test.go +++ b/internal/contact/repository_test.go @@ -180,7 +180,7 @@ func TestContactRepositoryGetByID(t *testing.T) { t.Fatalf("Failed to create contact: %v", err) } - retrievedContact, err := repo.GetByID(id) + retrievedContact, err := repo.GetContactByID(id) if err != nil { t.Fatalf("Failed to get contact by ID: %v", err) } @@ -220,7 +220,7 @@ func TestContactRepositoryGetByID(t *testing.T) { }) t.Run("non-existing contact with negative ID", func(t *testing.T) { - notFound, err := repo.GetByID(-1) + notFound, err := repo.GetContactByID(-1) if err != nil { t.Fatalf("Expected no error for non-existing contact, got: %v", err) } @@ -230,7 +230,7 @@ func TestContactRepositoryGetByID(t *testing.T) { }) t.Run("non-existing contact with zero ID", func(t *testing.T) { - notFound, err := repo.GetByID(0) + notFound, err := repo.GetContactByID(0) if err != nil { t.Fatalf("Expected no error for non-existing contact, got: %v", err) } @@ -240,7 +240,7 @@ func TestContactRepositoryGetByID(t *testing.T) { }) t.Run("non-existing contact with large ID", func(t *testing.T) { - notFound, err := repo.GetByID(999999) + notFound, err := repo.GetContactByID(999999) if err != nil { t.Fatalf("Expected no error for non-existing contact, got: %v", err) }