tsk-51: Send message endpoint (#52)

Closes #51

Reviewed-on: phoenix/textsender-api#52
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
This commit is contained in:
phoenix
2025-12-10 21:35:02 +00:00
committed by phoenix
parent 20ffc45f89
commit 0d252bc261
32 changed files with 753 additions and 96 deletions
+4 -2
View File
@@ -7,6 +7,7 @@ import (
"git.kundeng.us/phoenix/textsender-models/tx0/contact"
"github.com/google/uuid"
"git.kundeng.us/phoenix/textsender-api/internal/app"
"git.kundeng.us/phoenix/textsender-api/internal/store"
)
@@ -21,11 +22,12 @@ type AddContactResponse struct {
}
type ContactHandler struct {
App *app.App
ContactStore store.ContactStore
}
func NewContactHandler(str store.ContactStore) *ContactHandler {
return &ContactHandler{ContactStore: str}
func NewContactHandler(apiApp *app.App, str store.ContactStore) *ContactHandler {
return &ContactHandler{App: apiApp, ContactStore: str}
}
// AddContact godoc
+9 -4
View File
@@ -25,7 +25,10 @@ type Request struct {
func TestCreateContactWithMock(t *testing.T) {
mockstore := mock.NewMockContactStore()
handler := NewContactHandler(mockstore)
apiApp, err := GetApp()
assert.NoError(t, err, "Error getting app")
handler := NewContactHandler(apiApp, mockstore)
testUserId := uuid.New()
testBody := Request{PhoneNumber: "+12335403383", UserId: testUserId}
@@ -39,7 +42,7 @@ func TestCreateContactWithMock(t *testing.T) {
assert.Equal(t, http.StatusCreated, rr.Code)
var response AddContactResponse
err := json.Unmarshal(rr.Body.Bytes(), &response)
err = json.Unmarshal(rr.Body.Bytes(), &response)
assert.NoError(t, err, "Error Creating contact %v", err)
assert.NotEmpty(t, response.Data, "No Contact created")
@@ -62,13 +65,15 @@ func TestGetContactWithMock(t *testing.T) {
req, _ := http.NewRequest("GET", url, nil)
rr := httptest.NewRecorder()
contactHandler := NewContactHandler(mockstore)
apiApp, err := GetApp()
assert.NoError(t, err, "Error getting app")
contactHandler := NewContactHandler(apiApp, mockstore)
contactHandler.GetContact(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
var response GetContactResponse
err := json.Unmarshal(rr.Body.Bytes(), &response)
err = json.Unmarshal(rr.Body.Bytes(), &response)
assert.NoError(t, err, "Error getting contact %v", err)
assert.NotEmpty(t, response.Data, "No Contact retrieved")
+1
View File
@@ -13,3 +13,4 @@ const DeleteScheduledMessageEventEndpoint = "/api/v1/schedule/message/event/{id}
const UpdateScheduledMessageStatusEndpoint = "/api/v1/schedule/message/status/update"
const FetchNextScheduledMessageEndpoint = "/api/v1/schedule/message/fetch"
const RecordEventResponse = "/api/v1/schedule/message/event/response/record"
const SendInstantMessageEndpoint = "/api/v1/instant/message"
+5
View File
@@ -16,7 +16,12 @@ func ExtractFromRequest(r *http.Request, reqItem interface{}) error {
}
}
func InvalidJson(err error) string {
return fmt.Sprintf("Invalid JSON: %s", err.Error())
}
// Helper function to send JSON responses
// TODO: Change type of data to any
func RespondWithJSON(w http.ResponseWriter, statusCode int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
+31
View File
@@ -0,0 +1,31 @@
package handler
import (
"fmt"
"os"
"path"
"github.com/joho/godotenv"
"git.kundeng.us/phoenix/textsender-api/internal/app"
"git.kundeng.us/phoenix/textsender-api/internal/config"
)
func GetApp() (*app.App, error) {
cwd, _ := os.Getwd()
envPath := path.Join(cwd, ".env")
err := godotenv.Load(envPath)
if err != nil {
envPath = path.Join(cwd, "../..", ".env")
if err := godotenv.Load(envPath); err != nil {
panic("Error loading .env file: " + err.Error())
}
}
if tCfg, err := config.TwilioConfig(); err != nil {
return nil, fmt.Errorf("Error: %v", err)
} else {
return &app.App{TwilioConfig: tCfg}, nil
}
}
+191
View File
@@ -0,0 +1,191 @@
package handler
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
"time"
"git.kundeng.us/phoenix/swoosh/swoop/send"
"git.kundeng.us/phoenix/textsender-models/tx0/contact"
"git.kundeng.us/phoenix/textsender-models/tx0/message"
"github.com/google/uuid"
"git.kundeng.us/phoenix/textsender-api/internal/app"
"git.kundeng.us/phoenix/textsender-api/internal/store"
)
const Contact_Limit = 400
type SendInstantMessageHandler struct {
App *app.App
MERStore store.MessageEventResponseStore
ContactStore store.ContactStore
MessageStore store.MessageStore
}
func NewSendInstantMessageHandler(apiApp *app.App, str store.MessageEventResponseStore, cStore store.ContactStore, mStore store.MessageStore) *SendInstantMessageHandler {
return &SendInstantMessageHandler{App: apiApp, MERStore: str, ContactStore: cStore, MessageStore: mStore}
}
type SendInstantMessageRequest struct {
ContactIds []uuid.UUID `json:"contact_ids"`
MessageId uuid.UUID `json:"message_id"`
UserId uuid.UUID `json:"user_id"`
}
type SendInstantMessageResponse struct {
Message string `json:"message"`
Data []*message.MessageEventResponse `json:"data"`
}
// Sent godoc
// @Summary Send instant message
// @Description Send a message without the constraints of scheduled messages (requires JWT)
// @Tags messages
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body SendInstantMessageRequest true "Send an instnt message"
// @Success 201 {object} SendInstantMessageResponse
// @Failure 400 {object} SendInstantMessageResponse
// @Failure 500 {object} SendInstantMessageResponse
// @Router /instant/message [post]
func (s *SendInstantMessageHandler) Send(w http.ResponseWriter, r *http.Request) {
var req SendInstantMessageRequest
if err := ExtractFromRequest(r, &req); err != nil {
http.Error(w, InvalidJson(err), http.StatusBadRequest)
}
defer r.Body.Close()
var statusCode int
var resp SendInstantMessageResponse
if req.UserId == uuid.Nil {
statusCode = http.StatusBadRequest
resp.Message = "User Id is nil"
} else if req.MessageId == uuid.Nil {
statusCode = http.StatusBadRequest
resp.Message = "Message Id is nil"
} else if len(req.ContactIds) == 0 {
statusCode = http.StatusBadRequest
resp.Message = "No contacts provided"
} else {
if len(req.ContactIds) > Contact_Limit {
statusCode = http.StatusBadRequest
resp.Message = "Too many contacts"
} else {
ctx := r.Context()
if msg, contacts, err := s.verify(ctx, &req); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
if msg == nil {
statusCode = http.StatusBadRequest
resp.Message = "Message is empty"
} else if len(contacts) == 0 {
statusCode = http.StatusBadRequest
resp.Message = "Contacts not found"
} else {
if s.App != nil {
msgSender := send.MessageSender{Config: s.App.TwilioConfig}
for _, c := range contacts {
if twilioResp, err := msgSender.Send(*msg, *c, nil); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
if data, err := json.Marshal(twilioResp); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
dateStr := strings.TrimSpace(*twilioResp.DateUpdated)
if parsedTime, err := time.Parse(time.RFC1123Z, dateStr); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
mer := message.MessageEventResponse{}
mer.Response = data
mer.UserId = req.UserId
mer.Sent = parsedTime
if err := s.MERStore.Create(ctx, &mer); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
statusCode = http.StatusOK
resp.Message = "Successful"
resp.Data = append(resp.Data, &mer)
}
}
}
}
}
} else {
statusCode = http.StatusInternalServerError
resp.Message = "Config not properly configured"
}
}
}
}
}
RespondWithJSON(w, statusCode, &resp)
}
func (s *SendInstantMessageHandler) verify(ctx context.Context, req *SendInstantMessageRequest) (*message.Message, []*contact.Contact, error) {
if msg, err := s.verifyMessage(ctx, req.MessageId); err != nil {
log.Println("Huh?")
return nil, nil, fmt.Errorf("Error:%v", err)
} else {
log.Println("Okay")
if msg != nil {
if contacts, err := s.verifyContacts(ctx, req.ContactIds); err != nil {
return nil, nil, fmt.Errorf("Error:%v", err)
} else {
if len(contacts) == 0 {
return msg, nil, nil
} else {
return msg, contacts, nil
}
}
} else {
log.Println("How can this be?")
return nil, nil, fmt.Errorf("Could not verify message")
}
}
}
func (s *SendInstantMessageHandler) verifyMessage(ctx context.Context, messageId uuid.UUID) (*message.Message, error) {
if msg, err := s.MessageStore.GetMessageByID(ctx, messageId); err != nil {
return nil, fmt.Errorf("Error getting message:%v", err)
} else {
if msg == nil {
log.Println("Message is nil")
return nil, nil
} else {
return msg, nil
}
}
}
func (s *SendInstantMessageHandler) verifyContacts(ctx context.Context, contactIds []uuid.UUID) ([]*contact.Contact, error) {
var contacts []*contact.Contact
for _, contactId := range contactIds {
if c, err := s.ContactStore.GetContactByID(ctx, contactId); err != nil {
return nil, fmt.Errorf("Error getting contacts:%v", err)
} else {
if c != nil {
contacts = append(contacts, c)
}
}
}
if len(contacts) == 0 {
return nil, nil
} else {
return contacts, nil
}
}
+8 -4
View File
@@ -2,11 +2,13 @@ package handler
import (
"fmt"
"log"
"net/http"
"git.kundeng.us/phoenix/textsender-models/tx0/message"
"github.com/google/uuid"
"git.kundeng.us/phoenix/textsender-api/internal/app"
"git.kundeng.us/phoenix/textsender-api/internal/store"
)
@@ -21,11 +23,12 @@ type AddMessageResponse struct {
}
type MessageHandler struct {
App *app.App
MessageStore store.MessageStore
}
func NewMessageHandler(str store.MessageStore) *MessageHandler {
return &MessageHandler{MessageStore: str}
func NewMessageHandler(apiApp *app.App, str store.MessageStore) *MessageHandler {
return &MessageHandler{App: apiApp, MessageStore: str}
}
const Message_Limit = 200
@@ -143,7 +146,7 @@ func (c *MessageHandler) GetMessage(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if id != uuid.Nil {
fmt.Println("Checking with Id")
log.Println("Checking with Id")
if con, err := c.MessageStore.GetMessageByID(ctx, id); err == nil {
statusCode = http.StatusOK
resp.Message = "Successful"
@@ -153,8 +156,9 @@ func (c *MessageHandler) GetMessage(w http.ResponseWriter, r *http.Request) {
resp.Message = err.Error()
}
} else if userId != uuid.Nil {
fmt.Println("Checking with User Id")
log.Println("Checking with User Id")
if messages, err := c.MessageStore.GetAllMessages(ctx); err == nil {
log.Println("Amount of messages:", len(messages))
for _, msg := range messages {
if msg.UserId == userId {
resp.Data = append(resp.Data, *msg)
+4 -2
View File
@@ -8,15 +8,17 @@ import (
"git.kundeng.us/phoenix/textsender-models/tx0/types"
"github.com/google/uuid"
"git.kundeng.us/phoenix/textsender-api/internal/app"
"git.kundeng.us/phoenix/textsender-api/internal/store"
)
type EventResponseHandler struct {
App *app.App
MessageEventResponseStore store.MessageEventResponseStore
}
func NewEventResponseHandler(str store.MessageEventResponseStore) *EventResponseHandler {
return &EventResponseHandler{MessageEventResponseStore: str}
func NewEventResponseHandler(apiApp *app.App, str store.MessageEventResponseStore) *EventResponseHandler {
return &EventResponseHandler{App: apiApp, MessageEventResponseStore: str}
}
type RecordEventRequest struct {
@@ -26,7 +26,9 @@ func TestRecordMessageEventResponseWithMock(t *testing.T) {
schMsgStore := mock.NewMockScheduledMessageStore()
merStore := mock.NewMockMessageEventResponseStore()
handler := NewEventResponseHandler(merStore)
apiApp, err := GetApp()
assert.NoError(t, err, "Error getting app")
handler := NewEventResponseHandler(apiApp, merStore)
recipientId := uuid.New()
messageId := uuid.New()
@@ -69,7 +71,7 @@ func TestRecordMessageEventResponseWithMock(t *testing.T) {
assert.Equal(t, http.StatusCreated, rr.Code)
var response RecordEventResponse
err := json.Unmarshal(rr.Body.Bytes(), &response)
err = json.Unmarshal(rr.Body.Bytes(), &response)
assert.NoError(t, err, "Error parsing message event response:%v", err)
assert.NotEmpty(t, response.Data, "No message event response created")
+8 -4
View File
@@ -23,7 +23,9 @@ type CreateMessageRequest struct {
func TestCreateMessageWithMock(t *testing.T) {
mockStore := mock.NewMockMessageStore()
handler := NewMessageHandler(mockStore)
apiApp, err := GetApp()
assert.NoError(t, err, "Error getting app")
handler := NewMessageHandler(apiApp, mockStore)
testUserId := uuid.New()
testBody := CreateMessageRequest{Content: "Who could tell?", UserId: testUserId}
@@ -37,7 +39,7 @@ func TestCreateMessageWithMock(t *testing.T) {
assert.Equal(t, http.StatusCreated, rr.Code)
var response AddMessageResponse
err := json.Unmarshal(rr.Body.Bytes(), &response)
err = json.Unmarshal(rr.Body.Bytes(), &response)
assert.NoError(t, err, "Error Creating message %v", err)
assert.NotEmpty(t, response.Data, "No Message created")
@@ -60,13 +62,15 @@ func TestGetMessageWithMock(t *testing.T) {
req, _ := http.NewRequest("GET", url, nil)
rr := httptest.NewRecorder()
messageHandler := NewMessageHandler(mockstore)
apiApp, err := GetApp()
assert.NoError(t, err, "Error getting app")
messageHandler := NewMessageHandler(apiApp, mockstore)
messageHandler.GetMessage(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
var response GetMessageResponse
err := json.Unmarshal(rr.Body.Bytes(), &response)
err = json.Unmarshal(rr.Body.Bytes(), &response)
assert.NoError(t, err, "Error getting message %v", err)
assert.NotEmpty(t, response.Data, "No Message retrieved")
+4 -2
View File
@@ -6,6 +6,7 @@ import (
"git.kundeng.us/phoenix/textsender-models/tx0/message/scheduling"
"github.com/google/uuid"
"git.kundeng.us/phoenix/textsender-api/internal/app"
"git.kundeng.us/phoenix/textsender-api/internal/store"
)
@@ -25,12 +26,13 @@ type ScheduledMessageStatusResponse struct {
}
type ScheduledMessageStatusHandler struct {
App *app.App
ScheduledMessageEventStore store.ScheduledMessageEventStore
ScheduledMessageStore store.ScheduledMessageStore
}
func NewScheduledMessageStatusHandler(str store.ScheduledMessageEventStore, schStore store.ScheduledMessageStore) *ScheduledMessageStatusHandler {
return &ScheduledMessageStatusHandler{ScheduledMessageEventStore: str, ScheduledMessageStore: schStore}
func NewScheduledMessageStatusHandler(apiApp *app.App, str store.ScheduledMessageEventStore, schStore store.ScheduledMessageStore) *ScheduledMessageStatusHandler {
return &ScheduledMessageStatusHandler{App: apiApp, ScheduledMessageEventStore: str, ScheduledMessageStore: schStore}
}
// UpdateStatus godoc
@@ -55,13 +55,15 @@ func TestUpdateScheduledMessageStatusWithMock(t *testing.T) {
req, _ := http.NewRequest("PATCH", endpoint.UpdateScheduledMessageStatusEndpoint, jsonBody)
rr := httptest.NewRecorder()
handler := NewScheduledMessageStatusHandler(schMsgEventStore, schMsgStore)
apiApp, err := GetApp()
assert.NoError(t, err, "Error getting app")
handler := NewScheduledMessageStatusHandler(apiApp, schMsgEventStore, schMsgStore)
handler.UpdateStatus(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
var response ScheduledMessageStatusResponse
err := json.Unmarshal(rr.Body.Bytes(), &response)
err = json.Unmarshal(rr.Body.Bytes(), &response)
assert.NoError(t, err, "Error creating event %v", err)
assert.NotEmpty(t, response.Data, "Data should not be empty")
+4 -2
View File
@@ -9,6 +9,7 @@ import (
"git.kundeng.us/phoenix/textsender-models/tx0/message/scheduling"
"github.com/google/uuid"
"git.kundeng.us/phoenix/textsender-api/internal/app"
"git.kundeng.us/phoenix/textsender-api/internal/store"
)
@@ -34,11 +35,12 @@ type FetchNextMessageResponse struct {
}
type ScheduledMessageHandler struct {
App *app.App
ScheduledMessageStore store.ScheduledMessageStore
}
func NewScheduledMessageHandler(str store.ScheduledMessageStore) *ScheduledMessageHandler {
return &ScheduledMessageHandler{ScheduledMessageStore: str}
func NewScheduledMessageHandler(apiApp *app.App, str store.ScheduledMessageStore) *ScheduledMessageHandler {
return &ScheduledMessageHandler{App: apiApp, ScheduledMessageStore: str}
}
// AddScheduledMessage godoc
+4 -2
View File
@@ -8,6 +8,7 @@ import (
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"git.kundeng.us/phoenix/textsender-api/internal/app"
"git.kundeng.us/phoenix/textsender-api/internal/store"
)
@@ -33,12 +34,13 @@ type DeleteScheduledMessageEventResponse struct {
}
type ScheduledMessageEventHandler struct {
App *app.App
ScheduledMessageEventStore store.ScheduledMessageEventStore
ScheduledMessageStore store.ScheduledMessageStore
}
func NewScheduledMessageEventHandler(str store.ScheduledMessageEventStore, schStore store.ScheduledMessageStore) *ScheduledMessageEventHandler {
return &ScheduledMessageEventHandler{ScheduledMessageEventStore: str, ScheduledMessageStore: schStore}
func NewScheduledMessageEventHandler(apiApp *app.App, str store.ScheduledMessageEventStore, schStore store.ScheduledMessageStore) *ScheduledMessageEventHandler {
return &ScheduledMessageEventHandler{App: apiApp, ScheduledMessageEventStore: str, ScheduledMessageStore: schStore}
}
// AddContact godoc
@@ -40,7 +40,9 @@ func TestCreateScheduledMessageEventWithMock(t *testing.T) {
messageStore := mock.NewMockMessageStore()
schMsgStore := mock.NewMockScheduledMessageStore()
handler := NewScheduledMessageEventHandler(mockStore, schMsgStore)
apiApp, err := GetApp()
assert.NoError(t, err, "Error getting app")
handler := NewScheduledMessageEventHandler(apiApp, mockStore, schMsgStore)
recipientId := uuid.New()
messageId := uuid.New()
@@ -72,7 +74,7 @@ func TestCreateScheduledMessageEventWithMock(t *testing.T) {
assert.Equal(t, http.StatusCreated, rr.Code)
var response AddScheduledMessageEventResponse
err := json.Unmarshal(rr.Body.Bytes(), &response)
err = json.Unmarshal(rr.Body.Bytes(), &response)
assert.NoError(t, err, "Error creating event %v", err)
assert.NotEmpty(t, response.Data, "No event created")
@@ -90,7 +92,9 @@ func TestGetScheduledMessageEventWithMock(t *testing.T) {
messageStore := mock.NewMockMessageStore()
schMsgStore := mock.NewMockScheduledMessageStore()
schMsgEventStore := mock.NewMockScheduledMessageEventStore()
handler := NewScheduledMessageEventHandler(schMsgEventStore, schMsgStore)
apiApp, err := GetApp()
assert.NoError(t, err, "Error getting app")
handler := NewScheduledMessageEventHandler(apiApp, schMsgEventStore, schMsgStore)
recipientId := uuid.New()
messageId := uuid.New()
@@ -123,7 +127,7 @@ func TestGetScheduledMessageEventWithMock(t *testing.T) {
assert.Equal(t, http.StatusOK, rr.Code)
var response GetScheduledMessageEventResponse
err := json.Unmarshal(rr.Body.Bytes(), &response)
err = json.Unmarshal(rr.Body.Bytes(), &response)
assert.NoError(t, err, "Error creating event %v", err)
assert.NotEmpty(t, response.Data, "No event created")
@@ -139,7 +143,9 @@ func TestDeleteScheduledMessageEventWithMock(t *testing.T) {
messageStore := mock.NewMockMessageStore()
schMsgStore := mock.NewMockScheduledMessageStore()
schMsgEventStore := mock.NewMockScheduledMessageEventStore()
handler := NewScheduledMessageEventHandler(schMsgEventStore, schMsgStore)
apiApp, err := GetApp()
assert.NoError(t, err, "Error getting app")
handler := NewScheduledMessageEventHandler(apiApp, schMsgEventStore, schMsgStore)
con := testContact(recipientId, testUserId)
msg := testMessage(messageId, testUserId)
@@ -167,7 +173,7 @@ func TestDeleteScheduledMessageEventWithMock(t *testing.T) {
assert.Equal(t, http.StatusOK, rr.Code)
var response DeleteScheduledMessageEventResponse
err := json.Unmarshal(rr.Body.Bytes(), &response)
err = json.Unmarshal(rr.Body.Bytes(), &response)
assert.NoError(t, err, "Error creating event %v", err)
assert.NotEmpty(t, response.Data, "No event created")
+12 -6
View File
@@ -26,7 +26,9 @@ type CreateScheduledMessageRequest struct {
func TestCreateScheduledMessageWithMock(t *testing.T) {
now := time.Now()
mockStore := mock.NewMockScheduledMessageStore()
handler := NewScheduledMessageHandler(mockStore)
apiApp, err := GetApp()
assert.NoError(t, err, "Error getting app")
handler := NewScheduledMessageHandler(apiApp, mockStore)
testUserId := uuid.New()
testBody := testCreateScheduledMessageRequest(testUserId, now)
@@ -40,7 +42,7 @@ func TestCreateScheduledMessageWithMock(t *testing.T) {
assert.Equal(t, http.StatusCreated, rr.Code)
var response AddScheduledMessageResponse
err := json.Unmarshal(rr.Body.Bytes(), &response)
err = json.Unmarshal(rr.Body.Bytes(), &response)
assert.NoError(t, err, "Error Creating message %v", err)
assert.NotEmpty(t, response.Data, "No Message created")
@@ -68,13 +70,15 @@ func TestGetScheduledMessageWithMock(t *testing.T) {
req, _ := http.NewRequest("GET", url, nil)
rr := httptest.NewRecorder()
handler := NewScheduledMessageHandler(mockStore)
apiApp, err := GetApp()
assert.NoError(t, err, "Error getting app")
handler := NewScheduledMessageHandler(apiApp, mockStore)
handler.GetScheduledMessage(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
var response GetScheduledMessageResponse
err := json.Unmarshal(rr.Body.Bytes(), &response)
err = json.Unmarshal(rr.Body.Bytes(), &response)
assert.NoError(t, err, "Error parsing response %v", err)
}
@@ -114,13 +118,15 @@ func TestFetchScheduledMessageWithMock(t *testing.T) {
req, _ := http.NewRequest("GET", endpoint.FetchNextScheduledMessageEndpoint, nil)
rr := httptest.NewRecorder()
handler := NewScheduledMessageHandler(mockStore)
apiApp, err := GetApp()
assert.NoError(t, err, "Error getting app")
handler := NewScheduledMessageHandler(apiApp, mockStore)
handler.FetchNextMessage(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
var response FetchNextMessageResponse
err := json.Unmarshal(rr.Body.Bytes(), &response)
err = json.Unmarshal(rr.Body.Bytes(), &response)
assert.NoError(t, err, "Error fetching scheduled message: %v", err)
assert.NotEmpty(t, response.Data, "No data")