diff --git a/cmd/api/main.go b/cmd/api/main.go index d70d44c..6541b16 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -62,7 +62,10 @@ func main() { jwtService := services.NewJWTService(cfg.JWTSecret) contactStore := store.NewContactStore(db.Pool) + messageStore := store.NewMessageStore(db.Pool) + contactHandler := handler.NewContactHandler(contactStore) + messageHandler := handler.NewMessageHandler(messageStore) router := chi.NewRouter() @@ -72,6 +75,7 @@ func main() { router.Use(mdlware.JSONContentType) 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 server := &http.Server{ diff --git a/go.mod b/go.mod index 0692415..43a9468 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.25.3 require github.com/google/uuid v1.6.0 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/golang-jwt/jwt/v5 v5.3.0 github.com/gorilla/mux v1.8.1 diff --git a/go.sum b/go.sum index 1ee6492..b1c2430 100644 --- a/go.sum +++ b/go.sum @@ -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/go.mod h1:lx5MCnOgGgsdpwzrfi9uph5xmkeb6H8AuexUNGss2no= +git.kundeng.us/phoenix/textsender-models v0.0.4-2-2f78d9b1e6-556 h1:BRF4JrIVZMa3kZOaRfHNLJTcspD1RWRoCTcabn+CAIA= +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/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= diff --git a/internal/handler/endpoint/endpoint.go b/internal/handler/endpoint/endpoint.go index 3b42140..de58a86 100644 --- a/internal/handler/endpoint/endpoint.go +++ b/internal/handler/endpoint/endpoint.go @@ -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" diff --git a/internal/handler/message.go b/internal/handler/message.go new file mode 100644 index 0000000..3222cfc --- /dev/null +++ b/internal/handler/message.go @@ -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) +} diff --git a/internal/handler/message_test.go b/internal/handler/message_test.go new file mode 100644 index 0000000..a1ab699 --- /dev/null +++ b/internal/handler/message_test.go @@ -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") +} diff --git a/internal/handler/mock_store_test.go b/internal/handler/mock_store_test.go index 2f81755..9c3abc1 100644 --- a/internal/handler/mock_store_test.go +++ b/internal/handler/mock_store_test.go @@ -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 +} diff --git a/internal/store/message.go b/internal/store/message.go new file mode 100644 index 0000000..42560e9 --- /dev/null +++ b/internal/store/message.go @@ -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 + } +} diff --git a/migrations/schema.sql b/migrations/schema.sql index 2b987e1..50798d1 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -8,3 +8,8 @@ CREATE TABLE contacts ( 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 +);