Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/camp/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
30 changes: 30 additions & 0 deletions internal/contact/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"strconv"
)

type Handler struct {
Expand Down Expand Up @@ -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.GetContactByID(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)
}
70 changes: 70 additions & 0 deletions internal/contact/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,3 +221,73 @@ func createContact(txn *sql.Tx, c *Contact) (int64, error) {

return lastId, nil
}

func (r *Repository) GetContactByID(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
}
88 changes: 88 additions & 0 deletions internal/contact/repository_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.GetContactByID(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.GetContactByID(-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.GetContactByID(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.GetContactByID(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 {
Expand Down