tsk-8: Create Message endpoint (#15)

Closes #8

Reviewed-on: phoenix/textsender-api#15
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
This commit is contained in:
phoenix
2025-11-05 19:37:49 +00:00
committed by phoenix
parent 2045465111
commit 507eb9eb09
9 changed files with 230 additions and 3 deletions
+1
View File
@@ -1,4 +1,5 @@
package endpoint
const MESSAGE_DRAFT_ENDPOINT = "/api/v1/message/draft"
const ADD_MESSAGE = "/api/v1/message/new"
const ADD_CONTACT_ENDPOINT = "/api/v1/contact"
+72
View File
@@ -0,0 +1,72 @@
package handler
import (
"fmt"
"net/http"
"github.com/google/uuid"
"git.kundeng.us/phoenix/textsender-api/internal/store"
"git.kundeng.us/phoenix/textsender-models/pkg/message"
)
type RequestAddMessage struct {
Content string `json:"content"`
UserId uuid.UUID `json:"user_id"`
}
type AddMessageResponse struct {
Message string `json:"message"`
Data []message.Message `json:"data"`
}
type MessageHandler struct {
MessageStore store.MessageStore
}
func NewMessageHandler(str store.MessageStore) *MessageHandler {
return &MessageHandler{MessageStore: str}
}
func (m *MessageHandler) AddMessage(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req RequestAddMessage
if err := ExtractFromRequest(r, &req); err != nil {
http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest)
}
defer r.Body.Close()
newMessage := message.Message{Content: req.Content, UserId: req.UserId}
var statusCode int
var resp AddMessageResponse
ctx := r.Context()
if exists, err := m.MessageStore.MessageExists(ctx, &newMessage); err != nil {
fmt.Println("Error: ", err)
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
if exists {
statusCode = http.StatusBadRequest
resp.Message = "Cannot create message"
} else {
if err = m.MessageStore.CreateMessage(ctx, &newMessage); err != nil {
fmt.Println("Error:", err)
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
statusCode = http.StatusCreated
resp.Message = "Message created"
resp.Data = append(resp.Data, newMessage)
}
}
}
RespondWithJSON(w, statusCode, &resp)
}
+43
View File
@@ -0,0 +1,43 @@
package handler
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"git.kundeng.us/phoenix/textsender-api/internal/handler/endpoint"
)
type CreateMessageRequest struct {
Content string `json:"content"`
UserId uuid.UUID `json:"user_id"`
}
func TestCreateMessageWithMock(t *testing.T) {
mockStore := NewMockMessageStore()
handler := NewMessageHandler(mockStore)
testUserId := uuid.New()
testBody := CreateMessageRequest{Content: "Who could tell?", UserId: testUserId}
jsonValue, _ := json.Marshal(testBody)
req, _ := http.NewRequest("POST", endpoint.ADD_MESSAGE, strings.NewReader(string(jsonValue)))
rr := httptest.NewRecorder()
handler.AddMessage(rr, req)
assert.Equal(t, http.StatusCreated, rr.Code)
var response AddMessageResponse
err := json.Unmarshal(rr.Body.Bytes(), &response)
assert.NoError(t, err, "Error Creating message %v", err)
assert.NotEmpty(t, response.Data, "No Message created")
assert.NotNil(t, response.Data[0].Id, "Id should not be nil")
}
+55
View File
@@ -8,6 +8,7 @@ import (
"github.com/google/uuid"
"git.kundeng.us/phoenix/textsender-models/pkg/contact"
"git.kundeng.us/phoenix/textsender-models/pkg/message"
)
type Key struct {
@@ -115,3 +116,57 @@ func (m *MockContactStore) ContactExists(ctx context.Context, phoneNumber string
return exists, nil
}
type MessageKey struct {
Content string
UserId uuid.UUID
}
type MockMessageStore struct {
Messages map[uuid.UUID]*message.Message
MessagesByKey map[MessageKey]*message.Message
mu sync.RWMutex
Error error // Optional: simulate errors
}
func NewMockMessageStore() *MockMessageStore {
return &MockMessageStore{
Messages: make(map[uuid.UUID]*message.Message),
MessagesByKey: make(map[MessageKey]*message.Message),
}
}
func (m *MockMessageStore) CreateMessage(ctx context.Context, msg *message.Message) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return m.Error
}
if msg.Id == uuid.Nil {
msg.Id = uuid.New()
}
key := MessageKey{Content: msg.Content, UserId: msg.UserId}
if _, exists := m.MessagesByKey[key]; exists {
return errors.New("Message already exists")
}
m.Messages[msg.Id] = msg
m.MessagesByKey[key] = msg
return nil
}
func (m *MockMessageStore) MessageExists(ctx context.Context, msg *message.Message) (bool, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return false, m.Error
}
_, exists := m.MessagesByKey[MessageKey{Content: msg.Content, UserId: msg.UserId}]
return exists, nil
}
+47
View File
@@ -0,0 +1,47 @@
package store
import (
"context"
"fmt"
"github.com/jackc/pgx/v5/pgxpool"
"git.kundeng.us/phoenix/textsender-models/pkg/message"
)
type MessageStore interface {
CreateMessage(ctx context.Context, msg *message.Message) error
MessageExists(ctx context.Context, msg *message.Message) (bool, error)
}
type PGMessageStore struct {
db *pgxpool.Pool
}
func NewMessageStore(db *pgxpool.Pool) *PGMessageStore {
return &PGMessageStore{db: db}
}
func (m *PGMessageStore) CreateMessage(ctx context.Context, msg *message.Message) error {
query := `
INSERT INTO messages (content, user_id)
VALUES ($1, $2)
RETURNING id
`
return m.db.QueryRow(ctx, query, msg.Content, msg.UserId).Scan(
&msg.Id,
)
}
func (m *PGMessageStore) MessageExists(ctx context.Context, msg *message.Message) (bool, error) {
query := `SELECT EXISTS(SELECT 1 FROM messages WHERE content = $1 AND user_id = $2)`
var exists bool
err := m.db.QueryRow(ctx, query, msg.Content, msg.UserId).Scan(&exists)
if err != nil {
return false, fmt.Errorf("checking if message exists: %w", err)
} else {
return exists, nil
}
}