39 Commits
Author SHA1 Message Date
phoenix 94b80aeecc Docker fixes (#1)
Go / build (push) Successful in 8s
Go / Test (push) Successful in 16s
Go / build (pull_request) Successful in 1m46s
Go / Test (pull_request) Successful in 1m47s
Reviewed-on: phoenix/textsender-api#1
2026-05-29 19:11:17 -04:00
phoenixandphoenix 9dc4336761 Update go (#69)
Go / build (pull_request) Failing after 10s
Go / Test (pull_request) Failing after 1m33s
Reviewed-on: phoenix/textsender-api#69
Co-authored-by: phoenix <mail@kundeng.us>
Co-committed-by: phoenix <mail@kundeng.us>
2026-05-03 17:16:15 -04:00
phoenixandphoenix 69a20cfbbe Update go (#68)
Reviewed-on: phoenix/textsender-api#68
Co-authored-by: phoenix <mail@kundeng.us>
Co-committed-by: phoenix <mail@kundeng.us>
2026-04-04 22:16:49 -04:00
phoenixandphoenix a51979e724 tsk-58: Update names of Contact endpoint (#67)
Closes #58

Reviewed-on: phoenix/textsender-api#67
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2026-01-07 20:24:30 +00:00
phoenixandphoenix 4e250d095c tsk-60: Save extra fields to DB for Contact (#66)
Closes #60

Reviewed-on: phoenix/textsender-api#66
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2026-01-06 02:40:01 +00:00
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
37 changed files with 1782 additions and 568 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"
+8 -3
View File
@@ -16,7 +16,7 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v6 uses: actions/setup-go@v6
with: with:
go-version: '1.25.4' # You can specify a specific version or 'stable' go-version: '1.26.2'
- name: Build - name: Build
run: | run: |
@@ -36,6 +36,8 @@ jobs:
cat > ~/.gitconfig << "EOF" cat > ~/.gitconfig << "EOF"
[url "ssh://git@${{ secrets.GIT_HOST_ROOT }}"] [url "ssh://git@${{ secrets.GIT_HOST_ROOT }}"]
insteadOf = https://${{ secrets.GIT_HOST_ROOT }} insteadOf = https://${{ secrets.GIT_HOST_ROOT }}
[url "ssh://git@${{ secrets.GIT_HOST_ROOT }}"]
insteadOf = http://${{ secrets.GIT_HOST_ROOT }}
EOF EOF
echo "Building binary" echo "Building binary"
@@ -49,7 +51,7 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
services: services:
postgres: postgres:
image: postgres:18.0 image: postgres:18.4-alpine
env: env:
POSTGRES_USER: ${{ secrets.DB_TEST_USER }} POSTGRES_USER: ${{ secrets.DB_TEST_USER }}
POSTGRES_PASSWORD: ${{ secrets.DB_TEST_PASSWORD }} POSTGRES_PASSWORD: ${{ secrets.DB_TEST_PASSWORD }}
@@ -68,7 +70,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v4 uses: actions/setup-go@v4
with: with:
go-version: '1.25.3' go-version: '1.26.2'
- name: Install PostgreSQL client - name: Install PostgreSQL client
run: sudo apt update && sudo apt-get install -y postgresql-client run: sudo apt update && sudo apt-get install -y postgresql-client
@@ -107,6 +109,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
@@ -124,6 +127,8 @@ jobs:
cat > ~/.gitconfig << "EOF" cat > ~/.gitconfig << "EOF"
[url "ssh://git@${{ secrets.GIT_HOST_ROOT }}"] [url "ssh://git@${{ secrets.GIT_HOST_ROOT }}"]
insteadOf = https://${{ secrets.GIT_HOST_ROOT }} insteadOf = https://${{ secrets.GIT_HOST_ROOT }}
[url "ssh://git@${{ secrets.GIT_HOST_ROOT }}"]
insteadOf = http://${{ secrets.GIT_HOST_ROOT }}
EOF EOF
go test -v ./... go test -v ./...
+4 -3
View File
@@ -1,5 +1,5 @@
# Multi-stage Dockerfile for Go application # Multi-stage Dockerfile for Go application
FROM golang:1.25.4 AS builder FROM golang:1.26.2 AS builder
WORKDIR /app WORKDIR /app
@@ -11,7 +11,8 @@ RUN mkdir -p -m 0700 ~/.ssh && \
ssh-keyscan git.kundeng.us >> ~/.ssh/known_hosts ssh-keyscan git.kundeng.us >> ~/.ssh/known_hosts
# Configure Git to use SSH for GitHub # Configure Git to use SSH for GitHub
RUN git config --global url."ssh://git@git.kundeng.us".insteadOf "https://git.kundeng.us" RUN git config --global url."git@git.kundeng.us:".insteadOf "https://git.kundeng.us/" && \
git config --global url."git@git.kundeng.us:".insteadOf "http://git.kundeng.us/"
# Set up the Go environment for private modules # Set up the Go environment for private modules
ENV GOPRIVATE=git.kundeng.us ENV GOPRIVATE=git.kundeng.us
@@ -20,7 +21,7 @@ ENV GOPRIVATE=git.kundeng.us
# Copy go mod and sum files # Copy go mod and sum files
COPY go.mod go.sum ./ COPY go.mod go.sum ./
RUN --mount=type=ssh mkdir src && \ RUN --mount=type=ssh \
go mod download go mod download
# Copy source code # Copy source code
+18 -6
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,24 +93,35 @@ 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))
router.Use(mdlware.JSONContentType) router.Use(mdlware.JSONContentType)
router.Handle(endpoint.ADD_CONTACT_ENDPOINT, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(contactHandler.AddContact))) router.Method("POST", endpoint.ADD_CONTACT_ENDPOINT, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(contactHandler.AddContact)))
router.Handle(endpoint.GET_CONTACT, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(contactHandler.GetContact))) router.Method("GET", endpoint.GET_CONTACT, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(contactHandler.GetContact)))
router.Handle(endpoint.ADD_MESSAGE, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(messageHandler.AddMessage))) router.Method("POST", endpoint.ADD_MESSAGE, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(messageHandler.AddMessage)))
router.Handle(endpoint.GET_MESSAGE, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(messageHandler.GetMessage))) router.Method("GET", endpoint.GET_MESSAGE, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(messageHandler.GetMessage)))
router.Handle(endpoint.ScheduleMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageHandler.AddScheduledMessage))) router.Method("POST", endpoint.ScheduleMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageHandler.AddScheduledMessage)))
router.Handle(endpoint.AddEventToScheduledMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageEventHandler.AddScheduledMessageEvent))) router.Method("POST", endpoint.AddEventToScheduledMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageEventHandler.AddScheduledMessageEvent)))
router.Method("GET", endpoint.GetScheduledMessageEventEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageEventHandler.GetScheduledMessageEvent))) router.Method("GET", endpoint.GetScheduledMessageEventEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageEventHandler.GetScheduledMessageEvent)))
router.Method("DELETE", endpoint.DeleteScheduledMessageEventEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageEventHandler.DeleteScheduledMessageEvent))) router.Method("DELETE", endpoint.DeleteScheduledMessageEventEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageEventHandler.DeleteScheduledMessageEvent)))
router.Method("PATCH", endpoint.UpdateScheduledMessageStatusEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageStatusHandler.UpdateStatus))) router.Method("PATCH", endpoint.UpdateScheduledMessageStatusEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageStatusHandler.UpdateStatus)))
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("PATCH", endpoint.Update_Names_Endpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(contactHandler.UpdateName)))
router.Method("GET", "/swagger/*", httpSwagger.Handler( router.Method("GET", "/swagger/*", httpSwagger.Handler(
httpSwagger.URL(fmt.Sprintf("http://localhost:%s/swagger/doc.json", config.PORT)), httpSwagger.URL(fmt.Sprintf("http://localhost:%s/swagger/doc.json", config.PORT)),
+11 -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,9 @@ 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)))
testRouter.Method("PATCH", endpoint.Update_Names_Endpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(contactHandler.UpdateName)))
code := m.Run() code := m.Run()
os.Exit(code) os.Exit(code)
+4 -4
View File
@@ -53,7 +53,7 @@ services:
# PostgreSQL Database Service # PostgreSQL Database Service
main_db: main_db:
image: postgres:18.0-alpine # Use an official Postgres image (Alpine variant is smaller) image: postgres:18.4-alpine # Use an official Postgres image (Alpine variant is smaller)
container_name: textsender_db # Optional: Give the container a specific name container_name: textsender_db # Optional: Give the container a specific name
environment: environment:
# These MUST match the user, password, and database name in the DATABASE_URL above # These MUST match the user, password, and database name in the DATABASE_URL above
@@ -62,7 +62,7 @@ services:
POSTGRES_DB: ${POSTGRES_AUTH_DB:-textsender_db} POSTGRES_DB: ${POSTGRES_AUTH_DB:-textsender_db}
volumes: volumes:
# Persist database data using a named volume # Persist database data using a named volume
- postgres_data:/var/lib/postgresql/data - postgres_data:/var/lib/postgresql
ports: ports:
- "5432:5432" - "5432:5432"
healthcheck: healthcheck:
@@ -77,7 +77,7 @@ services:
restart: always # Optional: Restart policy restart: always # Optional: Restart policy
auth_db: auth_db:
image: postgres:18.0-alpine # Use an official Postgres image (Alpine variant is smaller) image: postgres:18.4-alpine # Use an official Postgres image (Alpine variant is smaller)
container_name: textsender_auth_db # Optional: Give the container a specific name container_name: textsender_auth_db # Optional: Give the container a specific name
environment: environment:
# These MUST match the user, password, and database name in the DATABASE_URL above # These MUST match the user, password, and database name in the DATABASE_URL above
@@ -86,7 +86,7 @@ services:
POSTGRES_DB: ${POSTGRES_AUTH_DB:-textsender_auth_db} POSTGRES_DB: ${POSTGRES_AUTH_DB:-textsender_auth_db}
volumes: volumes:
# Persist database data using a named volume # Persist database data using a named volume
- postgres_data_auth:/var/lib/postgresql/data - postgres_data_auth:/var/lib/postgresql
ports: ports:
- "5433:5432" - "5433:5432"
healthcheck: healthcheck:
+294 -29
View File
@@ -122,6 +122,57 @@ const docTemplate = `{
} }
} }
}, },
"/contact/update": {
"patch": {
"security": [
{
"BearerAuth": []
}
],
"description": "Update the first, last, or nickname of a Contact (requires JWT)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"contacts"
],
"summary": "Update names of Contact",
"parameters": [
{
"description": "Data to update contact's names",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handler.UpdateNameRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.UpdateNameResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handler.UpdateNameResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handler.UpdateNameResponse"
}
}
}
}
},
"/instant/message": { "/instant/message": {
"post": { "post": {
"security": [ "security": [
@@ -490,6 +541,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 +793,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 +813,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 +941,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,9 +997,59 @@ 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": {
"first_name": {
"type": "string"
},
"last_name": {
"type": "string"
},
"nickname": {
"type": "string"
},
"phone_number": { "phone_number": {
"type": "string" "type": "string"
}, },
@@ -832,10 +1086,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 +1156,41 @@ const docTemplate = `{
"data": { "data": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/message.MessageEventResponse" "$ref": "#/definitions/event.MessageEventResponse"
}
},
"message": {
"type": "string"
}
}
},
"handler.UpdateNameRequest": {
"type": "object",
"properties": {
"contact_id": {
"type": "string"
},
"first_name": {
"type": "string"
},
"last_name": {
"type": "string"
},
"nickname": {
"type": "string"
},
"user_id": {
"type": "string"
}
}
},
"handler.UpdateNameResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/contact.Contact"
} }
}, },
"message": { "message": {
@@ -924,29 +1212,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 +1235,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 +1247,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"
} }
+294 -29
View File
@@ -116,6 +116,57 @@
} }
} }
}, },
"/contact/update": {
"patch": {
"security": [
{
"BearerAuth": []
}
],
"description": "Update the first, last, or nickname of a Contact (requires JWT)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"contacts"
],
"summary": "Update names of Contact",
"parameters": [
{
"description": "Data to update contact's names",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handler.UpdateNameRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.UpdateNameResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handler.UpdateNameResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handler.UpdateNameResponse"
}
}
}
}
},
"/instant/message": { "/instant/message": {
"post": { "post": {
"security": [ "security": [
@@ -484,6 +535,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 +787,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 +807,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 +935,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,9 +991,59 @@
} }
} }
}, },
"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": {
"first_name": {
"type": "string"
},
"last_name": {
"type": "string"
},
"nickname": {
"type": "string"
},
"phone_number": { "phone_number": {
"type": "string" "type": "string"
}, },
@@ -826,10 +1080,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 +1150,41 @@
"data": { "data": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/message.MessageEventResponse" "$ref": "#/definitions/event.MessageEventResponse"
}
},
"message": {
"type": "string"
}
}
},
"handler.UpdateNameRequest": {
"type": "object",
"properties": {
"contact_id": {
"type": "string"
},
"first_name": {
"type": "string"
},
"last_name": {
"type": "string"
},
"nickname": {
"type": "string"
},
"user_id": {
"type": "string"
}
}
},
"handler.UpdateNameResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/contact.Contact"
} }
}, },
"message": { "message": {
@@ -918,29 +1206,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 +1229,9 @@
"scheduling.ScheduledMessageEvent": { "scheduling.ScheduledMessageEvent": {
"type": "object", "type": "object",
"properties": { "properties": {
"contact_id": {
"type": "string"
},
"created": { "created": {
"type": "string" "type": "string"
}, },
@@ -973,9 +1241,6 @@
"message_id": { "message_id": {
"type": "string" "type": "string"
}, },
"recipient_id": {
"type": "string"
},
"scheduled_message_id": { "scheduled_message_id": {
"type": "string" "type": "string"
} }
+190 -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,8 +134,41 @@ 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:
first_name:
type: string
last_name:
type: string
nickname:
type: string
phone_number: phone_number:
type: string type: string
user_id: user_id:
@@ -124,9 +192,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 +237,29 @@ definitions:
properties: properties:
data: data:
items: items:
$ref: '#/definitions/message.MessageEventResponse' $ref: '#/definitions/event.MessageEventResponse'
type: array
message:
type: string
type: object
handler.UpdateNameRequest:
properties:
contact_id:
type: string
first_name:
type: string
last_name:
type: string
nickname:
type: string
user_id:
type: string
type: object
handler.UpdateNameResponse:
properties:
data:
items:
$ref: '#/definitions/contact.Contact'
type: array type: array
message: message:
type: string type: string
@@ -183,21 +273,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 +288,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
@@ -301,6 +376,38 @@ paths:
summary: Add contact summary: Add contact
tags: tags:
- contacts - contacts
/contact/update:
patch:
consumes:
- application/json
description: Update the first, last, or nickname of a Contact (requires JWT)
parameters:
- description: Data to update contact's names
in: body
name: request
required: true
schema:
$ref: '#/definitions/handler.UpdateNameRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.UpdateNameResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/handler.UpdateNameResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handler.UpdateNameResponse'
security:
- BearerAuth: []
summary: Update names of Contact
tags:
- contacts
/instant/message: /instant/message:
post: post:
consumes: consumes:
@@ -569,6 +676,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:
+23 -22
View File
@@ -1,14 +1,15 @@
module git.kundeng.us/phoenix/textsender-api module git.kundeng.us/phoenix/textsender-api
go 1.25.4 go 1.26.2
require ( require (
git.kundeng.us/phoenix/swoosh v0.0.7 git.kundeng.us/phoenix/swoosh v0.2.1
git.kundeng.us/phoenix/textsender-models v0.0.11 git.kundeng.us/phoenix/textsender-models v0.2.1
github.com/go-chi/chi/v5 v5.2.3 github.com/go-chi/chi/v5 v5.2.5
github.com/golang-jwt/jwt/v5 v5.3.0 github.com/go-chi/cors v1.2.2
github.com/golang-jwt/jwt/v5 v5.3.1
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.9.2
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/swaggo/http-swagger/v2 v2.0.2 github.com/swaggo/http-swagger/v2 v2.0.2
@@ -18,16 +19,16 @@ require (
require ( require (
github.com/KyleBanks/depth v1.2.1 // indirect github.com/KyleBanks/depth v1.2.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-openapi/jsonpointer v0.22.3 // indirect github.com/go-openapi/jsonpointer v0.23.1 // indirect
github.com/go-openapi/jsonreference v0.21.3 // indirect github.com/go-openapi/jsonreference v0.21.5 // indirect
github.com/go-openapi/spec v0.22.1 // indirect github.com/go-openapi/spec v0.22.4 // indirect
github.com/go-openapi/swag/conv v0.25.4 // indirect github.com/go-openapi/swag/conv v0.26.0 // indirect
github.com/go-openapi/swag/jsonname v0.25.4 // indirect github.com/go-openapi/swag/jsonname v0.26.0 // indirect
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect github.com/go-openapi/swag/jsonutils v0.26.0 // indirect
github.com/go-openapi/swag/loading v0.25.4 // indirect github.com/go-openapi/swag/loading v0.26.0 // indirect
github.com/go-openapi/swag/stringutils v0.25.4 // indirect github.com/go-openapi/swag/stringutils v0.26.0 // indirect
github.com/go-openapi/swag/typeutils v0.25.4 // indirect github.com/go-openapi/swag/typeutils v0.26.0 // indirect
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect github.com/go-openapi/swag/yamlutils v0.26.0 // indirect
github.com/golang/mock v1.6.0 // indirect github.com/golang/mock v1.6.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
@@ -35,13 +36,13 @@ require (
github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/swaggo/files/v2 v2.0.2 // indirect github.com/swaggo/files/v2 v2.0.2 // indirect
github.com/twilio/twilio-go v1.28.8 // indirect github.com/twilio/twilio-go v1.30.5 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.45.0 // indirect golang.org/x/mod v0.35.0 // indirect
golang.org/x/mod v0.30.0 // indirect golang.org/x/sync v0.20.0 // indirect
golang.org/x/sync v0.18.0 // indirect golang.org/x/text v0.36.0 // indirect
golang.org/x/text v0.31.0 // indirect golang.org/x/tools v0.44.0 // indirect
golang.org/x/tools v0.39.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )
+48 -55
View File
@@ -1,46 +1,45 @@
git.kundeng.us/phoenix/swoosh v0.0.7 h1:9M42tgKr6o34YOiwds7offvjCUoFzYN2B5TrFKbefFo= git.kundeng.us/phoenix/swoosh v0.2.1 h1:mJ/zXBOqzi0YTrSm5kxvahzp2WH7lx4R3iIO0s9VFFI=
git.kundeng.us/phoenix/swoosh v0.0.7/go.mod h1:9iXlBd7UCMA7/jj8gZ1CxEfBualsClpiJrqGj8Fo2Xw= git.kundeng.us/phoenix/swoosh v0.2.1/go.mod h1:81XNzmTmDPuDPmXMUmKTLErNSKKBM+hsW5U60Lm78ww=
git.kundeng.us/phoenix/textsender-models v0.0.11 h1:kd2FdeZJhJJAXBm8MoyadtgNGyzC+puU1oR8B8N+MfE= git.kundeng.us/phoenix/textsender-models v0.2.1 h1:21br4NF58aUFuCx8laKxC5RvZMl4GsSIaMX4bvf5plw=
git.kundeng.us/phoenix/textsender-models v0.0.11/go.mod h1:9iPDQJg1Tc6WMNoW5+f8YKmnosMwlWHJ++hmxNLDEe0= git.kundeng.us/phoenix/textsender-models v0.2.1/go.mod h1:nu5QWy9o+spx/t9NFipaGmF5qiBJS/0QhxyCjoi3Z3E=
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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
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.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-openapi/jsonpointer v0.22.3 h1:dKMwfV4fmt6Ah90zloTbUKWMD+0he+12XYAsPotrkn8= github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
github.com/go-openapi/jsonpointer v0.22.3/go.mod h1:0lBbqeRsQ5lIanv3LHZBrmRGHLHcQoOXQnf88fHlGWo= github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc= github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4=
github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4= github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY=
github.com/go-openapi/spec v0.22.1 h1:beZMa5AVQzRspNjvhe5aG1/XyBSMeX1eEOs7dMoXh/k= github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE=
github.com/go-openapi/spec v0.22.1/go.mod h1:c7aeIQT175dVowfp7FeCvXXnjN/MrpaONStibD2WtDA= github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw=
github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ=
github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= github.com/go-openapi/swag/conv v0.26.0 h1:5yGGsPYI1ZCva93U0AoKi/iZrNhaJEjr324YVsiD89I=
github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= github.com/go-openapi/swag/conv v0.26.0/go.mod h1:tpAmIL7X58VPnHHiSO4uE3jBeRamGsFsfdDeDtb5ECE=
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= github.com/go-openapi/swag/jsonname v0.26.0 h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiNci8cGECU5w=
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= github.com/go-openapi/swag/jsonname v0.26.0/go.mod h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M=
github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= github.com/go-openapi/swag/jsonutils v0.26.0 h1:FawFML2iAXsPqmERscuMPIHmFsoP1tOqWkxBaKNMsnA=
github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= github.com/go-openapi/swag/jsonutils v0.26.0/go.mod h1:2VmA0CJlyFqgawOaPI9psnjFDqzyivIqLYN34t9p91E=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0 h1:apqeINu/ICHouqiRZbyFvuDge5jCmmLTqGQ9V95EaOM=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0/go.mod h1:AyM6QT8uz5IdKxk5akv0y6u4QvcL9GWERt0Jx/F/R8Y=
github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= github.com/go-openapi/swag/loading v0.26.0 h1:Apg6zaKhCJurpJer0DCxq99qwmhFddBhaMX7kilDcko=
github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= github.com/go-openapi/swag/loading v0.26.0/go.mod h1:dBxQ/6V2uBaAQdevN18VELE6xSpJWZxLX4txe12JwDg=
github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= github.com/go-openapi/swag/stringutils v0.26.0 h1:qZQngLxs5s7SLijc3N2ZO+fUq2o8LjuWAASSrJuh+xg=
github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= github.com/go-openapi/swag/stringutils v0.26.0/go.mod h1:sWn5uY+QIIspwPhvgnqJsH8xqFT2ZbYcvbcFanRyhFE=
github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= github.com/go-openapi/swag/typeutils v0.26.0 h1:2kdEwdiNWy+JJdOvu5MA2IIg2SylWAFuuyQIKYybfq4=
github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= github.com/go-openapi/swag/typeutils v0.26.0/go.mod h1:oovDuIUvTrEHVMqWilQzKzV4YlSKgyZmFh7AlfABNVE=
github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= github.com/go-openapi/swag/yamlutils v0.26.0 h1:H7O8l/8NJJQ/oiReEN+oMpnGMyt8G0hl460nRZxhLMQ=
github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= github.com/go-openapi/swag/yamlutils v0.26.0/go.mod h1:1evKEGAtP37Pkwcc7EWMF0hedX0/x3Rkvei2wtG/TbU=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= github.com/go-openapi/testify/enable/yaml/v2 v2.4.2 h1:5zRca5jw7lzVREKCZVNBpysDNBjj74rBh0N2BGQbSR0=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= github.com/go-openapi/testify/enable/yaml/v2 v2.4.2/go.mod h1:XVevPw5hUXuV+5AkI1u1PeAm27EQVrhXTTCPAF85LmE=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= github.com/go-openapi/testify/v2 v2.4.2 h1:tiByHpvE9uHrrKjOszax7ZvKB7QOgizBWGBLuq0ePx4=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= github.com/go-openapi/testify/v2 v2.4.2/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
@@ -51,21 +50,18 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk= github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275 h1:IZycmTpoUtQK3PD60UYBwjaCUHUP7cML494ao9/O8+Q= github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275 h1:IZycmTpoUtQK3PD60UYBwjaCUHUP7cML494ao9/O8+Q=
github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275/go.mod h1:zt6UU74K6Z6oMOYJbJzYpYucqdcQwSMPBEdSvGiaUMw= github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275/go.mod h1:zt6UU74K6Z6oMOYJbJzYpYucqdcQwSMPBEdSvGiaUMw=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -83,25 +79,23 @@ github.com/swaggo/http-swagger/v2 v2.0.2 h1:FKCdLsl+sFCx60KFsyM0rDarwiUSZ8DqbfSy
github.com/swaggo/http-swagger/v2 v2.0.2/go.mod h1:r7/GBkAWIfK6E/OLnE8fXnviHiDeAHmgIyooa4xm3AQ= github.com/swaggo/http-swagger/v2 v2.0.2/go.mod h1:r7/GBkAWIfK6E/OLnE8fXnviHiDeAHmgIyooa4xm3AQ=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
github.com/twilio/twilio-go v1.28.8 h1:wbFz7Wt4S5mCEaes6FcM/ddcJGIhdjwp/9CHb9e+4fk= github.com/twilio/twilio-go v1.30.5 h1:hi6+2kMte29zrFBsw7VSSNtbF30GPMGz/4LRIUgXng8=
github.com/twilio/twilio-go v1.28.8/go.mod h1:FpgNWMoD8CFnmukpKq9RNpUSGXC0BwnbeKZj2YHlIkw= github.com/twilio/twilio-go v1.30.5/go.mod h1:QbitvbvtkV77Jn4BABAKVmxabYSjMyQG4tHey9gfPqg=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -110,18 +104,17 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+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
+84 -20
View File
@@ -2,6 +2,7 @@ package handler
import ( import (
"fmt" "fmt"
"log"
"net/http" "net/http"
"git.kundeng.us/phoenix/textsender-models/tx0/contact" "git.kundeng.us/phoenix/textsender-models/tx0/contact"
@@ -11,16 +12,6 @@ import (
"git.kundeng.us/phoenix/textsender-api/internal/store" "git.kundeng.us/phoenix/textsender-api/internal/store"
) )
type RequestAddContact struct {
PhoneNumber string `json:"phone_number"`
UserId uuid.UUID `json:"user_id"`
}
type AddContactResponse struct {
Message string `json:"message"`
Data []contact.Contact `json:"data"`
}
type ContactHandler struct { type ContactHandler struct {
App *app.App App *app.App
ContactStore store.ContactStore ContactStore store.ContactStore
@@ -30,6 +21,19 @@ func NewContactHandler(apiApp *app.App, str store.ContactStore) *ContactHandler
return &ContactHandler{App: apiApp, ContactStore: str} return &ContactHandler{App: apiApp, ContactStore: str}
} }
type RequestAddContact struct {
PhoneNumber string `json:"phone_number"`
UserId uuid.UUID `json:"user_id"`
Firstname *string `json:"first_name,omitempty"`
Lastname *string `json:"last_name,omitempty"`
Nickname *string `json:"nickname,omitempty"`
}
type AddContactResponse struct {
Message string `json:"message"`
Data []contact.Contact `json:"data"`
}
// AddContact godoc // AddContact godoc
// @Summary Add contact // @Summary Add contact
// @Description Add a contact record to have a recipient to send a text to (requires JWT) // @Description Add a contact record to have a recipient to send a text to (requires JWT)
@@ -43,26 +47,21 @@ func NewContactHandler(apiApp *app.App, str store.ContactStore) *ContactHandler
// @Failure 500 {object} AddContactResponse // @Failure 500 {object} AddContactResponse
// @Router /contact/new [post] // @Router /contact/new [post]
func (c *ContactHandler) AddContact(w http.ResponseWriter, r *http.Request) { func (c *ContactHandler) AddContact(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req RequestAddContact var req RequestAddContact
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)
} }
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, Firstname: req.Firstname, Lastname: req.Lastname, Nickname: req.Nickname}
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) log.Printf("Error: %v", err)
statusCode = http.StatusInternalServerError statusCode = http.StatusInternalServerError
resp.Message = err.Error() resp.Message = err.Error()
} else { } else {
@@ -71,7 +70,7 @@ func (c *ContactHandler) AddContact(w http.ResponseWriter, r *http.Request) {
resp.Message = "Cannot create contact" resp.Message = "Cannot create contact"
} else { } else {
if err := c.ContactStore.CreateContact(ctx, &newContact); err != nil { if err := c.ContactStore.CreateContact(ctx, &newContact); err != nil {
fmt.Printf("Error: %v", err) log.Printf("Error: %v", err)
statusCode = http.StatusInternalServerError statusCode = http.StatusInternalServerError
resp.Message = err.Error() resp.Message = err.Error()
} else { } else {
@@ -157,7 +156,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)
} }
} }
@@ -180,3 +179,68 @@ func (c *ContactHandler) GetContact(w http.ResponseWriter, r *http.Request) {
RespondWithJSON(w, statusCode, &resp) RespondWithJSON(w, statusCode, &resp)
} }
type UpdateNameRequest struct {
Nickname *string `json:"nickname,omitempty"`
Firstname *string `json:"first_name,omitempty"`
Lastname *string `json:"last_name,omitempty"`
ContactId uuid.UUID `json:"contact_id"`
UserId uuid.UUID `json:"user_id"`
}
type UpdateNameResponse struct {
Message string `json:"message"`
Data []contact.Contact `json:"data"`
}
// UpdateName godoc
// @Summary Update names of Contact
// @Description Update the first, last, or nickname of a Contact (requires JWT)
// @Tags contacts
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body UpdateNameRequest true "Data to update contact's names"
// @Success 200 {object} UpdateNameResponse
// @Failure 400 {object} UpdateNameResponse
// @Failure 500 {object} UpdateNameResponse
// @Router /contact/update [patch]
func (c *ContactHandler) UpdateName(w http.ResponseWriter, r *http.Request) {
var req UpdateNameRequest
if err := ExtractFromRequest(r, &req); err != nil {
http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest)
}
defer r.Body.Close()
var statusCode int
var resp UpdateNameResponse
if req.UserId == uuid.Nil {
statusCode = http.StatusBadRequest
resp.Message = "User Id not provided"
} else if req.ContactId == uuid.Nil {
statusCode = http.StatusBadRequest
resp.Message = "Contact Id not provided"
} else if req.Firstname == nil && req.Lastname == nil && req.Nickname == nil {
statusCode = http.StatusBadRequest
resp.Message = "No name provided"
} else {
ctx := r.Context()
if con, err := c.ContactStore.GetContactByID(ctx, req.ContactId); err != nil {
resp.Message = err.Error()
statusCode = http.StatusInternalServerError
} else {
if affectedRows, err := c.ContactStore.UpdateNames(ctx, con, req.Firstname, req.Lastname, req.Nickname); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
log.Println("Updated", affectedRows, "rows")
statusCode = http.StatusOK
resp.Message = "Successful"
resp.Data = append(resp.Data, *con)
}
}
}
RespondWithJSON(w, statusCode, &resp)
}
+41 -8
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")
@@ -81,10 +79,45 @@ 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) { func TestUpdateContactNamesWithMock(t *testing.T) {
t.Helper() mockstore := mock.NewMockContactStore()
_, err := db.Pool.Exec(context.Background(), "DELETE FROM contacts") testUserId := uuid.New()
if err != nil {
t.Fatalf("Failed to reset test database: %v", err) testCon := contact.Contact{PhoneNumber: "+12335403383", UserId: &testUserId}
ctx := t.Context()
var firstname, lastname, nickname string
firstname = "Bob"
lastname = "De-Buildor"
nickname = "With The Tool"
if err := mockstore.CreateContact(ctx, &testCon); err != nil {
assert.NoError(t, err, "Error creating contact")
return
} }
var requestBody UpdateNameRequest
requestBody.Firstname = &firstname
requestBody.Lastname = &lastname
requestBody.Nickname = &nickname
requestBody.ContactId = *testCon.Id
requestBody.UserId = testUserId
jsonValue, _ := json.Marshal(requestBody)
req, _ := http.NewRequest("PATCH", endpoint.Update_Names_Endpoint, strings.NewReader(string(jsonValue)))
rr := httptest.NewRecorder()
apiApp, err := GetApp()
assert.NoError(t, err, "Error getting app")
contactHandler := NewContactHandler(apiApp, mockstore)
contactHandler.UpdateName(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
var response UpdateNameResponse
err = json.Unmarshal(rr.Body.Bytes(), &response)
assert.NoError(t, err, "Error updating names of contact %v", err)
assert.NotEmpty(t, response.Data, "No Contact updated")
assert.NotNil(t, response.Data[0].Firstname, "Firstname should not be nil")
assert.NotNil(t, response.Data[0].Lastname, "Lastname should not be nil")
assert.NotNil(t, response.Data[0].Nickname, "Nickname should not be nil")
} }
+27 -14
View File
@@ -1,16 +1,29 @@
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" Update_Names_Endpoint = "/api/v1/contact/update"
const DeleteScheduledMessageEventEndpoint = "/api/v1/schedule/message/event/{id}"
const UpdateScheduledMessageStatusEndpoint = "/api/v1/schedule/message/status/update" ScheduleMessageEndpoint = "/api/v1/schedule/message"
const FetchNextScheduledMessageEndpoint = "/api/v1/schedule/message/fetch" GetScheduledMessageEndpoint = "/api/v1/schedule/message"
const RecordEventResponse = "/api/v1/schedule/message/event/response/record"
const SendInstantMessageEndpoint = "/api/v1/instant/message" AddEventToScheduledMessageEndpoint = "/api/v1/schedule/message/event"
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")
}
+2 -7
View File
@@ -16,11 +16,6 @@ import (
"git.kundeng.us/phoenix/textsender-api/internal/store/mock" "git.kundeng.us/phoenix/textsender-api/internal/store/mock"
) )
type CreateMessageRequest struct {
Content string `json:"content"`
UserId uuid.UUID `json:"user_id"`
}
func TestCreateMessageWithMock(t *testing.T) { func TestCreateMessageWithMock(t *testing.T) {
mockStore := mock.NewMockMessageStore() mockStore := mock.NewMockMessageStore()
apiApp, err := GetApp() apiApp, err := GetApp()
@@ -28,7 +23,7 @@ func TestCreateMessageWithMock(t *testing.T) {
handler := NewMessageHandler(apiApp, mockStore) handler := NewMessageHandler(apiApp, mockStore)
testUserId := uuid.New() testUserId := uuid.New()
testBody := CreateMessageRequest{Content: "Who could tell?", UserId: testUserId} testBody := RequestAddMessage{Content: "Who could tell?", UserId: testUserId}
jsonValue, _ := json.Marshal(testBody) jsonValue, _ := json.Marshal(testBody)
req, _ := http.NewRequest("POST", endpoint.ADD_MESSAGE, strings.NewReader(string(jsonValue))) req, _ := http.NewRequest("POST", endpoint.ADD_MESSAGE, strings.NewReader(string(jsonValue)))
@@ -51,7 +46,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}
} }
+85 -17
View File
@@ -16,6 +16,7 @@ type ContactStore interface {
GetContactByPhone(ctx context.Context, phone string, userId uuid.UUID) (*contact.Contact, error) GetContactByPhone(ctx context.Context, phone string, userId uuid.UUID) (*contact.Contact, error)
GetAllContacts(ctx context.Context) ([]*contact.Contact, error) GetAllContacts(ctx context.Context) ([]*contact.Contact, error)
ContactExists(ctx context.Context, phone string, userId uuid.UUID) (bool, error) ContactExists(ctx context.Context, phone string, userId uuid.UUID) (bool, error)
UpdateNames(ctx context.Context, con *contact.Contact, firstname *string, lastname *string, nickname *string) (int64, error)
} }
type PGContactStore struct { type PGContactStore struct {
@@ -28,54 +29,52 @@ func NewContactStore(db *pgxpool.Pool) *PGContactStore {
func (s *PGContactStore) CreateContact(ctx context.Context, con *contact.Contact) error { func (s *PGContactStore) CreateContact(ctx context.Context, con *contact.Contact) error {
query := ` query := `
INSERT INTO contacts (phone_number, user_id) INSERT INTO contacts (phone_number, user_id, first_name, last_name, nickname)
VALUES ($1, $2) VALUES ($1, $2, $3, $4, $5)
RETURNING id, phone_number, user_id RETURNING id, phone_number, user_id
` `
return s.db.QueryRow(ctx, query, con.PhoneNumber, con.UserId).Scan( return s.db.QueryRow(ctx, query, con.PhoneNumber, con.UserId, con.Firstname, con.Lastname, con.Nickname).Scan(
&con.Id, &con.PhoneNumber, &con.UserId, &con.Id, &con.PhoneNumber, &con.UserId,
) )
} }
func (s *PGContactStore) GetContactByID(ctx context.Context, id uuid.UUID) (*contact.Contact, error) { func (s *PGContactStore) GetContactByID(ctx context.Context, id uuid.UUID) (*contact.Contact, error) {
query := `SELECT id, phone_number, user_id FROM contacts WHERE id = $1` query := `SELECT id, phone_number, user_id, first_name, last_name, nickname FROM contacts WHERE id = $1`
var con contact.Contact var con contact.Contact
err := s.db.QueryRow(ctx, query, id).Scan( err := s.db.QueryRow(ctx, query, id).Scan(
&con.Id, &con.PhoneNumber, &con.UserId, &con.Id, &con.PhoneNumber, &con.UserId, &con.Firstname, &con.Lastname, &con.Nickname,
) )
if err == pgx.ErrNoRows { if err == pgx.ErrNoRows {
return nil, nil return nil, nil
} } else if err != nil {
if err != nil {
return nil, fmt.Errorf("getting contact by ID: %w", err) return nil, fmt.Errorf("getting contact by ID: %w", err)
} else {
return &con, nil
} }
return &con, nil
} }
func (s *PGContactStore) GetContactByPhone(ctx context.Context, phone string, userId uuid.UUID) (*contact.Contact, error) { func (s *PGContactStore) GetContactByPhone(ctx context.Context, phone string, userId uuid.UUID) (*contact.Contact, error) {
query := `SELECT id, phone_number, user_id FROM contacts WHERE phone_number = $1 AND user_id = $2` query := `SELECT id, phone_number, user_id, first_name, last_name, nickname FROM contacts WHERE phone_number = $1 AND user_id = $2`
var con contact.Contact var con contact.Contact
err := s.db.QueryRow(ctx, query, phone, userId).Scan( err := s.db.QueryRow(ctx, query, phone, userId).Scan(
&con.Id, &con.PhoneNumber, &con.UserId, &con.Id, &con.PhoneNumber, &con.UserId, &con.Firstname, &con.Lastname, &con.Nickname,
) )
if err == pgx.ErrNoRows { if err == pgx.ErrNoRows {
return nil, nil return nil, nil
} } else if err != nil {
if err != nil {
return nil, fmt.Errorf("getting contact by phone and User ID: %w", err) return nil, fmt.Errorf("getting contact by phone and User ID: %w", err)
} else {
return &con, nil
} }
return &con, nil
} }
func (s *PGContactStore) GetAllContacts(ctx context.Context) ([]*contact.Contact, error) { func (s *PGContactStore) GetAllContacts(ctx context.Context) ([]*contact.Contact, error) {
query := `SELECT id, phone_number, user_id FROM contacts` query := `SELECT id, phone_number, user_id, first_name, last_name, nickname FROM contacts`
rows, err := s.db.Query(ctx, query) rows, err := s.db.Query(ctx, query)
if err != nil { if err != nil {
@@ -87,7 +86,7 @@ func (s *PGContactStore) GetAllContacts(ctx context.Context) ([]*contact.Contact
for rows.Next() { for rows.Next() {
var con contact.Contact var con contact.Contact
if err := rows.Scan( if err := rows.Scan(
&con.Id, &con.PhoneNumber, &con.UserId, &con.Id, &con.PhoneNumber, &con.UserId, &con.Firstname, &con.Lastname, &con.Nickname,
); err != nil { ); err != nil {
return nil, fmt.Errorf("scanning contact row: %w", err) return nil, fmt.Errorf("scanning contact row: %w", err)
} }
@@ -112,3 +111,72 @@ func (s *PGContactStore) ContactExists(ctx context.Context, phone string, userId
return exists, nil return exists, nil
} }
func (s *PGContactStore) UpdateNames(ctx context.Context, con *contact.Contact, firstname *string, lastname *string, nickname *string) (int64, error) {
if firstname == nil && lastname == nil && nickname == nil {
return 0, fmt.Errorf("Names are nil")
} else {
if firstname != nil && lastname != nil && nickname != nil {
query := "UPDATE contacts SET first_name = $1, last_name = $2, nickname = $3 WHERE id = $4"
if affected, err := s.db.Exec(ctx, query, firstname, lastname, nickname, con.Id); err != nil {
return 0, err
} else {
con.Firstname = firstname
con.Lastname = lastname
con.Nickname = nickname
return affected.RowsAffected(), nil
}
} else if firstname != nil && lastname != nil {
query := "UPDATE contacts SET first_name = $1, last_name = $2 WHERE id = $3"
if affected, err := s.db.Exec(ctx, query, firstname, lastname, con.Id); err != nil {
return 0, err
} else {
con.Firstname = firstname
con.Lastname = lastname
return affected.RowsAffected(), nil
}
} else if lastname != nil && nickname != nil {
query := "UPDATE contacts SET last_name = $1, nickname = $2 WHERE id = $3"
if affected, err := s.db.Exec(ctx, query, lastname, nickname, con.Id); err != nil {
return 0, err
} else {
con.Lastname = lastname
con.Nickname = nickname
return affected.RowsAffected(), nil
}
} else if nickname != nil && firstname != nil {
query := "UPDATE contacts SET nickname = $1, firstname = $2 WHERE id = $3"
if affected, err := s.db.Exec(ctx, query, nickname, firstname, con.Id); err != nil {
return 0, err
} else {
con.Firstname = firstname
con.Nickname = nickname
return affected.RowsAffected(), nil
}
} else if firstname != nil {
query := "UPDATE contacts SET firstname = $1 WHERE id = $2"
if affected, err := s.db.Exec(ctx, query, firstname, con.Id); err != nil {
return 0, err
} else {
con.Firstname = firstname
return affected.RowsAffected(), nil
}
} else if lastname != nil {
query := "UPDATE contacts SET lastname = $1 WHERE id = $2"
if affected, err := s.db.Exec(ctx, query, lastname, con.Id); err != nil {
return 0, err
} else {
con.Lastname = lastname
return affected.RowsAffected(), nil
}
} else {
query := "UPDATE contacts SET nickname = $1 WHERE id = $2"
if affected, err := s.db.Exec(ctx, query, nickname, con.Id); err != nil {
return 0, err
} else {
con.Nickname = nickname
return affected.RowsAffected(), nil
}
}
}
}
+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
}
+146
View File
@@ -0,0 +1,146 @@
package mock
import (
"context"
"errors"
"fmt"
"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
}
func (m *MockContactStore) UpdateNames(ctx context.Context, con *contact.Contact, firstname *string, lastname *string, nickname *string) (int64, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return 0, m.Error
} else if firstname == nil && lastname == nil && nickname == nil {
return 0, fmt.Errorf("Names are not provided")
} else {
if c, exists := m.Contacts[*con.Id]; !exists {
return 0, fmt.Errorf("Contact does not exist")
} else {
if firstname != nil {
c.Firstname = firstname
}
if lastname != nil {
c.Lastname = lastname
}
if nickname != nil {
c.Nickname = nickname
}
}
}
return 0, 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 {
+9 -3
View File
@@ -9,7 +9,10 @@ DROP TABLE IF EXISTS message_event_responses CASCADE;
CREATE TABLE IF NOT EXISTS contacts ( CREATE TABLE IF NOT EXISTS contacts (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
phone_number TEXT NOT NULL, phone_number TEXT NOT NULL,
user_id UUID NOT NULL user_id UUID NOT NULL,
first_name TEXT NULL,
last_name TEXT NULL,
nickname TEXT NULL
); );
CREATE TABLE IF NOT EXISTS messages ( CREATE TABLE IF NOT EXISTS messages (
@@ -28,7 +31,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 +42,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'))
); );