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:
@@ -62,7 +62,10 @@ func main() {
|
|||||||
jwtService := services.NewJWTService(cfg.JWTSecret)
|
jwtService := services.NewJWTService(cfg.JWTSecret)
|
||||||
|
|
||||||
contactStore := store.NewContactStore(db.Pool)
|
contactStore := store.NewContactStore(db.Pool)
|
||||||
|
messageStore := store.NewMessageStore(db.Pool)
|
||||||
|
|
||||||
contactHandler := handler.NewContactHandler(contactStore)
|
contactHandler := handler.NewContactHandler(contactStore)
|
||||||
|
messageHandler := handler.NewMessageHandler(messageStore)
|
||||||
|
|
||||||
router := chi.NewRouter()
|
router := chi.NewRouter()
|
||||||
|
|
||||||
@@ -72,6 +75,7 @@ func main() {
|
|||||||
router.Use(mdlware.JSONContentType)
|
router.Use(mdlware.JSONContentType)
|
||||||
|
|
||||||
router.Handle(endpoint.ADD_CONTACT_ENDPOINT, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(contactHandler.AddContact)))
|
router.Handle(endpoint.ADD_CONTACT_ENDPOINT, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(contactHandler.AddContact)))
|
||||||
|
router.Handle(endpoint.ADD_MESSAGE, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(messageHandler.AddMessage)))
|
||||||
|
|
||||||
// Start server
|
// Start server
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ go 1.25.3
|
|||||||
require github.com/google/uuid v1.6.0
|
require github.com/google/uuid v1.6.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.kundeng.us/phoenix/textsender-models v0.0.4
|
git.kundeng.us/phoenix/textsender-models v0.0.4-2-2f78d9b1e6-556
|
||||||
github.com/go-chi/chi/v5 v5.2.3
|
github.com/go-chi/chi/v5 v5.2.3
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||||
github.com/gorilla/mux v1.8.1
|
github.com/gorilla/mux v1.8.1
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
git.kundeng.us/phoenix/textsender-models v0.0.4 h1:r3kqo/Xe5hEcRceknPZ/gw1y4VHSrhG+UYYgrgGTsVg=
|
git.kundeng.us/phoenix/textsender-models v0.0.4-2-2f78d9b1e6-556 h1:BRF4JrIVZMa3kZOaRfHNLJTcspD1RWRoCTcabn+CAIA=
|
||||||
git.kundeng.us/phoenix/textsender-models v0.0.4/go.mod h1:lx5MCnOgGgsdpwzrfi9uph5xmkeb6H8AuexUNGss2no=
|
git.kundeng.us/phoenix/textsender-models v0.0.4-2-2f78d9b1e6-556/go.mod h1:lx5MCnOgGgsdpwzrfi9uph5xmkeb6H8AuexUNGss2no=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
package endpoint
|
package endpoint
|
||||||
|
|
||||||
const MESSAGE_DRAFT_ENDPOINT = "/api/v1/message/draft"
|
const MESSAGE_DRAFT_ENDPOINT = "/api/v1/message/draft"
|
||||||
|
const ADD_MESSAGE = "/api/v1/message/new"
|
||||||
const ADD_CONTACT_ENDPOINT = "/api/v1/contact"
|
const ADD_CONTACT_ENDPOINT = "/api/v1/contact"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
|
||||||
"git.kundeng.us/phoenix/textsender-models/pkg/contact"
|
"git.kundeng.us/phoenix/textsender-models/pkg/contact"
|
||||||
|
"git.kundeng.us/phoenix/textsender-models/pkg/message"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Key struct {
|
type Key struct {
|
||||||
@@ -115,3 +116,57 @@ func (m *MockContactStore) ContactExists(ctx context.Context, phoneNumber string
|
|||||||
|
|
||||||
return exists, nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,3 +8,8 @@ CREATE TABLE contacts (
|
|||||||
user_id UUID NOT NULL
|
user_id UUID NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE messages (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
user_id UUID NOT NULL
|
||||||
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user