34 Commits
Author SHA1 Message Date
phoenix cac1a9d97d bug_fix (#64)
Reviewed-on: phoenix/textsender-api#64
2026-01-01 21:04:28 +00:00
phoenix a33af13f45 textsender-models and swoosh version bump 2026-01-01 16:00:49 -05:00
phoenix 0e956da330 Missed another one 2026-01-01 14:33:44 -05:00
phoenix a0bab48f40 Could it be? 2026-01-01 14:28:55 -05:00
phoenix 400490b633 Whoops 2026-01-01 14:19:36 -05:00
phoenix b33c05531b Test change 2026-01-01 14:18:56 -05:00
phoenix 08650619dc Minor refactor 2026-01-01 14:11:44 -05:00
phoenix 161d0da377 Another one 2026-01-01 14:09:20 -05:00
phoenix 0334c67c36 Fix another test issue 2026-01-01 14:07:09 -05:00
phoenix 49f5e0ea40 Fix build issue 2026-01-01 14:04:06 -05:00
phoenix f73c959830 More test changes 2026-01-01 14:01:58 -05:00
phoenix 57619759d5 Test changes 2026-01-01 13:56:51 -05:00
phoenixandphoenix 59c9aa0ff1 tsk-59: Get MessageEventResponse endpoint (#61)
Closes #59

Reviewed-on: phoenix/textsender-api#61
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2026-01-01 05:28:49 +00:00
phoenixandphoenix 6641f384ab tsk-62: Modify how the Response from event.MessageEventResponse is handled (#63)
Closes #62

Reviewed-on: phoenix/textsender-api#63
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2026-01-01 01:39:12 +00:00
phoenixandphoenix 8860d6c801 tsk-57: Change schema of scheduled_message_events (#65)
Closes #57

Reviewed-on: phoenix/textsender-api#65
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2026-01-01 00:59:14 +00:00
phoenix 29f8c943a6 tsk-59: Tweaking code 2025-12-31 15:11:04 -05:00
phoenix 996b67d890 tsk-59: Made endpoint available 2025-12-31 14:49:49 -05:00
phoenix b58174eef2 tsk-59: Got the code for MER and added TODO 2025-12-31 14:44:43 -05:00
phoenix 1a3a244c07 tsk-59: Changes to store and mock package 2025-12-31 14:28:09 -05:00
phoenix aefa26199d tsk-59: Added endpoint constant and made changes to handler 2025-12-30 19:30:23 -05:00
phoenix 8728fe9b60 tsk-59: Added code to get MessageEventResponse 2025-12-30 19:13:51 -05:00
phoenix d3ce3583a2 changes (#55)
Reviewed-on: phoenix/textsender-api#55
2025-12-22 22:54:40 +00:00
phoenix 14f50a118f Test fixes 2025-12-22 17:52:45 -05:00
phoenix fb17c02f69 Code formatting 2025-12-22 17:48:37 -05:00
phoenix 0062811cb6 Modified mock package 2025-12-22 17:48:07 -05:00
phoenix 84fed38a67 Added mock Contact store 2025-12-22 17:47:51 -05:00
phoenixandphoenix 6fa5ede358 tsk-54: Tweak instant message endpoint to save contact and message identification (#56)
Closes #54

Reviewed-on: phoenix/textsender-api#56
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-12-22 22:39:29 +00:00
phoenixandphoenix f5f753fe02 tsk-4: Adding CORS support (#53)
Closes #4

Reviewed-on: phoenix/textsender-api#53
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-12-21 00:28:17 +00:00
phoenix 69b5f4ff89 tsk-4: Formatted code 2025-12-13 15:30:09 -05:00
phoenix 09744d4cdf tsk-4: Fixed code 2025-12-13 15:29:54 -05:00
phoenix fa8c7062fc tsk-4: Switched cors package 2025-12-13 15:29:39 -05:00
phoenix fd8fdd93b9 tsk-4: Adding CORS support 2025-12-12 13:59:04 -05:00
phoenix fbaa95b2e9 tsk-4: Added cors package 2025-12-12 13:57:55 -05:00
phoenix 37dcc437de tsk-4: Adding cors package 2025-12-11 17:37:07 -05:00
34 changed files with 1209 additions and 443 deletions
+1
View File
@@ -9,3 +9,4 @@ TWILIO_AUTH_SID=9M438C93R943U4329MCU43C34U
TWILIO_SERVICE_SID=9M4J3X8439U398NUVT3342MC349C348T TWILIO_SERVICE_SID=9M4J3X8439U398NUVT3342MC349C348T
TWILIO_AUTH_TOKEN="f4a1f2b0b79ea3735078c2d8ee9684e1" TWILIO_AUTH_TOKEN="f4a1f2b0b79ea3735078c2d8ee9684e1"
TWILIO_PHONE_NUMBER=+10123456789 TWILIO_PHONE_NUMBER=+10123456789
ALLOWED_ORIGINS="http://textsender.com"
+1
View File
@@ -9,3 +9,4 @@ TWILIO_AUTH_SID=9M438C93R943U4329MCU43C34U
TWILIO_SERVICE_SID=9M4J3X8439U398NUVT3342MC349C348T TWILIO_SERVICE_SID=9M4J3X8439U398NUVT3342MC349C348T
TWILIO_AUTH_TOKEN="f4a1f2b0b79ea3735078c2d8ee9684e1" TWILIO_AUTH_TOKEN="f4a1f2b0b79ea3735078c2d8ee9684e1"
TWILIO_PHONE_NUMBER=+10123456789 TWILIO_PHONE_NUMBER=+10123456789
ALLOWED_ORIGINS="http://textsender.com"
+1
View File
@@ -107,6 +107,7 @@ jobs:
echo "TWILIO_SERVICE_SID=9M4J3X8439U398NUVT3342MC349C348T" >> .env echo "TWILIO_SERVICE_SID=9M4J3X8439U398NUVT3342MC349C348T" >> .env
echo "TWILIO_AUTH_TOKEN=f4a1f2b0b79ea3735078c2d8ee9684e1" >> .env echo "TWILIO_AUTH_TOKEN=f4a1f2b0b79ea3735078c2d8ee9684e1" >> .env
echo "TWILIO_PHONE_NUMBER=10123456789" >> .env echo "TWILIO_PHONE_NUMBER=10123456789" >> .env
echo "ALLOWED_ORIGINS=http://localhost:5173" >> .env
echo "Initializing config" echo "Initializing config"
mkdir -p ~/.ssh mkdir -p ~/.ssh
+11
View File
@@ -12,6 +12,7 @@ import (
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware" "github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
"github.com/swaggo/http-swagger/v2" "github.com/swaggo/http-swagger/v2"
_ "git.kundeng.us/phoenix/textsender-api/docs" _ "git.kundeng.us/phoenix/textsender-api/docs"
@@ -92,6 +93,15 @@ func main() {
router := chi.NewRouter() router := chi.NewRouter()
// Configure CORS
router.Use(cors.Handler(cors.Options{
AllowedOrigins: cfg.AllowedOrigins,
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
ExposedHeaders: []string{"Link", "X-Total-Count"},
AllowCredentials: true,
MaxAge: 300, // 5 minutes
}))
router.Use(middleware.Logger) router.Use(middleware.Logger)
router.Use(middleware.Recoverer) router.Use(middleware.Recoverer)
router.Use(middleware.Timeout(60 * time.Second)) router.Use(middleware.Timeout(60 * time.Second))
@@ -109,6 +119,7 @@ func main() {
router.Method("GET", endpoint.GetScheduledMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageHandler.GetScheduledMessage))) router.Method("GET", endpoint.GetScheduledMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageHandler.GetScheduledMessage)))
router.Method("GET", endpoint.FetchNextScheduledMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageHandler.FetchNextMessage))) router.Method("GET", endpoint.FetchNextScheduledMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageHandler.FetchNextMessage)))
router.Method("POST", endpoint.RecordEventResponse, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(eventHandler.RecordResponse))) router.Method("POST", endpoint.RecordEventResponse, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(eventHandler.RecordResponse)))
router.Method("GET", endpoint.FetchMessageEventResponse, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(eventHandler.Fetch)))
router.Method("POST", endpoint.SendInstantMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(instantMessageHandler.Send))) router.Method("POST", endpoint.SendInstantMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(instantMessageHandler.Send)))
router.Method("GET", "/swagger/*", httpSwagger.Handler( router.Method("GET", "/swagger/*", httpSwagger.Handler(
+10 -8
View File
@@ -3,7 +3,6 @@ package main
import ( import (
"context" "context"
"flag" "flag"
"fmt"
"log" "log"
"net/http" "net/http"
"os" "os"
@@ -30,8 +29,8 @@ func TestMain(m *testing.M) {
database, err := db.NewDatabase(cfg.GetDBConnString()) database, err := db.NewDatabase(cfg.GetDBConnString())
if err != nil { if err != nil {
fmt.Println(err.Error()) log.Println(err.Error())
panic("Failed to initialize database") log.Fatal("Failed to initialize database")
} }
defer database.Close() defer database.Close()
@@ -39,18 +38,18 @@ func TestMain(m *testing.M) {
if cfg.ResetDB { if cfg.ResetDB {
if err = database.ResetDatabase(ctx); err != nil { if err = database.ResetDatabase(ctx); err != nil {
fmt.Println(err.Error()) log.Println(err.Error())
panic("Failed to initialize database") log.Fatalln("Failed to initialize database")
} }
} else { } else {
if exists, err := db.TableExists(ctx, database.Pool, "contacts "); err == nil && !exists { if exists, err := db.TableExists(ctx, database.Pool, "contacts "); err == nil && !exists {
fmt.Println("Resetting database") log.Println("Resetting database")
err = database.ResetDatabase(ctx) err = database.ResetDatabase(ctx)
if err != nil { if err != nil {
fmt.Printf("Error:%v", err) log.Println("Error:", err)
} }
} else { } else {
fmt.Printf("Error:%v", err) log.Println("Error:", err)
} }
} }
@@ -68,6 +67,7 @@ func TestMain(m *testing.M) {
scheduledMessageEventHandler := handler.NewScheduledMessageEventHandler(apiApp, schMsgEventStore, schStore) scheduledMessageEventHandler := handler.NewScheduledMessageEventHandler(apiApp, schMsgEventStore, schStore)
scheduledMessageStatusHandler := handler.NewScheduledMessageStatusHandler(apiApp, schMsgEventStore, schStore) scheduledMessageStatusHandler := handler.NewScheduledMessageStatusHandler(apiApp, schMsgEventStore, schStore)
eventHandler := handler.NewEventResponseHandler(apiApp, merStore) eventHandler := handler.NewEventResponseHandler(apiApp, merStore)
instantMessageHandler := handler.NewSendInstantMessageHandler(apiApp, merStore, contactStore, messageStore)
testRouter = chi.NewRouter() testRouter = chi.NewRouter()
testRouter.Handle(endpoint.ADD_CONTACT_ENDPOINT, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(contactHandler.AddContact))) testRouter.Handle(endpoint.ADD_CONTACT_ENDPOINT, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(contactHandler.AddContact)))
@@ -81,6 +81,8 @@ func TestMain(m *testing.M) {
testRouter.Method("GET", endpoint.GetScheduledMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageHandler.GetScheduledMessage))) testRouter.Method("GET", endpoint.GetScheduledMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageHandler.GetScheduledMessage)))
testRouter.Method("GET", endpoint.FetchNextScheduledMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageHandler.FetchNextMessage))) testRouter.Method("GET", endpoint.FetchNextScheduledMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageHandler.FetchNextMessage)))
testRouter.Method("POST", endpoint.RecordEventResponse, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(eventHandler.RecordResponse))) testRouter.Method("POST", endpoint.RecordEventResponse, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(eventHandler.RecordResponse)))
testRouter.Method("GET", endpoint.FetchMessageEventResponse, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(eventHandler.Fetch)))
testRouter.Method("POST", endpoint.SendInstantMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(instantMessageHandler.Send)))
code := m.Run() code := m.Run()
os.Exit(code) os.Exit(code)
+200 -29
View File
@@ -490,6 +490,106 @@ const docTemplate = `{
} }
} }
}, },
"/schedule/message/event/response": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Fetches a MessageEventResponse given a user id (requires JWT)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"message"
],
"summary": "Fetcht MessageEventResponse",
"parameters": [
{
"type": "string",
"description": "User Id",
"name": "user_id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.GetMessageEventResponseFetchedResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handler.GetMessageEventResponseFetchedResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handler.GetMessageEventResponseFetchedResponse"
}
}
}
}
},
"/schedule/message/event/response/record": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Saves the result of sending a text message (requires JWT)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"message"
],
"summary": "Record MessageEventResponse",
"parameters": [
{
"description": "Data to add MessageEventResponse",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handler.RecordEventRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/handler.RecordEventResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handler.RecordEventResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handler.RecordEventResponse"
}
}
}
}
},
"/schedule/message/event/{id}": { "/schedule/message/event/{id}": {
"delete": { "delete": {
"security": [ "security": [
@@ -642,9 +742,18 @@ const docTemplate = `{
"contact.Contact": { "contact.Contact": {
"type": "object", "type": "object",
"properties": { "properties": {
"first_name": {
"type": "string"
},
"id": { "id": {
"type": "string" "type": "string"
}, },
"last_name": {
"type": "string"
},
"nickname": {
"type": "string"
},
"phone_number": { "phone_number": {
"type": "string" "type": "string"
}, },
@@ -653,6 +762,36 @@ const docTemplate = `{
} }
} }
}, },
"event.MessageEventResponse": {
"type": "object",
"properties": {
"contact_id": {
"type": "string"
},
"id": {
"type": "string"
},
"message_id": {
"type": "string"
},
"response": {
"type": "object",
"additionalProperties": {}
},
"scheduled_message_event_id": {
"type": "string"
},
"sent": {
"type": "string"
},
"status": {
"type": "string"
},
"user_id": {
"type": "string"
}
}
},
"handler.AddContactResponse": { "handler.AddContactResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -751,6 +890,20 @@ const docTemplate = `{
} }
} }
}, },
"handler.GetMessageEventResponseFetchedResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/event.MessageEventResponse"
}
},
"message": {
"type": "string"
}
}
},
"handler.GetMessageResponse": { "handler.GetMessageResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -793,6 +946,47 @@ const docTemplate = `{
} }
} }
}, },
"handler.RecordEventRequest": {
"type": "object",
"properties": {
"contact_id": {
"type": "string"
},
"message_id": {
"type": "string"
},
"response": {
"type": "object",
"additionalProperties": {}
},
"scheduled_message_event_id": {
"type": "string"
},
"sent": {
"type": "string"
},
"status": {
"type": "string"
},
"user_id": {
"type": "string"
}
}
},
"handler.RecordEventResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/event.MessageEventResponse"
}
},
"message": {
"type": "string"
}
}
},
"handler.RequestAddContact": { "handler.RequestAddContact": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -832,10 +1026,10 @@ const docTemplate = `{
"handler.RequestAddScheduledMessageEvent": { "handler.RequestAddScheduledMessageEvent": {
"type": "object", "type": "object",
"properties": { "properties": {
"message_id": { "contact_id": {
"type": "string" "type": "string"
}, },
"recipient_id": { "message_id": {
"type": "string" "type": "string"
}, },
"scheduled_message_id": { "scheduled_message_id": {
@@ -902,7 +1096,7 @@ const docTemplate = `{
"data": { "data": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/message.MessageEventResponse" "$ref": "#/definitions/event.MessageEventResponse"
} }
}, },
"message": { "message": {
@@ -924,29 +1118,6 @@ const docTemplate = `{
} }
} }
}, },
"message.MessageEventResponse": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"response": {
"type": "array",
"items": {
"type": "integer"
}
},
"scheduled_message_event_id": {
"type": "string"
},
"sent": {
"type": "string"
},
"user_id": {
"type": "string"
}
}
},
"scheduling.ScheduledMessage": { "scheduling.ScheduledMessage": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -970,6 +1141,9 @@ const docTemplate = `{
"scheduling.ScheduledMessageEvent": { "scheduling.ScheduledMessageEvent": {
"type": "object", "type": "object",
"properties": { "properties": {
"contact_id": {
"type": "string"
},
"created": { "created": {
"type": "string" "type": "string"
}, },
@@ -979,9 +1153,6 @@ const docTemplate = `{
"message_id": { "message_id": {
"type": "string" "type": "string"
}, },
"recipient_id": {
"type": "string"
},
"scheduled_message_id": { "scheduled_message_id": {
"type": "string" "type": "string"
} }
+200 -29
View File
@@ -484,6 +484,106 @@
} }
} }
}, },
"/schedule/message/event/response": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Fetches a MessageEventResponse given a user id (requires JWT)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"message"
],
"summary": "Fetcht MessageEventResponse",
"parameters": [
{
"type": "string",
"description": "User Id",
"name": "user_id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.GetMessageEventResponseFetchedResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handler.GetMessageEventResponseFetchedResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handler.GetMessageEventResponseFetchedResponse"
}
}
}
}
},
"/schedule/message/event/response/record": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Saves the result of sending a text message (requires JWT)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"message"
],
"summary": "Record MessageEventResponse",
"parameters": [
{
"description": "Data to add MessageEventResponse",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handler.RecordEventRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/handler.RecordEventResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handler.RecordEventResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handler.RecordEventResponse"
}
}
}
}
},
"/schedule/message/event/{id}": { "/schedule/message/event/{id}": {
"delete": { "delete": {
"security": [ "security": [
@@ -636,9 +736,18 @@
"contact.Contact": { "contact.Contact": {
"type": "object", "type": "object",
"properties": { "properties": {
"first_name": {
"type": "string"
},
"id": { "id": {
"type": "string" "type": "string"
}, },
"last_name": {
"type": "string"
},
"nickname": {
"type": "string"
},
"phone_number": { "phone_number": {
"type": "string" "type": "string"
}, },
@@ -647,6 +756,36 @@
} }
} }
}, },
"event.MessageEventResponse": {
"type": "object",
"properties": {
"contact_id": {
"type": "string"
},
"id": {
"type": "string"
},
"message_id": {
"type": "string"
},
"response": {
"type": "object",
"additionalProperties": {}
},
"scheduled_message_event_id": {
"type": "string"
},
"sent": {
"type": "string"
},
"status": {
"type": "string"
},
"user_id": {
"type": "string"
}
}
},
"handler.AddContactResponse": { "handler.AddContactResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -745,6 +884,20 @@
} }
} }
}, },
"handler.GetMessageEventResponseFetchedResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/event.MessageEventResponse"
}
},
"message": {
"type": "string"
}
}
},
"handler.GetMessageResponse": { "handler.GetMessageResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -787,6 +940,47 @@
} }
} }
}, },
"handler.RecordEventRequest": {
"type": "object",
"properties": {
"contact_id": {
"type": "string"
},
"message_id": {
"type": "string"
},
"response": {
"type": "object",
"additionalProperties": {}
},
"scheduled_message_event_id": {
"type": "string"
},
"sent": {
"type": "string"
},
"status": {
"type": "string"
},
"user_id": {
"type": "string"
}
}
},
"handler.RecordEventResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/event.MessageEventResponse"
}
},
"message": {
"type": "string"
}
}
},
"handler.RequestAddContact": { "handler.RequestAddContact": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -826,10 +1020,10 @@
"handler.RequestAddScheduledMessageEvent": { "handler.RequestAddScheduledMessageEvent": {
"type": "object", "type": "object",
"properties": { "properties": {
"message_id": { "contact_id": {
"type": "string" "type": "string"
}, },
"recipient_id": { "message_id": {
"type": "string" "type": "string"
}, },
"scheduled_message_id": { "scheduled_message_id": {
@@ -896,7 +1090,7 @@
"data": { "data": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/message.MessageEventResponse" "$ref": "#/definitions/event.MessageEventResponse"
} }
}, },
"message": { "message": {
@@ -918,29 +1112,6 @@
} }
} }
}, },
"message.MessageEventResponse": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"response": {
"type": "array",
"items": {
"type": "integer"
}
},
"scheduled_message_event_id": {
"type": "string"
},
"sent": {
"type": "string"
},
"user_id": {
"type": "string"
}
}
},
"scheduling.ScheduledMessage": { "scheduling.ScheduledMessage": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -964,6 +1135,9 @@
"scheduling.ScheduledMessageEvent": { "scheduling.ScheduledMessageEvent": {
"type": "object", "type": "object",
"properties": { "properties": {
"contact_id": {
"type": "string"
},
"created": { "created": {
"type": "string" "type": "string"
}, },
@@ -973,9 +1147,6 @@
"message_id": { "message_id": {
"type": "string" "type": "string"
}, },
"recipient_id": {
"type": "string"
},
"scheduled_message_id": { "scheduled_message_id": {
"type": "string" "type": "string"
} }
+130 -20
View File
@@ -2,13 +2,39 @@ basePath: /api/v1
definitions: definitions:
contact.Contact: contact.Contact:
properties: properties:
first_name:
type: string
id: id:
type: string type: string
last_name:
type: string
nickname:
type: string
phone_number: phone_number:
type: string type: string
user_id: user_id:
type: string type: string
type: object type: object
event.MessageEventResponse:
properties:
contact_id:
type: string
id:
type: string
message_id:
type: string
response:
additionalProperties: {}
type: object
scheduled_message_event_id:
type: string
sent:
type: string
status:
type: string
user_id:
type: string
type: object
handler.AddContactResponse: handler.AddContactResponse:
properties: properties:
data: data:
@@ -72,6 +98,15 @@ definitions:
message: message:
type: string type: string
type: object type: object
handler.GetMessageEventResponseFetchedResponse:
properties:
data:
items:
$ref: '#/definitions/event.MessageEventResponse'
type: array
message:
type: string
type: object
handler.GetMessageResponse: handler.GetMessageResponse:
properties: properties:
data: data:
@@ -99,6 +134,33 @@ definitions:
message: message:
type: string type: string
type: object type: object
handler.RecordEventRequest:
properties:
contact_id:
type: string
message_id:
type: string
response:
additionalProperties: {}
type: object
scheduled_message_event_id:
type: string
sent:
type: string
status:
type: string
user_id:
type: string
type: object
handler.RecordEventResponse:
properties:
data:
items:
$ref: '#/definitions/event.MessageEventResponse'
type: array
message:
type: string
type: object
handler.RequestAddContact: handler.RequestAddContact:
properties: properties:
phone_number: phone_number:
@@ -124,9 +186,9 @@ definitions:
type: object type: object
handler.RequestAddScheduledMessageEvent: handler.RequestAddScheduledMessageEvent:
properties: properties:
message_id: contact_id:
type: string type: string
recipient_id: message_id:
type: string type: string
scheduled_message_id: scheduled_message_id:
type: string type: string
@@ -169,7 +231,7 @@ definitions:
properties: properties:
data: data:
items: items:
$ref: '#/definitions/message.MessageEventResponse' $ref: '#/definitions/event.MessageEventResponse'
type: array type: array
message: message:
type: string type: string
@@ -183,21 +245,6 @@ definitions:
user_id: user_id:
type: string type: string
type: object type: object
message.MessageEventResponse:
properties:
id:
type: string
response:
items:
type: integer
type: array
scheduled_message_event_id:
type: string
sent:
type: string
user_id:
type: string
type: object
scheduling.ScheduledMessage: scheduling.ScheduledMessage:
properties: properties:
created: created:
@@ -213,14 +260,14 @@ definitions:
type: object type: object
scheduling.ScheduledMessageEvent: scheduling.ScheduledMessageEvent:
properties: properties:
contact_id:
type: string
created: created:
type: string type: string
id: id:
type: string type: string
message_id: message_id:
type: string type: string
recipient_id:
type: string
scheduled_message_id: scheduled_message_id:
type: string type: string
type: object type: object
@@ -569,6 +616,69 @@ paths:
summary: Delete scheduled message event summary: Delete scheduled message event
tags: tags:
- scheduled message events - scheduled message events
/schedule/message/event/response:
get:
consumes:
- application/json
description: Fetches a MessageEventResponse given a user id (requires JWT)
parameters:
- description: User Id
in: path
name: user_id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.GetMessageEventResponseFetchedResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/handler.GetMessageEventResponseFetchedResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handler.GetMessageEventResponseFetchedResponse'
security:
- BearerAuth: []
summary: Fetcht MessageEventResponse
tags:
- message
/schedule/message/event/response/record:
post:
consumes:
- application/json
description: Saves the result of sending a text message (requires JWT)
parameters:
- description: Data to add MessageEventResponse
in: body
name: request
required: true
schema:
$ref: '#/definitions/handler.RecordEventRequest'
produces:
- application/json
responses:
"201":
description: Created
schema:
$ref: '#/definitions/handler.RecordEventResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/handler.RecordEventResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handler.RecordEventResponse'
security:
- BearerAuth: []
summary: Record MessageEventResponse
tags:
- message
/schedule/message/fetch: /schedule/message/fetch:
get: get:
consumes: consumes:
+3 -2
View File
@@ -3,9 +3,10 @@ module git.kundeng.us/phoenix/textsender-api
go 1.25.4 go 1.25.4
require ( require (
git.kundeng.us/phoenix/swoosh v0.0.7 git.kundeng.us/phoenix/swoosh v0.1.0
git.kundeng.us/phoenix/textsender-models v0.0.11 git.kundeng.us/phoenix/textsender-models v0.1.6
github.com/go-chi/chi/v5 v5.2.3 github.com/go-chi/chi/v5 v5.2.3
github.com/go-chi/cors v1.2.2
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/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.7.6 github.com/jackc/pgx/v5 v5.7.6
+6 -4
View File
@@ -1,7 +1,7 @@
git.kundeng.us/phoenix/swoosh v0.0.7 h1:9M42tgKr6o34YOiwds7offvjCUoFzYN2B5TrFKbefFo= git.kundeng.us/phoenix/swoosh v0.1.0 h1:9P+wLuJsIZmVcRoG+W22l06hwp54eNrYbsBtGjucnjI=
git.kundeng.us/phoenix/swoosh v0.0.7/go.mod h1:9iXlBd7UCMA7/jj8gZ1CxEfBualsClpiJrqGj8Fo2Xw= git.kundeng.us/phoenix/swoosh v0.1.0/go.mod h1:6/97/aS8KVdYqlczQ9ALvS3GVoce1UVl+D8VQK42Bd0=
git.kundeng.us/phoenix/textsender-models v0.0.11 h1:kd2FdeZJhJJAXBm8MoyadtgNGyzC+puU1oR8B8N+MfE= git.kundeng.us/phoenix/textsender-models v0.1.6 h1:d/rhvqjFSt1Ty/3UfwS21be+lYb9EtWEaDtkzfbzEp0=
git.kundeng.us/phoenix/textsender-models v0.0.11/go.mod h1:9iPDQJg1Tc6WMNoW5+f8YKmnosMwlWHJ++hmxNLDEe0= git.kundeng.us/phoenix/textsender-models v0.1.6/go.mod h1:9iPDQJg1Tc6WMNoW5+f8YKmnosMwlWHJ++hmxNLDEe0=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
@@ -11,6 +11,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-openapi/jsonpointer v0.22.3 h1:dKMwfV4fmt6Ah90zloTbUKWMD+0he+12XYAsPotrkn8= github.com/go-openapi/jsonpointer v0.22.3 h1:dKMwfV4fmt6Ah90zloTbUKWMD+0he+12XYAsPotrkn8=
github.com/go-openapi/jsonpointer v0.22.3/go.mod h1:0lBbqeRsQ5lIanv3LHZBrmRGHLHcQoOXQnf88fHlGWo= github.com/go-openapi/jsonpointer v0.22.3/go.mod h1:0lBbqeRsQ5lIanv3LHZBrmRGHLHcQoOXQnf88fHlGWo=
github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc= github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=
+2 -2
View File
@@ -1,9 +1,9 @@
package app package app
import ( import (
"git.kundeng.us/phoenix/textsender-models/tx0/config" auxcfg "git.kundeng.us/phoenix/textsender-models/tx0/config/auxiliary"
) )
type App struct { type App struct {
TwilioConfig *config.TwiloConfig TwilioConfig *auxcfg.TwilioConfig
} }
+23 -13
View File
@@ -5,19 +5,21 @@ import (
"fmt" "fmt"
"os" "os"
"strconv" "strconv"
"strings"
"git.kundeng.us/phoenix/textsender-models/tx0/config" auxcfg "git.kundeng.us/phoenix/textsender-models/tx0/config/auxiliary"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"git.kundeng.us/phoenix/textsender-api/internal/version" "git.kundeng.us/phoenix/textsender-api/internal/version"
) )
type Config struct { type Config struct {
DBConnString string DBConnString string
ServerPort string ServerPort string
ResetDB bool ResetDB bool
JWTSecret string `env:"JWT_SECRET" required:"true"` JWTSecret string `env:"JWT_SECRET" required:"true"`
TwilioConfig *config.TwiloConfig TwilioConfig *auxcfg.TwilioConfig
AllowedOrigins []string
} }
type ConnectionInfo struct { type ConnectionInfo struct {
@@ -47,7 +49,7 @@ func PrintName() {
fmt.Println(version.String()) fmt.Println(version.String())
} }
func Load() (*Config, *config.TwiloConfig, error) { func Load() (*Config, *auxcfg.TwilioConfig, error) {
versionFlag := flag.Bool("version", false, "Print version information") versionFlag := flag.Bool("version", false, "Print version information")
resetDb := flag.Bool("reset-db", false, "Reset the database schema and exit") resetDb := flag.Bool("reset-db", false, "Reset the database schema and exit")
port := flag.String("port", PORT, "Server port") port := flag.String("port", PORT, "Server port")
@@ -68,11 +70,13 @@ func Load() (*Config, *config.TwiloConfig, error) {
if cfg, err := TwilioConfig(); err != nil { if cfg, err := TwilioConfig(); err != nil {
return nil, nil, err return nil, nil, err
} else { } else {
allowedOrigins := unpackAllowedOrigins()
return &Config{ return &Config{
DBConnString: dbConnString, DBConnString: dbConnString,
ServerPort: *port, ServerPort: *port,
ResetDB: *resetDb, ResetDB: *resetDb,
JWTSecret: os.Getenv("JWT_SECRET"), JWTSecret: os.Getenv("JWT_SECRET"),
AllowedOrigins: allowedOrigins,
}, cfg, nil }, cfg, nil
} }
} }
@@ -128,7 +132,13 @@ func UnpackDBConnString() (connInfo ConnectionInfo) {
return return
} }
func TwilioConfig() (*config.TwiloConfig, error) { func unpackAllowedOrigins() []string {
allowedOriginsRaw := os.Getenv("ALLOWED_ORIGINS")
allowedOriginsSplit := strings.Split(allowedOriginsRaw, ",")
return allowedOriginsSplit
}
func TwilioConfig() (*auxcfg.TwilioConfig, error) {
authSid := os.Getenv("TWILIO_AUTH_SID") authSid := os.Getenv("TWILIO_AUTH_SID")
serviceSid := os.Getenv("TWILIO_SERVICE_SID") serviceSid := os.Getenv("TWILIO_SERVICE_SID")
authToken := os.Getenv("TWILIO_AUTH_TOKEN") authToken := os.Getenv("TWILIO_AUTH_TOKEN")
@@ -143,7 +153,7 @@ func TwilioConfig() (*config.TwiloConfig, error) {
} else if len(phoneNumber) == 0 { } else if len(phoneNumber) == 0 {
return nil, fmt.Errorf("Twilio config phone number not found") return nil, fmt.Errorf("Twilio config phone number not found")
} else { } else {
var cfg config.TwiloConfig var cfg auxcfg.TwilioConfig
cfg.AccountSID = authSid cfg.AccountSID = authSid
cfg.ServiceSID = serviceSid cfg.ServiceSID = serviceSid
cfg.AuthToken = authToken cfg.AuthToken = authToken
+3 -3
View File
@@ -54,14 +54,14 @@ func (c *ContactHandler) AddContact(w http.ResponseWriter, r *http.Request) {
} }
defer r.Body.Close() defer r.Body.Close()
newContact := contact.Contact{PhoneNumber: req.PhoneNumber, UserId: req.UserId} newContact := contact.Contact{PhoneNumber: req.PhoneNumber, UserId: &req.UserId}
var statusCode int var statusCode int
var resp AddContactResponse var resp AddContactResponse
ctx := r.Context() ctx := r.Context()
if exists, err := c.ContactStore.ContactExists(ctx, newContact.PhoneNumber, newContact.UserId); err != nil { if exists, err := c.ContactStore.ContactExists(ctx, newContact.PhoneNumber, *newContact.UserId); err != nil {
fmt.Printf("Error: %v", err) fmt.Printf("Error: %v", err)
statusCode = http.StatusInternalServerError statusCode = http.StatusInternalServerError
resp.Message = err.Error() resp.Message = err.Error()
@@ -157,7 +157,7 @@ func (c *ContactHandler) GetContact(w http.ResponseWriter, r *http.Request) {
fmt.Println("Checking with User Id") fmt.Println("Checking with User Id")
if contacts, err := c.ContactStore.GetAllContacts(ctx); err == nil { if contacts, err := c.ContactStore.GetAllContacts(ctx); err == nil {
for _, con := range contacts { for _, con := range contacts {
if con.UserId == userId { if *con.UserId == userId {
resp.Data = append(resp.Data, *con) resp.Data = append(resp.Data, *con)
} }
} }
+1 -11
View File
@@ -1,7 +1,6 @@
package handler package handler
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
@@ -13,7 +12,6 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"git.kundeng.us/phoenix/textsender-api/internal/db"
"git.kundeng.us/phoenix/textsender-api/internal/handler/endpoint" "git.kundeng.us/phoenix/textsender-api/internal/handler/endpoint"
"git.kundeng.us/phoenix/textsender-api/internal/store/mock" "git.kundeng.us/phoenix/textsender-api/internal/store/mock"
) )
@@ -54,7 +52,7 @@ func TestGetContactWithMock(t *testing.T) {
mockstore := mock.NewMockContactStore() mockstore := mock.NewMockContactStore()
testUserId := uuid.New() testUserId := uuid.New()
testCon := contact.Contact{PhoneNumber: "+12335403383", UserId: testUserId} testCon := contact.Contact{PhoneNumber: "+12335403383", UserId: &testUserId}
ctx := t.Context() ctx := t.Context()
if err := mockstore.CreateContact(ctx, &testCon); err != nil { if err := mockstore.CreateContact(ctx, &testCon); err != nil {
assert.NoError(t, err, "Error creating contact") assert.NoError(t, err, "Error creating contact")
@@ -80,11 +78,3 @@ func TestGetContactWithMock(t *testing.T) {
assert.NotNil(t, response.Data[0].Id, "Id should not be nil") assert.NotNil(t, response.Data[0].Id, "Id should not be nil")
} }
func resetTestDB(t *testing.T) {
t.Helper()
_, err := db.Pool.Exec(context.Background(), "DELETE FROM contacts")
if err != nil {
t.Fatalf("Failed to reset test database: %v", err)
}
}
+26 -14
View File
@@ -1,16 +1,28 @@
package endpoint package endpoint
const MESSAGE_DRAFT_ENDPOINT = "/api/v1/message/draft" const (
const ADD_MESSAGE = "/api/v1/message/new" MESSAGE_DRAFT_ENDPOINT = "/api/v1/message/draft"
const GET_MESSAGE = "/api/v1/message"
const GET_CONTACT = "/api/v1/contact" ADD_MESSAGE = "/api/v1/message/new"
const ADD_CONTACT_ENDPOINT = "/api/v1/contact/new" GET_MESSAGE = "/api/v1/message"
const ScheduleMessageEndpoint = "/api/v1/schedule/message"
const GetScheduledMessageEndpoint = "/api/v1/schedule/message" GET_CONTACT = "/api/v1/contact"
const AddEventToScheduledMessageEndpoint = "/api/v1/schedule/message/event" ADD_CONTACT_ENDPOINT = "/api/v1/contact/new"
const GetScheduledMessageEventEndpoint = "/api/v1/schedule/message/event"
const DeleteScheduledMessageEventEndpoint = "/api/v1/schedule/message/event/{id}" ScheduleMessageEndpoint = "/api/v1/schedule/message"
const UpdateScheduledMessageStatusEndpoint = "/api/v1/schedule/message/status/update" GetScheduledMessageEndpoint = "/api/v1/schedule/message"
const FetchNextScheduledMessageEndpoint = "/api/v1/schedule/message/fetch"
const RecordEventResponse = "/api/v1/schedule/message/event/response/record" AddEventToScheduledMessageEndpoint = "/api/v1/schedule/message/event"
const SendInstantMessageEndpoint = "/api/v1/instant/message" GetScheduledMessageEventEndpoint = "/api/v1/schedule/message/event"
DeleteScheduledMessageEventEndpoint = "/api/v1/schedule/message/event/{id}"
UpdateScheduledMessageStatusEndpoint = "/api/v1/schedule/message/status/update"
FetchNextScheduledMessageEndpoint = "/api/v1/schedule/message/fetch"
// TODO: Tweak endpoint verbage for MessageEventResponse endpoint
// TODO: Make RecordEventResponse more consistent with MessageEventResponse
RecordEventResponse = "/api/v1/schedule/message/event/response/record"
FetchMessageEventResponse = "/api/v1/schedule/message/event/response"
SendInstantMessageEndpoint = "/api/v1/instant/message"
)
+80
View File
@@ -1,14 +1,23 @@
package handler package handler
import ( import (
"context"
"encoding/json"
"fmt" "fmt"
"os" "os"
"path" "path"
"testing"
"time"
"git.kundeng.us/phoenix/textsender-models/tx0/contact"
"git.kundeng.us/phoenix/textsender-models/tx0/message"
"git.kundeng.us/phoenix/textsender-models/tx0/message/scheduling"
"github.com/google/uuid"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"git.kundeng.us/phoenix/textsender-api/internal/app" "git.kundeng.us/phoenix/textsender-api/internal/app"
"git.kundeng.us/phoenix/textsender-api/internal/config" "git.kundeng.us/phoenix/textsender-api/internal/config"
"git.kundeng.us/phoenix/textsender-api/internal/db"
) )
func GetApp() (*app.App, error) { func GetApp() (*app.App, error) {
@@ -29,3 +38,74 @@ func GetApp() (*app.App, error) {
return &app.App{TwilioConfig: tCfg}, nil return &app.App{TwilioConfig: tCfg}, nil
} }
} }
func ResetTestDB(t *testing.T, tableName string) {
t.Helper()
_, err := db.Pool.Exec(context.Background(), fmt.Sprintf("DELETE FROM %s", tableName))
if err != nil {
t.Fatalf("Failed to reset test database: %v", err)
}
}
func ContactTest(id uuid.UUID, userId uuid.UUID) contact.Contact {
if id == uuid.Nil {
id = uuid.New()
return contact.Contact{Id: &id, PhoneNumber: "+10123456789", UserId: &userId}
} else {
return contact.Contact{Id: &id, PhoneNumber: "+10123456789", UserId: &userId}
}
}
func MessageTest(id uuid.UUID, userId uuid.UUID) message.Message {
if id == uuid.Nil {
id = uuid.New()
return message.Message{Id: &id, 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 ScheduledMessageTest(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 ScheduledMessageEventTest(messageId, contactId, scheduledMessageId uuid.UUID) scheduling.ScheduledMessageEvent {
return scheduling.ScheduledMessageEvent{MessageId: messageId, ContactId: contactId, ScheduledMessageId: scheduledMessageId}
}
func MERResponseInString() string {
return `{
"body": "Whoknows?",
"num_segments": "0",
"direction": "outbound-api",
"from": "+12243026041",
"to": "+16303831708",
"date_updated": "Sat,29Nov202519:06:59+0000",
"uri": "/2010-04-01/Accounts/ACefa1ef516314c9d1a68cbd657de49277/Messages/SM1193a529e7f7a840667cd1e0f13ea95a.json",
"account_sid": "ACefa1ef516314c9d1a68cbd657de49277",
"num_media": "0",
"status": "scheduled",
"messaging_service_sid": "MG803f3676706b92eb02e18dd820c447f2",
"sid": "SM1193a529e7f7a840667cd1e0f13ea95a",
"date_created": "Sat,29Nov202519:06:59+0000",
"api_version": "2010-04-01",
"subresource_uris": {
"media": "/2010-04-01/Accounts/ACefa1ef516314c9d1a68cbd657de49277/Messages/SM1193a529e7f7a840667cd1e0f13ea95a/Media.json"
}
}`
}
func ConvertStringToMapOfAny(response string) (map[string]any, error) {
bytes := []byte(response)
var merResponse map[string]any
err := json.Unmarshal(bytes, &merResponse)
if err != nil {
return nil, err
} else {
return merResponse, nil
}
}
+17 -19
View File
@@ -2,7 +2,6 @@ package handler
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
@@ -12,6 +11,7 @@ import (
"git.kundeng.us/phoenix/swoosh/swoop/send" "git.kundeng.us/phoenix/swoosh/swoop/send"
"git.kundeng.us/phoenix/textsender-models/tx0/contact" "git.kundeng.us/phoenix/textsender-models/tx0/contact"
"git.kundeng.us/phoenix/textsender-models/tx0/message" "git.kundeng.us/phoenix/textsender-models/tx0/message"
"git.kundeng.us/phoenix/textsender-models/tx0/message/event"
"github.com/google/uuid" "github.com/google/uuid"
"git.kundeng.us/phoenix/textsender-api/internal/app" "git.kundeng.us/phoenix/textsender-api/internal/app"
@@ -38,8 +38,8 @@ type SendInstantMessageRequest struct {
} }
type SendInstantMessageResponse struct { type SendInstantMessageResponse struct {
Message string `json:"message"` Message string `json:"message"`
Data []*message.MessageEventResponse `json:"data"` Data []*event.MessageEventResponse `json:"data"`
} }
// Sent godoc // Sent godoc
@@ -93,31 +93,29 @@ func (s *SendInstantMessageHandler) Send(w http.ResponseWriter, r *http.Request)
if s.App != nil { if s.App != nil {
msgSender := send.MessageSender{Config: s.App.TwilioConfig} msgSender := send.MessageSender{Config: s.App.TwilioConfig}
for _, c := range contacts { for _, c := range contacts {
if twilioResp, err := msgSender.Send(*msg, *c, nil); err != nil { if twilioResp, twilioRespObject, err := msgSender.Send(*msg, *c, nil); err != nil {
statusCode = http.StatusInternalServerError statusCode = http.StatusInternalServerError
resp.Message = err.Error() resp.Message = err.Error()
} else { } else {
if data, err := json.Marshal(twilioResp); err != nil { dateStr := strings.TrimSpace(*twilioResp.DateUpdated)
if parsedTime, err := time.Parse(time.RFC1123Z, dateStr); err != nil {
statusCode = http.StatusInternalServerError statusCode = http.StatusInternalServerError
resp.Message = err.Error() resp.Message = err.Error()
} else { } else {
dateStr := strings.TrimSpace(*twilioResp.DateUpdated) mer := event.MessageEventResponse{}
if parsedTime, err := time.Parse(time.RFC1123Z, dateStr); err != nil { mer.Response = twilioRespObject
mer.UserId = req.UserId
mer.Sent = parsedTime
mer.Status = event.Message_Event_Response_Status_Instant
mer.ContactId = c.Id
mer.MessageId = msg.Id
if err := s.MERStore.Create(ctx, &mer); err != nil {
statusCode = http.StatusInternalServerError statusCode = http.StatusInternalServerError
resp.Message = err.Error() resp.Message = err.Error()
} else { } else {
mer := message.MessageEventResponse{} statusCode = http.StatusOK
mer.Response = data resp.Message = "Successful"
mer.UserId = req.UserId resp.Data = append(resp.Data, &mer)
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)
}
} }
} }
} }
+12 -17
View File
@@ -12,16 +12,6 @@ import (
"git.kundeng.us/phoenix/textsender-api/internal/store" "git.kundeng.us/phoenix/textsender-api/internal/store"
) )
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 { type MessageHandler struct {
App *app.App App *app.App
MessageStore store.MessageStore MessageStore store.MessageStore
@@ -33,6 +23,16 @@ func NewMessageHandler(apiApp *app.App, str store.MessageStore) *MessageHandler
const Message_Limit = 200 const Message_Limit = 200
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"`
}
// AddMessage godoc // AddMessage godoc
// @Summary Add message // @Summary Add message
// @Description Add a message record to send a text to (requires JWT) // @Description Add a message record to send a text to (requires JWT)
@@ -46,11 +46,6 @@ const Message_Limit = 200
// @Failure 500 {object} AddMessageResponse // @Failure 500 {object} AddMessageResponse
// @Router /message/new [post] // @Router /message/new [post]
func (m *MessageHandler) AddMessage(w http.ResponseWriter, r *http.Request) { 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 var req RequestAddMessage
if err := ExtractFromRequest(r, &req); err != nil { if err := ExtractFromRequest(r, &req); err != nil {
http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest) http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest)
@@ -59,7 +54,7 @@ func (m *MessageHandler) AddMessage(w http.ResponseWriter, r *http.Request) {
var statusCode int var statusCode int
var resp AddMessageResponse var resp AddMessageResponse
newMessage := message.Message{Content: req.Content, UserId: req.UserId} newMessage := message.Message{Content: req.Content, UserId: &req.UserId}
if isMessageValid(&newMessage) { if isMessageValid(&newMessage) {
ctx := r.Context() ctx := r.Context()
@@ -160,7 +155,7 @@ func (c *MessageHandler) GetMessage(w http.ResponseWriter, r *http.Request) {
if messages, err := c.MessageStore.GetAllMessages(ctx); err == nil { if messages, err := c.MessageStore.GetAllMessages(ctx); err == nil {
log.Println("Amount of messages:", len(messages)) log.Println("Amount of messages:", len(messages))
for _, msg := range messages { for _, msg := range messages {
if msg.UserId == userId { if *msg.UserId == userId {
resp.Data = append(resp.Data, *msg) resp.Data = append(resp.Data, *msg)
} }
} }
+106 -9
View File
@@ -1,11 +1,11 @@
package handler package handler
import ( import (
"log"
"net/http" "net/http"
"time" "time"
"git.kundeng.us/phoenix/textsender-models/tx0/message" "git.kundeng.us/phoenix/textsender-models/tx0/message/event"
"git.kundeng.us/phoenix/textsender-models/tx0/types"
"github.com/google/uuid" "github.com/google/uuid"
"git.kundeng.us/phoenix/textsender-api/internal/app" "git.kundeng.us/phoenix/textsender-api/internal/app"
@@ -22,17 +22,32 @@ func NewEventResponseHandler(apiApp *app.App, str store.MessageEventResponseStor
} }
type RecordEventRequest struct { type RecordEventRequest struct {
ScheduledMessageEventId uuid.UUID `json:"scheduled_message_event_id"` ScheduledMessageEventId uuid.UUID `json:"scheduled_message_event_id"`
Response types.JSONB `json:"response"` Response map[string]any `json:"response"`
UserId uuid.UUID `json:"user_id"` UserId uuid.UUID `json:"user_id"`
Sent *time.Time `json:"sent,omitempty"` Sent *time.Time `json:"sent,omitempty"`
Status string `json:"status"`
ContactId *uuid.UUID `json:"contact_id"`
MessageId *uuid.UUID `json:"message_id"`
} }
type RecordEventResponse struct { type RecordEventResponse struct {
Message string `json:"message"` Message string `json:"message"`
Data []*message.MessageEventResponse `json:"data"` Data []*event.MessageEventResponse `json:"data"`
} }
// RecordMessageEventResponse godoc
// @Summary Record MessageEventResponse
// @Description Saves the result of sending a text message (requires JWT)
// @Tags message
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body RecordEventRequest true "Data to add MessageEventResponse"
// @Success 201 {object} RecordEventResponse
// @Failure 400 {object} RecordEventResponse
// @Failure 500 {object} RecordEventResponse
// @Router /schedule/message/event/response/record [post]
func (e *EventResponseHandler) RecordResponse(w http.ResponseWriter, r *http.Request) { func (e *EventResponseHandler) RecordResponse(w http.ResponseWriter, r *http.Request) {
var req RecordEventRequest var req RecordEventRequest
if err := ExtractFromRequest(r, &req); err != nil { if err := ExtractFromRequest(r, &req); err != nil {
@@ -41,7 +56,7 @@ func (e *EventResponseHandler) RecordResponse(w http.ResponseWriter, r *http.Req
defer r.Body.Close() defer r.Body.Close()
var statusCode int var statusCode int
var rp message.MessageEventResponse var rp event.MessageEventResponse
var resp RecordEventResponse var resp RecordEventResponse
if req.Sent == nil { if req.Sent == nil {
@@ -57,16 +72,39 @@ func (e *EventResponseHandler) RecordResponse(w http.ResponseWriter, r *http.Req
statusCode = http.StatusBadRequest statusCode = http.StatusBadRequest
resp.Message = "Scheduled message event Id is empty" resp.Message = "Scheduled message event Id is empty"
} else { } else {
log.Println("Starting process to record Message Event Response")
rp.ScheduledMessageEventId = req.ScheduledMessageEventId rp.ScheduledMessageEventId = req.ScheduledMessageEventId
rp.Response = req.Response rp.Response = req.Response
rp.UserId = req.UserId rp.UserId = req.UserId
rp.Sent = *req.Sent rp.Sent = *req.Sent
switch req.Status {
case event.Message_Event_Response_Status_Instant:
{
log.Println("Status:", req.Status)
rp.Status = req.Status
rp.MessageId = req.MessageId
rp.ContactId = req.ContactId
}
case event.Message_Event_Response_Status_Scheduled:
{
log.Println("Status:", req.Status)
rp.Status = req.Status
}
default:
{
statusCode = http.StatusBadRequest
resp.Message = "Status is invalid"
}
}
ctx := r.Context() ctx := r.Context()
if err := e.MessageEventResponseStore.Create(ctx, &rp); err != nil { if err := e.MessageEventResponseStore.Create(ctx, &rp); err != nil {
log.Println("Error:", err)
resp.Message = err.Error() resp.Message = err.Error()
statusCode = http.StatusInternalServerError statusCode = http.StatusInternalServerError
} else { } else {
log.Println("Created Message Event Response")
statusCode = http.StatusCreated statusCode = http.StatusCreated
resp.Message = "Successful" resp.Message = "Successful"
resp.Data = append(resp.Data, &rp) resp.Data = append(resp.Data, &rp)
@@ -75,3 +113,62 @@ func (e *EventResponseHandler) RecordResponse(w http.ResponseWriter, r *http.Req
RespondWithJSON(w, statusCode, &resp) RespondWithJSON(w, statusCode, &resp)
} }
type GetMessageEventResponseFetchedResponse struct {
Message string `json:"message"`
Data []*event.MessageEventResponse `json:"data"`
}
// FetchtMessageEventResponse godoc
// @Summary Fetcht MessageEventResponse
// @Description Fetches a MessageEventResponse given a user id (requires JWT)
// @Tags message
// @Accept json
// @Produce json
// @Param user_id path string true "User Id"
// @Security BearerAuth
// @Success 200 {object} GetMessageEventResponseFetchedResponse
// @Failure 400 {object} GetMessageEventResponseFetchedResponse
// @Failure 500 {object} GetMessageEventResponseFetchedResponse
// @Router /schedule/message/event/response [get]
func (e *EventResponseHandler) Fetch(w http.ResponseWriter, r *http.Request) {
queryParams := r.URL.Query()
var userId uuid.UUID
if _, exists := queryParams["user_id"]; !exists {
http.Error(w, "Query params", http.StatusBadRequest)
return
} else {
var err error
userId, err = uuid.Parse(queryParams.Get("user_id"))
if err != nil {
http.Error(w, "Query params", http.StatusBadRequest)
return
}
}
var statusCode int
var resp GetMessageEventResponseFetchedResponse
if userId == uuid.Nil {
statusCode = http.StatusBadRequest
resp.Message = "User Id is nil"
} else {
ctx := r.Context()
if sentMessages, err := e.MessageEventResponseStore.GetWithUserId(ctx, userId); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
if len(sentMessages) == 0 {
statusCode = http.StatusNotFound
resp.Message = "Not MessageEventResponses found"
} else {
statusCode = http.StatusOK
resp.Message = "Successful"
resp.Data = sentMessages
}
}
}
RespondWithJSON(w, statusCode, &resp)
}
+88 -10
View File
@@ -2,13 +2,14 @@ package handler
import ( import (
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings" "strings"
"testing" "testing"
"time" "time"
"git.kundeng.us/phoenix/textsender-models/tx0/message" evnt "git.kundeng.us/phoenix/textsender-models/tx0/message/event"
"git.kundeng.us/phoenix/textsender-models/tx0/message/scheduling" "git.kundeng.us/phoenix/textsender-models/tx0/message/scheduling"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -30,14 +31,14 @@ func TestRecordMessageEventResponseWithMock(t *testing.T) {
assert.NoError(t, err, "Error getting app") assert.NoError(t, err, "Error getting app")
handler := NewEventResponseHandler(apiApp, merStore) handler := NewEventResponseHandler(apiApp, merStore)
recipientId := uuid.New() contactId := uuid.New()
messageId := uuid.New() messageId := uuid.New()
scheduledMessageId := uuid.New() scheduledMessageId := uuid.New()
testUserId := uuid.New() testUserId := uuid.New()
con := testContact(recipientId, testUserId) con := ContactTest(contactId, testUserId)
msg := testMessage(messageId, testUserId) msg := MessageTest(messageId, testUserId)
schMsg := testScheduledMessage(scheduledMessageId, testUserId, now) schMsg := ScheduledMessageTest(scheduledMessageId, testUserId, now)
event := scheduling.ScheduledMessageEvent{} event := scheduling.ScheduledMessageEvent{}
ctx := t.Context() ctx := t.Context()
@@ -50,17 +51,19 @@ func TestRecordMessageEventResponseWithMock(t *testing.T) {
assert.NoError(t, err, "Error creating scheduled message: %v", err) assert.NoError(t, err, "Error creating scheduled message: %v", err)
} }
event.MessageId = msg.Id event.MessageId = *msg.Id
event.RecipientId = con.Id event.ContactId = *con.Id
event.ScheduledMessageId = schMsg.Id event.ScheduledMessageId = schMsg.Id
if err := mockStore.CreateScheduledMessageEvent(ctx, &event); err != nil { if err := mockStore.CreateScheduledMessageEvent(ctx, &event); err != nil {
assert.NoError(t, err, "Error creating scheduled message event: %v", err) assert.NoError(t, err, "Error creating scheduled message event: %v", err)
} }
sent := now.Add(30 * time.Minute) sent := now.Add(30 * time.Minute)
bytes := []byte("{\"body\":\"Whoknows?\",\"num_segments\":\"0\",\"direction\":\"outbound-api\",\"from\":\"+12243026041\",\"to\":\"+16303831708\",\"date_updated\":\"Sat,29Nov202519:06:59+0000\",\"uri\":\"/2010-04-01/Accounts/ACefa1ef516314c9d1a68cbd657de49277/Messages/SM1193a529e7f7a840667cd1e0f13ea95a.json\",\"account_sid\":\"ACefa1ef516314c9d1a68cbd657de49277\",\"num_media\":\"0\",\"status\":\"scheduled\",\"messaging_service_sid\":\"MG803f3676706b92eb02e18dd820c447f2\",\"sid\":\"SM1193a529e7f7a840667cd1e0f13ea95a\",\"date_created\":\"Sat,29Nov202519:06:59+0000\",\"api_version\":\"2010-04-01\",\"subresource_uris\":{\"media\":\"/2010-04-01/Accounts/ACefa1ef516314c9d1a68cbd657de49277/Messages/SM1193a529e7f7a840667cd1e0f13ea95a/Media.json\"}}") var merResponse map[string]any
merResponse, err = ConvertStringToMapOfAny(MERResponseInString())
assert.NoError(t, err, "Error Converting to map[string]any")
testReq := RecordEventRequest{ScheduledMessageEventId: event.Id, Response: bytes, UserId: schMsg.UserId, Sent: &sent} testReq := RecordEventRequest{ScheduledMessageEventId: event.Id, Response: merResponse, UserId: schMsg.UserId, Sent: &sent, Status: evnt.Message_Event_Response_Status_Scheduled}
jsonValue, _ := json.Marshal(testReq) jsonValue, _ := json.Marshal(testReq)
req, _ := http.NewRequest("POST", endpoint.RecordEventResponse, strings.NewReader(string(jsonValue))) req, _ := http.NewRequest("POST", endpoint.RecordEventResponse, strings.NewReader(string(jsonValue)))
@@ -76,9 +79,84 @@ func TestRecordMessageEventResponseWithMock(t *testing.T) {
assert.NotEmpty(t, response.Data, "No message event response created") assert.NotEmpty(t, response.Data, "No message event response created")
var msgEventResp message.MessageEventResponse var msgEventResp evnt.MessageEventResponse
msgEventResp = *response.Data[0] msgEventResp = *response.Data[0]
assert.NotEmpty(t, msgEventResp.Response, "The response of the message event response is empty") assert.NotEmpty(t, msgEventResp.Response, "The response of the message event response is empty")
assert.Equal(t, msgEventResp.Response, testReq.Response, "Responses do not match") assert.Equal(t, msgEventResp.Response, testReq.Response, "Responses do not match")
} }
func TestGetMessageEventResponse(t *testing.T) {
now := time.Now()
mockStore := mock.NewMockScheduledMessageEventStore()
contactStore := mock.NewMockContactStore()
messageStore := mock.NewMockMessageStore()
schMsgStore := mock.NewMockScheduledMessageStore()
merStore := mock.NewMockMessageEventResponseStore()
apiApp, err := GetApp()
assert.NoError(t, err, "Error getting app")
handler := NewEventResponseHandler(apiApp, merStore)
contactId := uuid.New()
messageId := uuid.New()
scheduledMessageId := uuid.New()
testUserId := uuid.New()
con := ContactTest(contactId, testUserId)
msg := MessageTest(messageId, testUserId)
schMsg := ScheduledMessageTest(scheduledMessageId, testUserId, now)
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)
}
event := scheduling.ScheduledMessageEvent{}
event.MessageId = *msg.Id
event.ContactId = *con.Id
event.ScheduledMessageId = schMsg.Id
sent := now.Add(30 * time.Minute)
if err := mockStore.CreateScheduledMessageEvent(ctx, &event); err != nil {
assert.NoError(t, err, "Error creating scheduled message event: %v", err)
} else {
m := evnt.MessageEventResponse{}
m.ScheduledMessageEventId = event.Id
m.UserId = schMsg.UserId
m.Status = evnt.Message_Event_Response_Status_Scheduled
m.Sent = sent
var merResponse map[string]any
merResponse, err = ConvertStringToMapOfAny(MERResponseInString())
assert.NoError(t, err, "Error Converting to map[string]any")
m.Response = merResponse
if err := merStore.Create(ctx, &m); err != nil {
assert.NoError(t, err, "Error creating MessageEventResponse: %v", err)
}
}
url := fmt.Sprintf("%s?user_id=%s", endpoint.FetchMessageEventResponse, testUserId)
req, _ := http.NewRequest("GET", url, nil)
rr := httptest.NewRecorder()
handler.Fetch(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
var response GetMessageEventResponseFetchedResponse
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 found")
var msgEventResp evnt.MessageEventResponse
msgEventResp = *response.Data[0]
assert.NotEmpty(t, msgEventResp.Response, "The response of the message event response is empty")
}
+1 -1
View File
@@ -51,7 +51,7 @@ func TestGetMessageWithMock(t *testing.T) {
mockstore := mock.NewMockMessageStore() mockstore := mock.NewMockMessageStore()
testUserId := uuid.New() testUserId := uuid.New()
testCon := message.Message{Content: "Who is the one that benefits?", UserId: testUserId} testCon := message.Message{Content: "Who is the one that benefits?", UserId: &testUserId}
ctx := t.Context() ctx := t.Context()
if err := mockstore.CreateMessage(ctx, &testCon); err != nil { if err := mockstore.CreateMessage(ctx, &testCon); err != nil {
assert.NoError(t, err, "Error creating message") assert.NoError(t, err, "Error creating message")
+10 -10
View File
@@ -10,6 +10,16 @@ import (
"git.kundeng.us/phoenix/textsender-api/internal/store" "git.kundeng.us/phoenix/textsender-api/internal/store"
) )
type ScheduledMessageStatusHandler struct {
App *app.App
ScheduledMessageEventStore store.ScheduledMessageEventStore
ScheduledMessageStore store.ScheduledMessageStore
}
func NewScheduledMessageStatusHandler(apiApp *app.App, str store.ScheduledMessageEventStore, schStore store.ScheduledMessageStore) *ScheduledMessageStatusHandler {
return &ScheduledMessageStatusHandler{App: apiApp, ScheduledMessageEventStore: str, ScheduledMessageStore: schStore}
}
type RequestScheduledMessageStatus struct { type RequestScheduledMessageStatus struct {
ScheduledMessageId uuid.UUID `json:"scheduled_message_id"` ScheduledMessageId uuid.UUID `json:"scheduled_message_id"`
Status string `json:"status"` Status string `json:"status"`
@@ -25,16 +35,6 @@ type ScheduledMessageStatusResponse struct {
Data []ScheduledMessageChange `json:"data"` Data []ScheduledMessageChange `json:"data"`
} }
type ScheduledMessageStatusHandler struct {
App *app.App
ScheduledMessageEventStore store.ScheduledMessageEventStore
ScheduledMessageStore store.ScheduledMessageStore
}
func NewScheduledMessageStatusHandler(apiApp *app.App, str store.ScheduledMessageEventStore, schStore store.ScheduledMessageStore) *ScheduledMessageStatusHandler {
return &ScheduledMessageStatusHandler{App: apiApp, ScheduledMessageEventStore: str, ScheduledMessageStore: schStore}
}
// UpdateStatus godoc // UpdateStatus godoc
// @Summary Update status // @Summary Update status
// @Description Update the status of a scheduled message (requires JWT) // @Description Update the status of a scheduled message (requires JWT)
@@ -24,15 +24,15 @@ func TestUpdateScheduledMessageStatusWithMock(t *testing.T) {
messageStore := mock.NewMockMessageStore() messageStore := mock.NewMockMessageStore()
schMsgStore := mock.NewMockScheduledMessageStore() schMsgStore := mock.NewMockScheduledMessageStore()
recipientId := uuid.New() contactId := uuid.New()
messageId := uuid.New() messageId := uuid.New()
scheduledMessageId := uuid.New() scheduledMessageId := uuid.New()
testUserId := uuid.New() testUserId := uuid.New()
con := testContact(recipientId, testUserId) con := ContactTest(contactId, testUserId)
msg := testMessage(messageId, testUserId) msg := MessageTest(messageId, testUserId)
schMsg := testScheduledMessage(scheduledMessageId, testUserId, now) schMsg := ScheduledMessageTest(scheduledMessageId, testUserId, now)
event := testScheduledMessageEvent(msg.Id, con.Id, schMsg.Id) event := ScheduledMessageEventTest(*msg.Id, *con.Id, schMsg.Id)
ctx := t.Context() ctx := t.Context()
+19 -19
View File
@@ -13,6 +13,15 @@ import (
"git.kundeng.us/phoenix/textsender-api/internal/store" "git.kundeng.us/phoenix/textsender-api/internal/store"
) )
type ScheduledMessageHandler struct {
App *app.App
ScheduledMessageStore store.ScheduledMessageStore
}
func NewScheduledMessageHandler(apiApp *app.App, str store.ScheduledMessageStore) *ScheduledMessageHandler {
return &ScheduledMessageHandler{App: apiApp, ScheduledMessageStore: str}
}
type RequestAddScheduledMessage struct { type RequestAddScheduledMessage struct {
Scheduled time.Time `json:"scheduled"` Scheduled time.Time `json:"scheduled"`
Status string `json:"status"` Status string `json:"status"`
@@ -24,25 +33,6 @@ type AddScheduledMessageResponse struct {
Data []scheduling.ScheduledMessage `json:"data"` Data []scheduling.ScheduledMessage `json:"data"`
} }
type GetScheduledMessageResponse struct {
Message string `json:"message"`
Data []scheduling.ScheduledMessage `json:"data"`
}
type FetchNextMessageResponse struct {
Message string `json:"message"`
Data []scheduling.ScheduledMessage `json:"data"`
}
type ScheduledMessageHandler struct {
App *app.App
ScheduledMessageStore store.ScheduledMessageStore
}
func NewScheduledMessageHandler(apiApp *app.App, str store.ScheduledMessageStore) *ScheduledMessageHandler {
return &ScheduledMessageHandler{App: apiApp, ScheduledMessageStore: str}
}
// AddScheduledMessage godoc // AddScheduledMessage godoc
// @Summary Add scheduled message // @Summary Add scheduled message
// @Description Adds a scheduled message (requires JWT) // @Description Adds a scheduled message (requires JWT)
@@ -106,6 +96,11 @@ func (s *ScheduledMessageHandler) AddScheduledMessage(w http.ResponseWriter, r *
RespondWithJSON(w, statusCode, &resp) RespondWithJSON(w, statusCode, &resp)
} }
type GetScheduledMessageResponse struct {
Message string `json:"message"`
Data []scheduling.ScheduledMessage `json:"data"`
}
// GetScheduledMessage godoc // GetScheduledMessage godoc
// @Summary Get scheduled message // @Summary Get scheduled message
// @Description Get a scheduled message (requires JWT) // @Description Get a scheduled message (requires JWT)
@@ -183,6 +178,11 @@ func (s *ScheduledMessageHandler) GetScheduledMessage(w http.ResponseWriter, r *
RespondWithJSON(w, statusCode, &resp) RespondWithJSON(w, statusCode, &resp)
} }
type FetchNextMessageResponse struct {
Message string `json:"message"`
Data []scheduling.ScheduledMessage `json:"data"`
}
// FetchNextMessage godoc // FetchNextMessage godoc
// @Summary Fetch scheduled message // @Summary Fetch scheduled message
// @Description Fetches a scheduled message that is available to be processed (requires JWT) // @Description Fetches a scheduled message that is available to be processed (requires JWT)
+25 -25
View File
@@ -12,27 +12,6 @@ import (
"git.kundeng.us/phoenix/textsender-api/internal/store" "git.kundeng.us/phoenix/textsender-api/internal/store"
) )
type RequestAddScheduledMessageEvent struct {
RecipientId uuid.UUID `json:"recipient_id"`
MessageId uuid.UUID `json:"message_id"`
ScheduledMessageId uuid.UUID `json:"scheduled_message_id"`
}
type AddScheduledMessageEventResponse struct {
Message string `json:"message"`
Data []scheduling.ScheduledMessageEvent `json:"data"`
}
type GetScheduledMessageEventResponse struct {
Message string `json:"message"`
Data []scheduling.ScheduledMessageEvent `json:"data"`
}
type DeleteScheduledMessageEventResponse struct {
Message string `json:"message"`
Data []scheduling.ScheduledMessageEvent `json:"data"`
}
type ScheduledMessageEventHandler struct { type ScheduledMessageEventHandler struct {
App *app.App App *app.App
ScheduledMessageEventStore store.ScheduledMessageEventStore ScheduledMessageEventStore store.ScheduledMessageEventStore
@@ -43,7 +22,18 @@ func NewScheduledMessageEventHandler(apiApp *app.App, str store.ScheduledMessage
return &ScheduledMessageEventHandler{App: apiApp, ScheduledMessageEventStore: str, ScheduledMessageStore: schStore} return &ScheduledMessageEventHandler{App: apiApp, ScheduledMessageEventStore: str, ScheduledMessageStore: schStore}
} }
// AddContact godoc type RequestAddScheduledMessageEvent struct {
ContactId uuid.UUID `json:"contact_id"`
MessageId uuid.UUID `json:"message_id"`
ScheduledMessageId uuid.UUID `json:"scheduled_message_id"`
}
type AddScheduledMessageEventResponse struct {
Message string `json:"message"`
Data []scheduling.ScheduledMessageEvent `json:"data"`
}
// AddScheduledMessageEvent godoc
// @Summary Add scheduled message event // @Summary Add scheduled message event
// @Description Add a scheduled message event (requires JWT) // @Description Add a scheduled message event (requires JWT)
// @Tags scheduled message events // @Tags scheduled message events
@@ -67,14 +57,14 @@ func (s *ScheduledMessageEventHandler) AddScheduledMessageEvent(w http.ResponseW
} }
defer r.Body.Close() defer r.Body.Close()
event := scheduling.ScheduledMessageEvent{RecipientId: req.RecipientId, MessageId: req.MessageId, ScheduledMessageId: req.ScheduledMessageId} event := scheduling.ScheduledMessageEvent{ContactId: req.ContactId, MessageId: req.MessageId, ScheduledMessageId: req.ScheduledMessageId}
var statusCode int var statusCode int
var resp AddScheduledMessageEventResponse var resp AddScheduledMessageEventResponse
if event.RecipientId == uuid.Nil { if event.ContactId == uuid.Nil {
statusCode = http.StatusBadRequest statusCode = http.StatusBadRequest
resp.Message = "Recipient Id is nil" resp.Message = "Contact Id is nil"
} else if event.MessageId == uuid.Nil { } else if event.MessageId == uuid.Nil {
statusCode = http.StatusBadRequest statusCode = http.StatusBadRequest
resp.Message = "Message Id is nil" resp.Message = "Message Id is nil"
@@ -117,6 +107,11 @@ func (s *ScheduledMessageEventHandler) AddScheduledMessageEvent(w http.ResponseW
RespondWithJSON(w, statusCode, &resp) RespondWithJSON(w, statusCode, &resp)
} }
type GetScheduledMessageEventResponse struct {
Message string `json:"message"`
Data []scheduling.ScheduledMessageEvent `json:"data"`
}
// GetScheduledMessageEvent godoc // GetScheduledMessageEvent godoc
// @Summary Get scheduled message event // @Summary Get scheduled message event
// @Description Gets a scheduled message event (requires JWT) // @Description Gets a scheduled message event (requires JWT)
@@ -181,6 +176,11 @@ func (s *ScheduledMessageEventHandler) GetScheduledMessageEvent(w http.ResponseW
RespondWithJSON(w, statusCode, &resp) RespondWithJSON(w, statusCode, &resp)
} }
type DeleteScheduledMessageEventResponse struct {
Message string `json:"message"`
Data []scheduling.ScheduledMessageEvent `json:"data"`
}
// DeleteScheduledMessageEvent godoc // DeleteScheduledMessageEvent godoc
// @Summary Delete scheduled message event // @Summary Delete scheduled message event
// @Description Deletes a scheduled message event (requires JWT) // @Description Deletes a scheduled message event (requires JWT)
@@ -9,8 +9,6 @@ import (
"testing" "testing"
"time" "time"
"git.kundeng.us/phoenix/textsender-models/tx0/contact"
"git.kundeng.us/phoenix/textsender-models/tx0/message"
"git.kundeng.us/phoenix/textsender-models/tx0/message/scheduling" "git.kundeng.us/phoenix/textsender-models/tx0/message/scheduling"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -20,14 +18,14 @@ import (
) )
var ( var (
recipientId = uuid.New() contactId = uuid.New()
messageId = uuid.New() messageId = uuid.New()
scheduledMessageId = uuid.New() scheduledMessageId = uuid.New()
testUserId = uuid.New() testUserId = uuid.New()
) )
type CreateScheduledMessageEventRequest struct { type CreateScheduledMessageEventRequest struct {
RecipientId uuid.UUID `json:"recipient_id"` ContactId uuid.UUID `json:"contact_id"`
MessageId uuid.UUID `json:"message_id"` MessageId uuid.UUID `json:"message_id"`
ScheduledMessageId uuid.UUID `json:"scheduled_message_id"` ScheduledMessageId uuid.UUID `json:"scheduled_message_id"`
} }
@@ -44,14 +42,14 @@ func TestCreateScheduledMessageEventWithMock(t *testing.T) {
assert.NoError(t, err, "Error getting app") assert.NoError(t, err, "Error getting app")
handler := NewScheduledMessageEventHandler(apiApp, mockStore, schMsgStore) handler := NewScheduledMessageEventHandler(apiApp, mockStore, schMsgStore)
recipientId := uuid.New() contactId := uuid.New()
messageId := uuid.New() messageId := uuid.New()
scheduledMessageId := uuid.New() scheduledMessageId := uuid.New()
testUserId := uuid.New() testUserId := uuid.New()
con := testContact(recipientId, testUserId) con := ContactTest(contactId, testUserId)
msg := testMessage(messageId, testUserId) msg := MessageTest(messageId, testUserId)
schMsg := testScheduledMessage(scheduledMessageId, testUserId, now) schMsg := ScheduledMessageTest(scheduledMessageId, testUserId, now)
ctx := t.Context() ctx := t.Context()
@@ -63,7 +61,7 @@ func TestCreateScheduledMessageEventWithMock(t *testing.T) {
assert.NoError(t, err, "Error creating scheduled message: %v", err) assert.NoError(t, err, "Error creating scheduled message: %v", err)
} }
testReq := CreateScheduledMessageEventRequest{RecipientId: recipientId, MessageId: messageId, ScheduledMessageId: scheduledMessageId} testReq := CreateScheduledMessageEventRequest{ContactId: contactId, MessageId: messageId, ScheduledMessageId: scheduledMessageId}
jsonValue, _ := json.Marshal(testReq) jsonValue, _ := json.Marshal(testReq)
req, _ := http.NewRequest("POST", endpoint.AddEventToScheduledMessageEndpoint, strings.NewReader(string(jsonValue))) req, _ := http.NewRequest("POST", endpoint.AddEventToScheduledMessageEndpoint, strings.NewReader(string(jsonValue)))
@@ -96,15 +94,15 @@ func TestGetScheduledMessageEventWithMock(t *testing.T) {
assert.NoError(t, err, "Error getting app") assert.NoError(t, err, "Error getting app")
handler := NewScheduledMessageEventHandler(apiApp, schMsgEventStore, schMsgStore) handler := NewScheduledMessageEventHandler(apiApp, schMsgEventStore, schMsgStore)
recipientId := uuid.New() contactId := uuid.New()
messageId := uuid.New() messageId := uuid.New()
scheduledMessageId := uuid.New() scheduledMessageId := uuid.New()
testUserId := uuid.New() testUserId := uuid.New()
con := testContact(recipientId, testUserId) con := ContactTest(contactId, testUserId)
msg := testMessage(messageId, testUserId) msg := MessageTest(messageId, testUserId)
schMsg := testScheduledMessage(scheduledMessageId, testUserId, now) schMsg := ScheduledMessageTest(scheduledMessageId, testUserId, now)
event := testScheduledMessageEvent(msg.Id, con.Id, schMsg.Id) event := ScheduledMessageEventTest(*msg.Id, *con.Id, schMsg.Id)
ctx := t.Context() ctx := t.Context()
@@ -147,10 +145,10 @@ func TestDeleteScheduledMessageEventWithMock(t *testing.T) {
assert.NoError(t, err, "Error getting app") assert.NoError(t, err, "Error getting app")
handler := NewScheduledMessageEventHandler(apiApp, schMsgEventStore, schMsgStore) handler := NewScheduledMessageEventHandler(apiApp, schMsgEventStore, schMsgStore)
con := testContact(recipientId, testUserId) con := ContactTest(contactId, testUserId)
msg := testMessage(messageId, testUserId) msg := MessageTest(messageId, testUserId)
schMsg := testScheduledMessage(scheduledMessageId, testUserId, now) schMsg := ScheduledMessageTest(scheduledMessageId, testUserId, now)
event := testScheduledMessageEvent(msg.Id, con.Id, schMsg.Id) event := ScheduledMessageEventTest(*msg.Id, *con.Id, schMsg.Id)
ctx := t.Context() ctx := t.Context()
@@ -182,31 +180,3 @@ func TestDeleteScheduledMessageEventWithMock(t *testing.T) {
assert.NotEmpty(t, msgEvent.Created, "Created date should not be empty") 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}
}
+8 -8
View File
@@ -31,7 +31,7 @@ func TestCreateScheduledMessageWithMock(t *testing.T) {
handler := NewScheduledMessageHandler(apiApp, mockStore) handler := NewScheduledMessageHandler(apiApp, mockStore)
testUserId := uuid.New() testUserId := uuid.New()
testBody := testCreateScheduledMessageRequest(testUserId, now) testBody := createScheduledMessageRequest(testUserId, now)
jsonValue, _ := json.Marshal(testBody) jsonValue, _ := json.Marshal(testBody)
req, _ := http.NewRequest("POST", endpoint.ScheduleMessageEndpoint, strings.NewReader(string(jsonValue))) req, _ := http.NewRequest("POST", endpoint.ScheduleMessageEndpoint, strings.NewReader(string(jsonValue)))
@@ -55,7 +55,7 @@ func TestGetScheduledMessageWithMock(t *testing.T) {
mockStore := mock.NewMockScheduledMessageStore() mockStore := mock.NewMockScheduledMessageStore()
testUserId := uuid.New() testUserId := uuid.New()
testBody := testCreateScheduledMessageRequest(testUserId, now) testBody := createScheduledMessageRequest(testUserId, now)
ctx := t.Context() ctx := t.Context()
schMsg := scheduling.ScheduledMessage{} schMsg := scheduling.ScheduledMessage{}
@@ -89,15 +89,15 @@ func TestFetchScheduledMessageWithMock(t *testing.T) {
messageStore := mock.NewMockMessageStore() messageStore := mock.NewMockMessageStore()
schMsgEventStore := mock.NewMockScheduledMessageEventStore() schMsgEventStore := mock.NewMockScheduledMessageEventStore()
recipientId := uuid.New() contactId := uuid.New()
messageId := uuid.New() messageId := uuid.New()
scheduledMessageId := uuid.New() scheduledMessageId := uuid.New()
testUserId := uuid.New() testUserId := uuid.New()
con := testContact(recipientId, testUserId) con := ContactTest(contactId, testUserId)
msg := testMessage(messageId, testUserId) msg := MessageTest(messageId, testUserId)
schMsg := testScheduledMessage(scheduledMessageId, testUserId, now) schMsg := ScheduledMessageTest(scheduledMessageId, testUserId, now)
event := testScheduledMessageEvent(msg.Id, con.Id, schMsg.Id) event := ScheduledMessageEventTest(*msg.Id, *con.Id, schMsg.Id)
ctx := t.Context() ctx := t.Context()
@@ -135,7 +135,7 @@ func TestFetchScheduledMessageWithMock(t *testing.T) {
assert.Equal(t, scheduling.Processing, fetchedSchMsg.Status, "The statuses do not match") assert.Equal(t, scheduling.Processing, fetchedSchMsg.Status, "The statuses do not match")
} }
func testCreateScheduledMessageRequest(userId uuid.UUID, now time.Time) CreateScheduledMessageRequest { func createScheduledMessageRequest(userId uuid.UUID, now time.Time) CreateScheduledMessageRequest {
scheduled := now.Add(5 * time.Minute) scheduled := now.Add(5 * time.Minute)
return CreateScheduledMessageRequest{Status: scheduling.Pending, UserId: userId, Scheduled: scheduled} return CreateScheduledMessageRequest{Status: scheduling.Pending, UserId: userId, Scheduled: scheduled}
} }
+32 -7
View File
@@ -4,13 +4,14 @@ import (
"context" "context"
"fmt" "fmt"
"git.kundeng.us/phoenix/textsender-models/tx0/message" "git.kundeng.us/phoenix/textsender-models/tx0/message/event"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/pgxpool"
) )
type MessageEventResponseStore interface { type MessageEventResponseStore interface {
Create(ctx context.Context, mer *message.MessageEventResponse) error Create(ctx context.Context, mer *event.MessageEventResponse) error
GetWithUserId(ctx context.Context, userId uuid.UUID) ([]*event.MessageEventResponse, error)
} }
type PGMessageEventResponseStore struct { type PGMessageEventResponseStore struct {
@@ -21,16 +22,40 @@ func NewMessageEventResponseStore(db *pgxpool.Pool) *PGMessageEventResponseStore
return &PGMessageEventResponseStore{db: db} return &PGMessageEventResponseStore{db: db}
} }
func (m *PGMessageEventResponseStore) Create(ctx context.Context, mer *message.MessageEventResponse) error { func (m *PGMessageEventResponseStore) Create(ctx context.Context, mer *event.MessageEventResponse) error {
var query string var query string
queryBase := "INSERT INTO message_event_responses" queryBase := "INSERT INTO message_event_responses"
queryReturn := "RETURNING id" queryReturn := "RETURNING id"
if mer.ScheduledMessageEventId == uuid.Nil { if mer.ScheduledMessageEventId == uuid.Nil {
query = fmt.Sprintf("%s (response, user_id, sent) VALUES ($1, $2, $3) %s", queryBase, queryReturn) query = fmt.Sprintf("%s (response, user_id, sent, contact_id, message_id, status) VALUES ($1, $2, $3, $4, $5, $6) %s", queryBase, queryReturn)
return m.db.QueryRow(ctx, query, mer.Response, mer.UserId, mer.Sent).Scan(&mer.Id) return m.db.QueryRow(ctx, query, mer.Response, mer.UserId, mer.Sent, mer.ContactId, mer.MessageId, mer.Status).Scan(&mer.Id)
} else { } else {
query = fmt.Sprintf("%s (scheduled_message_event_id, response, user_id, sent) %s", queryBase, queryReturn) query = fmt.Sprintf("%s (scheduled_message_event_id, response, user_id, sent, contact_id, message_id, status) VALUES ($1, $2, $3, $4, $5, $6, $7) %s", queryBase, queryReturn)
return m.db.QueryRow(ctx, query, mer.ScheduledMessageEventId, mer.Response, mer.UserId, mer.Sent).Scan(&mer.Id) return m.db.QueryRow(ctx, query, mer.ScheduledMessageEventId, mer.Response, mer.UserId, mer.Sent, mer.ContactId, mer.MessageId, mer.Status).Scan(&mer.Id)
} }
} }
func (m *PGMessageEventResponseStore) GetWithUserId(ctx context.Context, userId uuid.UUID) ([]*event.MessageEventResponse, error) {
query := "SELECT id, scheduled_message_event_id, response, user_id, sent, contact_id, message_id, status FROM message_event_responses WHERE user_id = $1"
rows, err := m.db.Query(ctx, query, userId)
if err != nil {
return nil, fmt.Errorf("Error querying: %w", err)
}
defer rows.Close()
var sentMessages []*event.MessageEventResponse
for rows.Next() {
var msg event.MessageEventResponse
if err := rows.Scan(&msg.Id, &msg.ScheduledMessageEventId, &msg.Response, &msg.UserId, &msg.Sent, &msg.ContactId, &msg.MessageId, &msg.Status); err != nil {
return nil, fmt.Errorf("Scanning row: %w", err)
}
sentMessages = append(sentMessages, &msg)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("Iterating message_event_responses: %w", err)
}
return sentMessages, nil
}
+118
View File
@@ -0,0 +1,118 @@
package mock
import (
"context"
"errors"
"sync"
"git.kundeng.us/phoenix/textsender-models/tx0/contact"
"github.com/google/uuid"
)
type Key struct {
PhoneNumber string
UserId uuid.UUID
}
type MockContactStore struct {
Contacts map[uuid.UUID]*contact.Contact
ContactsByKey map[Key]*contact.Contact
mu sync.RWMutex
Error error // Optional: simulate errors
}
func NewMockContactStore() *MockContactStore {
return &MockContactStore{
Contacts: make(map[uuid.UUID]*contact.Contact),
ContactsByKey: make(map[Key]*contact.Contact),
}
}
func (m *MockContactStore) CreateContact(ctx context.Context, con *contact.Contact) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return m.Error
}
var id uuid.UUID
if con.Id == nil {
id = uuid.New()
con.Id = &id
}
key := Key{PhoneNumber: con.PhoneNumber, UserId: *con.UserId}
if _, exists := m.ContactsByKey[key]; exists {
return errors.New("Contact with phone number already exists")
}
m.Contacts[*con.Id] = con
m.ContactsByKey[key] = con
return nil
}
func (m *MockContactStore) GetContactByID(ctx context.Context, id uuid.UUID) (*contact.Contact, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return nil, m.Error
}
if m.Error != nil {
return nil, m.Error
}
con, exists := m.Contacts[id]
if !exists {
return nil, errors.New("Contact not found")
}
return con, nil
}
func (m *MockContactStore) GetContactByPhone(ctx context.Context, phoneNumber string, userId uuid.UUID) (*contact.Contact, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return nil, m.Error
}
con, exists := m.ContactsByKey[Key{PhoneNumber: phoneNumber, UserId: userId}]
if !exists {
return nil, errors.New("Contact not found")
}
return con, nil
}
func (m *MockContactStore) GetAllContacts(ctx context.Context) ([]*contact.Contact, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return nil, m.Error
}
cons := make([]*contact.Contact, 0, len(m.Contacts))
for _, con := range m.Contacts {
cons = append(cons, con)
}
return cons, nil
}
func (m *MockContactStore) ContactExists(ctx context.Context, phoneNumber string, userId uuid.UUID) (bool, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return false, m.Error
}
_, exists := m.ContactsByKey[Key{PhoneNumber: phoneNumber, UserId: userId}]
return exists, nil
}
@@ -6,7 +6,7 @@ import (
"sync" "sync"
"time" "time"
"git.kundeng.us/phoenix/textsender-models/tx0/message" "git.kundeng.us/phoenix/textsender-models/tx0/message/event"
"github.com/google/uuid" "github.com/google/uuid"
) )
@@ -15,20 +15,20 @@ type MessageEventResponseKey struct {
UserId uuid.UUID UserId uuid.UUID
} }
type MockMessageEventResponseStore struct { type MockMessageEventResponseStore struct {
MessageEventResponses map[uuid.UUID]*message.MessageEventResponse MessageEventResponses map[uuid.UUID]*event.MessageEventResponse
MessageEventResponsesByKey map[MessageEventResponseKey]*message.MessageEventResponse MessageEventResponsesByKey map[MessageEventResponseKey]*event.MessageEventResponse
mu sync.RWMutex mu sync.RWMutex
Error error Error error
} }
func NewMockMessageEventResponseStore() *MockMessageEventResponseStore { func NewMockMessageEventResponseStore() *MockMessageEventResponseStore {
return &MockMessageEventResponseStore{ return &MockMessageEventResponseStore{
MessageEventResponses: make(map[uuid.UUID]*message.MessageEventResponse), MessageEventResponses: make(map[uuid.UUID]*event.MessageEventResponse),
MessageEventResponsesByKey: make(map[MessageEventResponseKey]*message.MessageEventResponse), MessageEventResponsesByKey: make(map[MessageEventResponseKey]*event.MessageEventResponse),
} }
} }
func (m *MockMessageEventResponseStore) Create(ctx context.Context, mer *message.MessageEventResponse) error { func (m *MockMessageEventResponseStore) Create(ctx context.Context, mer *event.MessageEventResponse) error {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
@@ -60,3 +60,22 @@ func (m *MockMessageEventResponseStore) Create(ctx context.Context, mer *message
return nil return nil
} }
} }
// TODO: Add code to get mock MessageEventResponse
func (m *MockMessageEventResponseStore) GetWithUserId(ctx context.Context, userId uuid.UUID) ([]*event.MessageEventResponse, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return nil, m.Error
}
var messages []*event.MessageEventResponse
for _, msg := range m.MessageEventResponses {
if msg.UserId == userId {
messages = append(messages, msg)
}
}
return messages, nil
}
@@ -11,7 +11,7 @@ import (
) )
type ScheduledMessageEventKey struct { type ScheduledMessageEventKey struct {
RecipientId uuid.UUID ContactId uuid.UUID
MessageId uuid.UUID MessageId uuid.UUID
ScheduledMessageId uuid.UUID ScheduledMessageId uuid.UUID
} }
@@ -94,7 +94,7 @@ func (m *MockScheduledMessageEventStore) Delete(ctx context.Context, id uuid.UUI
copiedEventsKey := make(map[ScheduledMessageEventKey]*scheduling.ScheduledMessageEvent) copiedEventsKey := make(map[ScheduledMessageEventKey]*scheduling.ScheduledMessageEvent)
for i, schMsgEvent := range m.ScheduledMessageEvents { for i, schMsgEvent := range m.ScheduledMessageEvents {
key := ScheduledMessageEventKey{RecipientId: schMsgEvent.RecipientId, MessageId: schMsgEvent.MessageId, ScheduledMessageId: schMsgEvent.ScheduledMessageId} key := ScheduledMessageEventKey{ContactId: schMsgEvent.ContactId, MessageId: schMsgEvent.MessageId, ScheduledMessageId: schMsgEvent.ScheduledMessageId}
if schMsgEvent.Id != id { if schMsgEvent.Id != id {
copiedEvents[i] = schMsgEvent copiedEvents[i] = schMsgEvent
copiedEventsKey[key] = schMsgEvent copiedEventsKey[key] = schMsgEvent
@@ -132,7 +132,7 @@ func (m *MockScheduledMessageEventStore) CreateScheduledMessageEvent(ctx context
event.Id = uuid.New() event.Id = uuid.New()
} }
key := ScheduledMessageEventKey{RecipientId: event.RecipientId, MessageId: event.MessageId, ScheduledMessageId: event.ScheduledMessageId} key := ScheduledMessageEventKey{ContactId: event.ContactId, MessageId: event.MessageId, ScheduledMessageId: event.ScheduledMessageId}
if _, exists := m.ScheduledMessageEventsByKey[key]; exists { if _, exists := m.ScheduledMessageEventsByKey[key]; exists {
return fmt.Errorf("Already exists") return fmt.Errorf("Already exists")
@@ -152,7 +152,7 @@ func (m *MockScheduledMessageEventStore) Exists(ctx context.Context, event *sche
return false, m.Error return false, m.Error
} }
key := ScheduledMessageEventKey{RecipientId: event.RecipientId, MessageId: event.MessageId, ScheduledMessageId: event.ScheduledMessageId} key := ScheduledMessageEventKey{ContactId: event.ContactId, MessageId: event.MessageId, ScheduledMessageId: event.ScheduledMessageId}
_, exists := m.ScheduledMessageEventsByKey[key] _, exists := m.ScheduledMessageEventsByKey[key]
return exists, nil return exists, nil
+12 -112
View File
@@ -3,119 +3,13 @@ package mock
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"sync" "sync"
"git.kundeng.us/phoenix/textsender-models/tx0/contact"
"git.kundeng.us/phoenix/textsender-models/tx0/message" "git.kundeng.us/phoenix/textsender-models/tx0/message"
"github.com/google/uuid" "github.com/google/uuid"
) )
type Key struct {
PhoneNumber string
UserId uuid.UUID
}
type MockContactStore struct {
Contacts map[uuid.UUID]*contact.Contact
ContactsByKey map[Key]*contact.Contact
mu sync.RWMutex
Error error // Optional: simulate errors
}
func NewMockContactStore() *MockContactStore {
return &MockContactStore{
Contacts: make(map[uuid.UUID]*contact.Contact),
ContactsByKey: make(map[Key]*contact.Contact),
}
}
func (m *MockContactStore) CreateContact(ctx context.Context, con *contact.Contact) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return m.Error
}
if con.Id == uuid.Nil {
con.Id = uuid.New()
}
key := Key{PhoneNumber: con.PhoneNumber, UserId: con.UserId}
if _, exists := m.ContactsByKey[key]; exists {
return errors.New("Contact with phone number already exists")
}
m.Contacts[con.Id] = con
m.ContactsByKey[key] = con
return nil
}
func (m *MockContactStore) GetContactByID(ctx context.Context, id uuid.UUID) (*contact.Contact, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return nil, m.Error
}
if m.Error != nil {
return nil, m.Error
}
con, exists := m.Contacts[id]
if !exists {
return nil, errors.New("Contact not found")
}
return con, nil
}
func (m *MockContactStore) GetContactByPhone(ctx context.Context, phoneNumber string, userId uuid.UUID) (*contact.Contact, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return nil, m.Error
}
con, exists := m.ContactsByKey[Key{PhoneNumber: phoneNumber, UserId: userId}]
if !exists {
return nil, errors.New("Contact not found")
}
return con, nil
}
func (m *MockContactStore) GetAllContacts(ctx context.Context) ([]*contact.Contact, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return nil, m.Error
}
cons := make([]*contact.Contact, 0, len(m.Contacts))
for _, con := range m.Contacts {
cons = append(cons, con)
}
return cons, nil
}
func (m *MockContactStore) ContactExists(ctx context.Context, phoneNumber string, userId uuid.UUID) (bool, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return false, m.Error
}
_, exists := m.ContactsByKey[Key{PhoneNumber: phoneNumber, UserId: userId}]
return exists, nil
}
type MessageKey struct { type MessageKey struct {
Content string Content string
UserId uuid.UUID UserId uuid.UUID
@@ -143,16 +37,22 @@ func (m *MockMessageStore) CreateMessage(ctx context.Context, msg *message.Messa
return m.Error return m.Error
} }
if msg.Id == uuid.Nil { var id uuid.UUID
msg.Id = uuid.New() if msg != nil {
if msg.Id == nil || *msg.Id == uuid.Nil {
id = uuid.New()
msg.Id = &id
}
} else {
return fmt.Errorf("Message not populated")
} }
key := MessageKey{Content: msg.Content, UserId: msg.UserId} key := MessageKey{Content: msg.Content, UserId: *msg.UserId}
if _, exists := m.MessagesByKey[key]; exists { if _, exists := m.MessagesByKey[key]; exists {
return errors.New("Message already exists") return errors.New("Message already exists")
} }
m.Messages[msg.Id] = msg m.Messages[*msg.Id] = msg
m.MessagesByKey[key] = msg m.MessagesByKey[key] = msg
return nil return nil
} }
@@ -195,7 +95,7 @@ func (m *MockMessageStore) MessageExists(ctx context.Context, msg *message.Messa
return false, m.Error return false, m.Error
} }
_, exists := m.MessagesByKey[MessageKey{Content: msg.Content, UserId: msg.UserId}] _, exists := m.MessagesByKey[MessageKey{Content: msg.Content, UserId: *msg.UserId}]
return exists, nil return exists, nil
} }
@@ -27,11 +27,11 @@ func NewScheduledMessageEventStore(db *pgxpool.Pool) *PGScheduledMessageEventSto
} }
func (s *PGScheduledMessageEventStore) Get(ctx context.Context, id uuid.UUID) (*scheduling.ScheduledMessageEvent, error) { func (s *PGScheduledMessageEventStore) Get(ctx context.Context, id uuid.UUID) (*scheduling.ScheduledMessageEvent, error) {
query := `SELECT id, recipient_id, message_id, scheduled_message_id, created FROM scheduled_message_events WHERE id = $1` query := `SELECT id, contact_id, message_id, scheduled_message_id, created FROM scheduled_message_events WHERE id = $1`
var event scheduling.ScheduledMessageEvent var event scheduling.ScheduledMessageEvent
err := s.db.QueryRow(ctx, query, id).Scan( err := s.db.QueryRow(ctx, query, id).Scan(
&event.Id, &event.RecipientId, &event.MessageId, &event.ScheduledMessageId, &event.Created, &event.Id, &event.ContactId, &event.MessageId, &event.ScheduledMessageId, &event.Created,
) )
if err == pgx.ErrNoRows { if err == pgx.ErrNoRows {
@@ -45,7 +45,7 @@ func (s *PGScheduledMessageEventStore) Get(ctx context.Context, id uuid.UUID) (*
func (s *PGScheduledMessageEventStore) GetWithScheduleMessageId(ctx context.Context, schMsgId uuid.UUID) ([]*scheduling.ScheduledMessageEvent, error) { func (s *PGScheduledMessageEventStore) GetWithScheduleMessageId(ctx context.Context, schMsgId uuid.UUID) ([]*scheduling.ScheduledMessageEvent, error) {
var events []*scheduling.ScheduledMessageEvent var events []*scheduling.ScheduledMessageEvent
query := `SELECT id, recipient_id, message_id, scheduled_message_id, created FROM scheduled_message_events WHERE scheduled_message_id = $1 query := `SELECT id, contact_id, message_id, scheduled_message_id, created FROM scheduled_message_events WHERE scheduled_message_id = $1
` `
rows, err := s.db.Query(ctx, query, schMsgId) rows, err := s.db.Query(ctx, query, schMsgId)
@@ -57,7 +57,7 @@ func (s *PGScheduledMessageEventStore) GetWithScheduleMessageId(ctx context.Cont
for rows.Next() { for rows.Next() {
var event scheduling.ScheduledMessageEvent var event scheduling.ScheduledMessageEvent
if err := rows.Scan(&event.Id, &event.RecipientId, &event.MessageId, &event.ScheduledMessageId, &event.Created); err != nil { if err := rows.Scan(&event.Id, &event.ContactId, &event.MessageId, &event.ScheduledMessageId, &event.Created); err != nil {
return nil, fmt.Errorf("Error fetching data from row: %w", err) return nil, fmt.Errorf("Error fetching data from row: %w", err)
} else { } else {
events = append(events, &event) events = append(events, &event)
@@ -90,21 +90,21 @@ func (s *PGScheduledMessageEventStore) Delete(ctx context.Context, id uuid.UUID)
func (s *PGScheduledMessageEventStore) CreateScheduledMessageEvent(ctx context.Context, event *scheduling.ScheduledMessageEvent) error { func (s *PGScheduledMessageEventStore) CreateScheduledMessageEvent(ctx context.Context, event *scheduling.ScheduledMessageEvent) error {
query := ` query := `
INSERT INTO scheduled_message_events (recipient_id, message_id, scheduled_message_id) INSERT INTO scheduled_message_events (contact_id, message_id, scheduled_message_id)
VALUES ($1, $2, $3) VALUES ($1, $2, $3)
RETURNING id, created RETURNING id, created
` `
return s.db.QueryRow(ctx, query, event.RecipientId, event.MessageId, event.ScheduledMessageId).Scan( return s.db.QueryRow(ctx, query, event.ContactId, event.MessageId, event.ScheduledMessageId).Scan(
&event.Id, &event.Created, &event.Id, &event.Created,
) )
} }
func (s *PGScheduledMessageEventStore) Exists(ctx context.Context, event *scheduling.ScheduledMessageEvent) (bool, error) { func (s *PGScheduledMessageEventStore) Exists(ctx context.Context, event *scheduling.ScheduledMessageEvent) (bool, error) {
query := `SELECT EXISTS(SELECT 1 FROM scheduled_message_events WHERE scheduled_message_id = $1 AND recipient_id = $2)` query := `SELECT EXISTS(SELECT 1 FROM scheduled_message_events WHERE scheduled_message_id = $1 AND contact_id = $2)`
var exists bool var exists bool
err := s.db.QueryRow(ctx, query, event.ScheduledMessageId, event.RecipientId).Scan(&exists) err := s.db.QueryRow(ctx, query, event.ScheduledMessageId, event.ContactId).Scan(&exists)
if err != nil { if err != nil {
return false, fmt.Errorf("Scheduled message event does not exist: %w", err) return false, fmt.Errorf("Scheduled message event does not exist: %w", err)
} else { } else {
+5 -2
View File
@@ -28,7 +28,7 @@ CREATE TABLE IF NOT EXISTS scheduled_messages (
CREATE TABLE IF NOT EXISTS scheduled_message_events ( CREATE TABLE IF NOT EXISTS scheduled_message_events (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
recipient_id UUID NOT NULL, contact_id UUID NOT NULL,
message_id UUID NOT NULL, message_id UUID NOT NULL,
scheduled_message_id UUID NOT NULL, scheduled_message_id UUID NOT NULL,
created timestamptz DEFAULT now() created timestamptz DEFAULT now()
@@ -39,5 +39,8 @@ CREATE TABLE IF NOT EXISTS message_event_responses (
scheduled_message_event_id UUID NULL, scheduled_message_event_id UUID NULL,
response JSONB NOT NULL, response JSONB NOT NULL,
user_id UUID NOT NULL, user_id UUID NOT NULL,
sent timestamptz NOT NULL sent timestamptz NOT NULL,
contact_id UUID NULL,
message_id UUID NULL,
status TEXT CHECK (status IN ('INSTANT', 'SCHEDULED'))
); );