From d191d6a9c1dbcead36111c65ae845a11bdbef436 Mon Sep 17 00:00:00 2001 From: phoenix Date: Tue, 11 Nov 2025 18:35:04 +0000 Subject: [PATCH] tsk-27: Added endpoint to delete scheduled message event (#31) Closes #27 Reviewed-on: https://git.kundeng.us/phoenix/textsender-api/pulls/31 Co-authored-by: phoenix Co-committed-by: phoenix --- cmd/api/main.go | 3 +- cmd/api/main_test.go | 4 +- internal/handler/endpoint/endpoint.go | 1 + internal/handler/scheduled_message_event.go | 69 +++++++++- .../handler/scheduled_message_event_test.go | 121 +++++++++++++----- .../mock/scheduled_message_event_store.go | 45 ++++++- .../store/scheduled_message_event_store.go | 23 +++- 7 files changed, 221 insertions(+), 45 deletions(-) diff --git a/cmd/api/main.go b/cmd/api/main.go index bf14f3b..a98725a 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -84,7 +84,8 @@ func main() { router.Handle(endpoint.GET_MESSAGE, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(messageHandler.GetMessage))) router.Handle(endpoint.ScheduleMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageHandler.AddScheduledMessage))) router.Handle(endpoint.AddEventToScheduledMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageEventHandler.AddScheduledMessageEvent))) - router.Handle(endpoint.GetScheduledMessageEventEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageEventHandler.GetScheduledMessageEvent))) + router.Method("GET", endpoint.GetScheduledMessageEventEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageEventHandler.GetScheduledMessageEvent))) + router.Method("DELETE", endpoint.DeleteScheduledMessageEventEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageEventHandler.DeleteScheduledMessageEvent))) // Start server server := &http.Server{ diff --git a/cmd/api/main_test.go b/cmd/api/main_test.go index 5150fb5..57ebb0b 100644 --- a/cmd/api/main_test.go +++ b/cmd/api/main_test.go @@ -70,8 +70,8 @@ func TestMain(m *testing.M) { testRouter.Handle(endpoint.ADD_MESSAGE, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(messageHandler.AddMessage))) testRouter.Handle(endpoint.GET_MESSAGE, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(messageHandler.GetMessage))) testRouter.Handle(endpoint.ScheduleMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageHandler.AddScheduledMessage))) - testRouter.Handle(endpoint.AddEventToScheduledMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageEventHandler.AddScheduledMessageEvent))) - testRouter.Handle(endpoint.GetScheduledMessageEventEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageEventHandler.GetScheduledMessageEvent))) + testRouter.Method("GET", endpoint.GetScheduledMessageEventEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageEventHandler.GetScheduledMessageEvent))) + testRouter.Method("DELETE", endpoint.DeleteScheduledMessageEventEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageEventHandler.DeleteScheduledMessageEvent))) code := m.Run() os.Exit(code) diff --git a/internal/handler/endpoint/endpoint.go b/internal/handler/endpoint/endpoint.go index be1aac0..3a68620 100644 --- a/internal/handler/endpoint/endpoint.go +++ b/internal/handler/endpoint/endpoint.go @@ -8,3 +8,4 @@ const ADD_CONTACT_ENDPOINT = "/api/v1/contact/new" const ScheduleMessageEndpoint = "/api/v1/schedule/message" const AddEventToScheduledMessageEndpoint = "/api/v1/schedule/message/event" const GetScheduledMessageEventEndpoint = "/api/v1/schedule/message/event/{id}" +const DeleteScheduledMessageEventEndpoint = "/api/v1/schedule/message/event/{id}" diff --git a/internal/handler/scheduled_message_event.go b/internal/handler/scheduled_message_event.go index 52746f3..ee8984b 100644 --- a/internal/handler/scheduled_message_event.go +++ b/internal/handler/scheduled_message_event.go @@ -27,6 +27,11 @@ type GetScheduledMessageEventResponse struct { Data []scheduling.ScheduledMessageEvent `json:"data"` } +type DeleteScheduledMessageEventResponse struct { + Message string `json:"message"` + Data []scheduling.ScheduledMessageEvent `json:"data"` +} + type ScheduledMessageEventHandler struct { ScheduledMessageEventStore store.ScheduledMessageEventStore ScheduledMessageStore store.ScheduledMessageStore @@ -127,9 +132,67 @@ func (s *ScheduledMessageEventHandler) GetScheduledMessageEvent(w http.ResponseW resp.Message = err.Error() statusCode = http.StatusInternalServerError } else { - resp.Message = "Successful" - statusCode = http.StatusOK - resp.Data = append(resp.Data, *event) + if event != nil { + resp.Message = "Successful" + statusCode = http.StatusOK + resp.Data = append(resp.Data, *event) + } else { + statusCode = http.StatusNotFound + resp.Message = "Scheduled message event not found" + } + } + } + + RespondWithJSON(w, statusCode, &resp) +} + +func (s *ScheduledMessageEventHandler) DeleteScheduledMessageEvent(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodDelete { + http.Error(w, "Metnot allowed", http.StatusMethodNotAllowed) + return + } + + id := chi.URLParam(r, "id") + if len(id) == 0 { + pathParts := strings.Split(r.URL.Path, "/") + if len(pathParts) < 7 { + http.Error(w, "Id not provided", http.StatusBadRequest) + return + } else { + id = pathParts[6] + } + } + + var resp DeleteScheduledMessageEventResponse + var statusCode int + + if parsedId, err := uuid.Parse(id); err != nil { + resp.Message = err.Error() + statusCode = http.StatusBadRequest + } else { + ctx := r.Context() + if event, err := s.ScheduledMessageEventStore.Get(ctx, parsedId); err != nil { + resp.Message = err.Error() + statusCode = http.StatusInternalServerError + } else { + if schMsg, err := s.ScheduledMessageStore.Get(ctx, event.ScheduledMessageId); err != nil { + resp.Message = err.Error() + statusCode = http.StatusInternalServerError + } else { + if schMsg.Status == scheduling.Pending { + if err = s.ScheduledMessageEventStore.Delete(ctx, event.Id); err != nil { + resp.Message = err.Error() + statusCode = http.StatusInternalServerError + } else { + resp.Message = "Successful" + resp.Data = append(resp.Data, *event) + statusCode = http.StatusOK + } + } else { + statusCode = http.StatusBadRequest + resp.Message = "Invalid status" + } + } } } diff --git a/internal/handler/scheduled_message_event_test.go b/internal/handler/scheduled_message_event_test.go index cdee1d9..82fd742 100644 --- a/internal/handler/scheduled_message_event_test.go +++ b/internal/handler/scheduled_message_event_test.go @@ -18,6 +18,13 @@ import ( "git.kundeng.us/phoenix/textsender-api/internal/store/mock" ) +var ( + recipientId = uuid.New() + messageId = uuid.New() + scheduledMessageId = uuid.New() + testUserId = uuid.New() +) + type CreateScheduledMessageEventRequest struct { RecipientId uuid.UUID `json:"recipient_id"` MessageId uuid.UUID `json:"message_id"` @@ -39,21 +46,9 @@ func TestCreateScheduledMessageEventWithMock(t *testing.T) { scheduledMessageId := uuid.New() testUserId := uuid.New() - con := contact.Contact{} - con.Id = recipientId - con.PhoneNumber = "+10123456789" - con.UserId = testUserId - - msg := message.Message{} - msg.Id = messageId - msg.Content = "Oh how the might have fallen" - msg.UserId = testUserId - - schMsg := scheduling.ScheduledMessage{} - schMsg.Id = scheduledMessageId - schMsg.UserId = testUserId - schMsg.Scheduled = now.Add(20 * time.Minute) - schMsg.Status = scheduling.Pending + con := testContact(recipientId, testUserId) + msg := testMessage(messageId, testUserId) + schMsg := testScheduledMessage(scheduledMessageId, testUserId, now) ctx := t.Context() @@ -101,26 +96,10 @@ func TestGetScheduledMessageEventWithMock(t *testing.T) { scheduledMessageId := uuid.New() testUserId := uuid.New() - con := contact.Contact{} - con.Id = recipientId - con.PhoneNumber = "+10123456789" - con.UserId = testUserId - - msg := message.Message{} - msg.Id = messageId - msg.Content = "Oh how the might have fallen" - msg.UserId = testUserId - - schMsg := scheduling.ScheduledMessage{} - schMsg.Id = scheduledMessageId - schMsg.UserId = testUserId - schMsg.Scheduled = now.Add(20 * time.Minute) - schMsg.Status = scheduling.Pending - - event := scheduling.ScheduledMessageEvent{} - event.MessageId = msg.Id - event.RecipientId = con.Id - event.ScheduledMessageId = schMsg.Id + con := testContact(recipientId, testUserId) + msg := testMessage(messageId, testUserId) + schMsg := testScheduledMessage(scheduledMessageId, testUserId, now) + event := testScheduledMessageEvent(msg.Id, con.Id, schMsg.Id) ctx := t.Context() @@ -152,3 +131,75 @@ func TestGetScheduledMessageEventWithMock(t *testing.T) { assert.NotEmpty(t, msgEvent.Created, "Created date should not be empty") } + +func TestDeleteScheduledMessageEventWithMock(t *testing.T) { + now := time.Now() + contactStore := mock.NewMockContactStore() + messageStore := mock.NewMockMessageStore() + schMsgStore := mock.NewMockScheduledMessageStore() + schMsgEventStore := mock.NewMockScheduledMessageEventStore() + handler := NewScheduledMessageEventHandler(schMsgEventStore, schMsgStore) + + con := testContact(recipientId, testUserId) + msg := testMessage(messageId, testUserId) + schMsg := testScheduledMessage(scheduledMessageId, testUserId, now) + event := testScheduledMessageEvent(msg.Id, con.Id, schMsg.Id) + + ctx := t.Context() + + if err := contactStore.CreateContact(ctx, &con); err != nil { + assert.NoError(t, err, "Error creating contact: %v", err) + } else if err = messageStore.CreateMessage(ctx, &msg); err != nil { + assert.NoError(t, err, "Error creating message: %v", err) + } else if err = schMsgStore.CreateScheduledMessage(ctx, &schMsg); err != nil { + assert.NoError(t, err, "Error creating scheduled message: %v", err) + } else if err = schMsgEventStore.CreateScheduledMessageEvent(ctx, &event); err != nil { + assert.NoError(t, err, "Error creating scheduled message event: %v", err) + } + + endpointValue := strings.Replace(endpoint.DeleteScheduledMessageEventEndpoint, "{id}", event.Id.String(), 1) + req, _ := http.NewRequest("DELETE", endpointValue, nil) + rr := httptest.NewRecorder() + + handler.DeleteScheduledMessageEvent(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + + var response DeleteScheduledMessageEventResponse + err := json.Unmarshal(rr.Body.Bytes(), &response) + assert.NoError(t, err, "Error creating event %v", err) + + assert.NotEmpty(t, response.Data, "No event created") + + msgEvent := response.Data[0] + + assert.NotEmpty(t, msgEvent.Created, "Created date should not be empty") +} + +func testContact(id uuid.UUID, userId uuid.UUID) contact.Contact { + if id == uuid.Nil { + return contact.Contact{Id: uuid.New(), PhoneNumber: "+10123456789", UserId: userId} + } else { + return contact.Contact{Id: id, PhoneNumber: "+10123456789", UserId: userId} + } +} + +func testMessage(id uuid.UUID, userId uuid.UUID) message.Message { + if id == uuid.Nil { + return message.Message{Id: uuid.New(), Content: "Oh how the mighty have fallen", UserId: userId} + } else { + return message.Message{Id: id, Content: "Oh how the mighty have fallen", UserId: userId} + } +} + +func testScheduledMessage(id uuid.UUID, userId uuid.UUID, now time.Time) scheduling.ScheduledMessage { + if id == uuid.Nil { + return scheduling.ScheduledMessage{Id: uuid.New(), UserId: userId, Scheduled: now.Add(20 * time.Minute), Status: scheduling.Pending} + } else { + return scheduling.ScheduledMessage{Id: id, UserId: userId, Scheduled: now.Add(20 * time.Minute), Status: scheduling.Pending} + } +} + +func testScheduledMessageEvent(messageId, recipientId, scheduledMessageId uuid.UUID) scheduling.ScheduledMessageEvent { + return scheduling.ScheduledMessageEvent{MessageId: messageId, RecipientId: recipientId, ScheduledMessageId: scheduledMessageId} +} diff --git a/internal/store/mock/scheduled_message_event_store.go b/internal/store/mock/scheduled_message_event_store.go index 60e287c..a7e06ae 100644 --- a/internal/store/mock/scheduled_message_event_store.go +++ b/internal/store/mock/scheduled_message_event_store.go @@ -46,11 +46,54 @@ func (m *MockScheduledMessageEventStore) Get(ctx context.Context, id uuid.UUID) if _, exists := m.ScheduledMessageEvents[id]; exists { return m.ScheduledMessageEvents[id], nil } else { - fmt.Println("Not found") return nil, fmt.Errorf("Not found") } } +func (m *MockScheduledMessageEventStore) Delete(ctx context.Context, id uuid.UUID) error { + m.mu.Lock() + defer m.mu.Unlock() + + if m.Error != nil { + return m.Error + } + + if id == uuid.Nil { + return fmt.Errorf("Id is nil") + } + + if _, exists := m.ScheduledMessageEvents[id]; exists { + originalAmount := len(m.ScheduledMessageEvents) + copiedEvents := make(map[uuid.UUID]*scheduling.ScheduledMessageEvent) + copiedEventsKey := make(map[ScheduledMessageEventKey]*scheduling.ScheduledMessageEvent) + + for i, schMsgEvent := range m.ScheduledMessageEvents { + key := ScheduledMessageEventKey{RecipientId: schMsgEvent.RecipientId, MessageId: schMsgEvent.MessageId, ScheduledMessageId: schMsgEvent.ScheduledMessageId} + if schMsgEvent.Id != id { + copiedEvents[i] = schMsgEvent + copiedEventsKey[key] = schMsgEvent + } + } + + if originalAmount > 1 { + if len(copiedEvents) > 0 && len(copiedEventsKey) > 0 { + m.ScheduledMessageEvents = copiedEvents + m.ScheduledMessageEventsByKey = copiedEventsKey + return nil + } else { + return fmt.Errorf("Not removed") + } + } else { + m.ScheduledMessageEvents = make(map[uuid.UUID]*scheduling.ScheduledMessageEvent) + m.ScheduledMessageEventsByKey = make(map[ScheduledMessageEventKey]*scheduling.ScheduledMessageEvent) + + return nil + } + } else { + return fmt.Errorf("Not found") + } +} + func (m *MockScheduledMessageEventStore) CreateScheduledMessageEvent(ctx context.Context, event *scheduling.ScheduledMessageEvent) error { m.mu.Lock() defer m.mu.Unlock() diff --git a/internal/store/scheduled_message_event_store.go b/internal/store/scheduled_message_event_store.go index ce2ab26..5694692 100644 --- a/internal/store/scheduled_message_event_store.go +++ b/internal/store/scheduled_message_event_store.go @@ -13,6 +13,7 @@ import ( type ScheduledMessageEventStore interface { Get(ctx context.Context, id uuid.UUID) (*scheduling.ScheduledMessageEvent, error) + Delete(ctx context.Context, id uuid.UUID) error CreateScheduledMessageEvent(ctx context.Context, event *scheduling.ScheduledMessageEvent) error Exists(ctx context.Context, event *scheduling.ScheduledMessageEvent) (bool, error) } @@ -35,12 +36,28 @@ func (s *PGScheduledMessageEventStore) Get(ctx context.Context, id uuid.UUID) (* if err == pgx.ErrNoRows { return nil, nil - } - if err != nil { + } else if err != nil { return nil, fmt.Errorf("getting scheduled message event by ID: %w", err) + } else { + return &event, nil + } +} + +func (s *PGScheduledMessageEventStore) Delete(ctx context.Context, id uuid.UUID) error { + query := ` + DELETE FROM scheduled_message_events WHERE id = $1 + ` + + commandTag, err := s.db.Exec(ctx, query, id) + if err != nil { + return fmt.Errorf("error deleting event: %w", err) } - return &event, nil + if commandTag.RowsAffected() == 0 { + return fmt.Errorf("no event found with id %d", id) + } else { + return nil + } } func (s *PGScheduledMessageEventStore) CreateScheduledMessageEvent(ctx context.Context, event *scheduling.ScheduledMessageEvent) error {