tsk-9: Schedule queued message endpoint (#22)
Closes #9 Reviewed-on: phoenix/textsender-api#22 Co-authored-by: phoenix <kundeng00@pm.me> Co-committed-by: phoenix <kundeng00@pm.me>
This commit is contained in:
@@ -63,9 +63,11 @@ func main() {
|
|||||||
|
|
||||||
contactStore := store.NewContactStore(db.Pool)
|
contactStore := store.NewContactStore(db.Pool)
|
||||||
messageStore := store.NewMessageStore(db.Pool)
|
messageStore := store.NewMessageStore(db.Pool)
|
||||||
|
schStore := store.NewScheduledMessageStore(db.Pool)
|
||||||
|
|
||||||
contactHandler := handler.NewContactHandler(contactStore)
|
contactHandler := handler.NewContactHandler(contactStore)
|
||||||
messageHandler := handler.NewMessageHandler(messageStore)
|
messageHandler := handler.NewMessageHandler(messageStore)
|
||||||
|
scheduledMessageHandler := handler.NewScheduledMessageHandler(schStore)
|
||||||
|
|
||||||
router := chi.NewRouter()
|
router := chi.NewRouter()
|
||||||
|
|
||||||
@@ -78,6 +80,7 @@ func main() {
|
|||||||
router.Handle(endpoint.GET_CONTACT, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(contactHandler.GetContact)))
|
router.Handle(endpoint.GET_CONTACT, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(contactHandler.GetContact)))
|
||||||
router.Handle(endpoint.ADD_MESSAGE, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(messageHandler.AddMessage)))
|
router.Handle(endpoint.ADD_MESSAGE, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(messageHandler.AddMessage)))
|
||||||
router.Handle(endpoint.GET_MESSAGE, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(messageHandler.GetMessage)))
|
router.Handle(endpoint.GET_MESSAGE, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(messageHandler.GetMessage)))
|
||||||
|
router.Handle(endpoint.ScheduleMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageHandler.AddScheduledMessage)))
|
||||||
|
|
||||||
// Start server
|
// Start server
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
|
|||||||
@@ -2,12 +2,11 @@ module git.kundeng.us/phoenix/textsender-api
|
|||||||
|
|
||||||
go 1.25.3
|
go 1.25.3
|
||||||
|
|
||||||
require github.com/google/uuid v1.6.0
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.kundeng.us/phoenix/textsender-models v0.0.4-2-2f78d9b1e6-556
|
git.kundeng.us/phoenix/textsender-models v0.0.5-3-ca38ce876f-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/google/uuid v1.6.0
|
||||||
github.com/gorilla/mux v1.8.1
|
github.com/gorilla/mux v1.8.1
|
||||||
github.com/jackc/pgx/v5 v5.7.6
|
github.com/jackc/pgx/v5 v5.7.6
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
git.kundeng.us/phoenix/textsender-models v0.0.4-2-2f78d9b1e6-556 h1:BRF4JrIVZMa3kZOaRfHNLJTcspD1RWRoCTcabn+CAIA=
|
git.kundeng.us/phoenix/textsender-models v0.0.5-3-ca38ce876f-556 h1:dDgjre2FlukxeKZfJiP3gtIT4wyzpQ8glAYV9LpLtuU=
|
||||||
git.kundeng.us/phoenix/textsender-models v0.0.4-2-2f78d9b1e6-556/go.mod h1:lx5MCnOgGgsdpwzrfi9uph5xmkeb6H8AuexUNGss2no=
|
git.kundeng.us/phoenix/textsender-models v0.0.5-3-ca38ce876f-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=
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ const ADD_MESSAGE = "/api/v1/message/new"
|
|||||||
const GET_MESSAGE = "/api/v1/message"
|
const GET_MESSAGE = "/api/v1/message"
|
||||||
const GET_CONTACT = "/api/v1/contact"
|
const GET_CONTACT = "/api/v1/contact"
|
||||||
const ADD_CONTACT_ENDPOINT = "/api/v1/contact/new"
|
const ADD_CONTACT_ENDPOINT = "/api/v1/contact/new"
|
||||||
|
const ScheduleMessageEndpoint = "/api/v1/schedule/message"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"git.kundeng.us/phoenix/textsender-models/pkg/contact"
|
"git.kundeng.us/phoenix/textsender-models/pkg/contact"
|
||||||
"git.kundeng.us/phoenix/textsender-models/pkg/message"
|
"git.kundeng.us/phoenix/textsender-models/pkg/message"
|
||||||
|
"git.kundeng.us/phoenix/textsender-models/pkg/message/scheduling"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Key struct {
|
type Key struct {
|
||||||
@@ -200,3 +201,40 @@ func (m *MockMessageStore) MessageExists(ctx context.Context, msg *message.Messa
|
|||||||
|
|
||||||
return exists, nil
|
return exists, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ScheduledMessageKey struct {
|
||||||
|
UserId uuid.UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockScheduledMessageStore struct {
|
||||||
|
ScheduledMessages map[uuid.UUID]*scheduling.ScheduledMessage
|
||||||
|
ScheduledMessagesByKey map[ScheduledMessageKey]*scheduling.ScheduledMessage
|
||||||
|
mu sync.RWMutex
|
||||||
|
Error error // Optional: simulate errors
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMockScheduledMessageStore() *MockScheduledMessageStore {
|
||||||
|
return &MockScheduledMessageStore{
|
||||||
|
ScheduledMessages: make(map[uuid.UUID]*scheduling.ScheduledMessage),
|
||||||
|
ScheduledMessagesByKey: make(map[ScheduledMessageKey]*scheduling.ScheduledMessage),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockScheduledMessageStore) CreateScheduledMessage(ctx context.Context, schedMsg *scheduling.ScheduledMessage) error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
if m.Error != nil {
|
||||||
|
return m.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
if schedMsg.Id == uuid.Nil {
|
||||||
|
schedMsg.Id = uuid.New()
|
||||||
|
}
|
||||||
|
|
||||||
|
key := ScheduledMessageKey{UserId: schedMsg.UserId}
|
||||||
|
|
||||||
|
m.ScheduledMessages[schedMsg.Id] = schedMsg
|
||||||
|
m.ScheduledMessagesByKey[key] = schedMsg
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
|
"git.kundeng.us/phoenix/textsender-api/internal/store"
|
||||||
|
"git.kundeng.us/phoenix/textsender-models/pkg/message/scheduling"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RequestAddScheduledMessage struct {
|
||||||
|
Scheduled time.Time `json:"scheduled"`
|
||||||
|
Created time.Time `json:"created"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
UserId uuid.UUID `json:"user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddScheduledMessageResponse struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data []scheduling.ScheduledMessage `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScheduledMessageHandler struct {
|
||||||
|
ScheduledMessageStore store.ScheduledMessageStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewScheduledMessageHandler(str store.ScheduledMessageStore) *ScheduledMessageHandler {
|
||||||
|
return &ScheduledMessageHandler{ScheduledMessageStore: str}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ScheduledMessageHandler) AddScheduledMessage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req RequestAddScheduledMessage
|
||||||
|
if err := ExtractFromRequest(r, &req); err != nil {
|
||||||
|
http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
scheduledMessage := scheduling.ScheduledMessage{Scheduled: req.Scheduled, Status: req.Status, UserId: req.UserId}
|
||||||
|
|
||||||
|
var statusCode int
|
||||||
|
var resp AddScheduledMessageResponse
|
||||||
|
|
||||||
|
if scheduledMessage.UserId == uuid.Nil {
|
||||||
|
statusCode = http.StatusBadRequest
|
||||||
|
resp.Message = "No UserId"
|
||||||
|
} else {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
if validStatus, err := isStatusValid(&scheduledMessage); err == nil {
|
||||||
|
if validStatus {
|
||||||
|
if valid, err := isScheduledTimeValid(&scheduledMessage); err == nil && valid {
|
||||||
|
if err = c.ScheduledMessageStore.CreateScheduledMessage(ctx, &scheduledMessage); err == nil {
|
||||||
|
statusCode = http.StatusCreated
|
||||||
|
resp.Data = append(resp.Data, scheduledMessage)
|
||||||
|
resp.Message = "Successful"
|
||||||
|
} else {
|
||||||
|
statusCode = http.StatusInternalServerError
|
||||||
|
resp.Message = err.Error()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
statusCode = http.StatusBadRequest
|
||||||
|
resp.Message = "Invalid scheduled time"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
statusCode = http.StatusBadRequest
|
||||||
|
resp.Message = "Invalid status"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
statusCode = http.StatusInternalServerError
|
||||||
|
resp.Message = err.Error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RespondWithJSON(w, statusCode, &resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isScheduledTimeValid(schMsg *scheduling.ScheduledMessage) (bool, error) {
|
||||||
|
now := time.Now()
|
||||||
|
timeCutOff := now.Add(-5 * time.Minute)
|
||||||
|
futureCutOff := now.Add((24 * 7) * time.Hour)
|
||||||
|
|
||||||
|
if schMsg.Scheduled.After(timeCutOff) {
|
||||||
|
if schMsg.Scheduled.Before(futureCutOff) {
|
||||||
|
return true, nil
|
||||||
|
} else {
|
||||||
|
return false, fmt.Errorf("Scheduled Time is too far in the future")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isStatusValid(schMsg *scheduling.ScheduledMessage) (bool, error) {
|
||||||
|
schMsg.Status = strings.ToUpper(schMsg.Status)
|
||||||
|
if schMsg.Status == scheduling.Pending {
|
||||||
|
return true, nil
|
||||||
|
} else {
|
||||||
|
return false, fmt.Errorf("Status is not valid for creating scheduled message %s", schMsg.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.kundeng.us/phoenix/textsender-models/pkg/message/scheduling"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"git.kundeng.us/phoenix/textsender-api/internal/handler/endpoint"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CreateScheduledMessageRequest struct {
|
||||||
|
Scheduled time.Time `json:"scheduled"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
UserId uuid.UUID `json:"user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateScheduledMessageWithMock(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
mockStore := NewMockScheduledMessageStore()
|
||||||
|
handler := NewScheduledMessageHandler(mockStore)
|
||||||
|
|
||||||
|
testUserId := uuid.New()
|
||||||
|
scheduled := now.Add(5 * time.Minute)
|
||||||
|
testBody := CreateScheduledMessageRequest{Status: scheduling.Pending, UserId: testUserId, Scheduled: scheduled}
|
||||||
|
jsonValue, _ := json.Marshal(testBody)
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("POST", endpoint.ScheduleMessageEndpoint, strings.NewReader(string(jsonValue)))
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.AddScheduledMessage(rr, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusCreated, rr.Code)
|
||||||
|
|
||||||
|
var response AddScheduledMessageResponse
|
||||||
|
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")
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
// "fmt"
|
||||||
|
|
||||||
|
// "github.com/google/uuid"
|
||||||
|
// "github.com/jackc/pgx/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
|
||||||
|
"git.kundeng.us/phoenix/textsender-models/pkg/message/scheduling"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ScheduledMessageStore interface {
|
||||||
|
CreateScheduledMessage(ctx context.Context, schedMsg *scheduling.ScheduledMessage) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type PGScheduledMessageStore struct {
|
||||||
|
db *pgxpool.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewScheduledMessageStore(db *pgxpool.Pool) *PGScheduledMessageStore {
|
||||||
|
return &PGScheduledMessageStore{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PGScheduledMessageStore) CreateScheduledMessage(ctx context.Context, schedMsg *scheduling.ScheduledMessage) error {
|
||||||
|
query := `
|
||||||
|
INSERT INTO scheduled_messages (scheduled, status, user_id)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
RETURNING id, created
|
||||||
|
`
|
||||||
|
|
||||||
|
return s.db.QueryRow(ctx, query, schedMsg.Scheduled, schedMsg.Status, schedMsg.UserId).Scan(
|
||||||
|
&schedMsg.Id, &schedMsg.Created,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -13,3 +13,11 @@ CREATE TABLE messages (
|
|||||||
content TEXT NOT NULL,
|
content TEXT NOT NULL,
|
||||||
user_id UUID NOT NULL
|
user_id UUID NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE scheduled_messages (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
scheduled timestamptz NOT NULL,
|
||||||
|
created timestamptz DEFAULT now(),
|
||||||
|
status TEXT CHECK (status IN ('PENDING', 'READY', 'PROCESSING', 'DONE')),
|
||||||
|
user_id UUID NOT NULL
|
||||||
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user