diff --git a/.env.docker.sample b/.env.docker.sample index af92142..4bcc074 100644 --- a/.env.docker.sample +++ b/.env.docker.sample @@ -9,3 +9,4 @@ TWILIO_AUTH_SID=9M438C93R943U4329MCU43C34U TWILIO_SERVICE_SID=9M4J3X8439U398NUVT3342MC349C348T TWILIO_AUTH_TOKEN="f4a1f2b0b79ea3735078c2d8ee9684e1" TWILIO_PHONE_NUMBER=+10123456789 +ALLOWED_ORIGINS="http://textsender.com" diff --git a/.env.local.sample b/.env.local.sample index 5c205fa..e47c1f5 100644 --- a/.env.local.sample +++ b/.env.local.sample @@ -9,3 +9,4 @@ TWILIO_AUTH_SID=9M438C93R943U4329MCU43C34U TWILIO_SERVICE_SID=9M4J3X8439U398NUVT3342MC349C348T TWILIO_AUTH_TOKEN="f4a1f2b0b79ea3735078c2d8ee9684e1" TWILIO_PHONE_NUMBER=+10123456789 +ALLOWED_ORIGINS="http://textsender.com" diff --git a/.gitea/workflows/workflow.yaml b/.gitea/workflows/workflow.yaml index 1c6034c..dc80dac 100644 --- a/.gitea/workflows/workflow.yaml +++ b/.gitea/workflows/workflow.yaml @@ -107,6 +107,7 @@ jobs: echo "TWILIO_SERVICE_SID=9M4J3X8439U398NUVT3342MC349C348T" >> .env echo "TWILIO_AUTH_TOKEN=f4a1f2b0b79ea3735078c2d8ee9684e1" >> .env echo "TWILIO_PHONE_NUMBER=10123456789" >> .env + echo "ALLOWED_ORIGINS=http://localhost:5173" >> .env echo "Initializing config" mkdir -p ~/.ssh diff --git a/cmd/api/main.go b/cmd/api/main.go index e6cc12a..6a7e0a6 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -12,6 +12,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" + "github.com/go-chi/cors" "github.com/swaggo/http-swagger/v2" _ "git.kundeng.us/phoenix/textsender-api/docs" @@ -92,6 +93,15 @@ func main() { 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.Recoverer) router.Use(middleware.Timeout(60 * time.Second)) diff --git a/go.mod b/go.mod index 257bfe1..51f4ae7 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,10 @@ module git.kundeng.us/phoenix/textsender-api go 1.25.4 require ( - git.kundeng.us/phoenix/swoosh v0.0.7 - git.kundeng.us/phoenix/textsender-models v0.0.11 + git.kundeng.us/phoenix/swoosh v0.0.7-8-523c7cf67c-556 + git.kundeng.us/phoenix/textsender-models v0.0.12 github.com/go-chi/chi/v5 v5.2.3 + github.com/go-chi/cors v1.2.2 github.com/golang-jwt/jwt/v5 v5.3.0 github.com/google/uuid v1.6.0 github.com/jackc/pgx/v5 v5.7.6 diff --git a/go.sum b/go.sum index 5d25576..848543d 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ -git.kundeng.us/phoenix/swoosh v0.0.7 h1:9M42tgKr6o34YOiwds7offvjCUoFzYN2B5TrFKbefFo= -git.kundeng.us/phoenix/swoosh v0.0.7/go.mod h1:9iXlBd7UCMA7/jj8gZ1CxEfBualsClpiJrqGj8Fo2Xw= -git.kundeng.us/phoenix/textsender-models v0.0.11 h1:kd2FdeZJhJJAXBm8MoyadtgNGyzC+puU1oR8B8N+MfE= -git.kundeng.us/phoenix/textsender-models v0.0.11/go.mod h1:9iPDQJg1Tc6WMNoW5+f8YKmnosMwlWHJ++hmxNLDEe0= +git.kundeng.us/phoenix/swoosh v0.0.7-8-523c7cf67c-556 h1:vzTrhx7auc8OlUGmfTDektHXCkoFOKzVhdsW+6oKXMI= +git.kundeng.us/phoenix/swoosh v0.0.7-8-523c7cf67c-556/go.mod h1:OAh9jVBQ3vRJ1EHTM6pFyWd9eXf1H+CevbDKkJuoDZU= +git.kundeng.us/phoenix/textsender-models v0.0.12 h1:ps9H3FS5LyCwQhAiIvg4vYyfLZZ64dex1y9ytb0o9C4= +git.kundeng.us/phoenix/textsender-models v0.0.12/go.mod h1:9iPDQJg1Tc6WMNoW5+f8YKmnosMwlWHJ++hmxNLDEe0= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= 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= @@ -11,6 +11,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE= +github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-openapi/jsonpointer v0.22.3 h1:dKMwfV4fmt6Ah90zloTbUKWMD+0he+12XYAsPotrkn8= github.com/go-openapi/jsonpointer v0.22.3/go.mod h1:0lBbqeRsQ5lIanv3LHZBrmRGHLHcQoOXQnf88fHlGWo= github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc= diff --git a/internal/app/app.go b/internal/app/app.go index f09df6f..f2ef963 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -5,5 +5,5 @@ import ( ) type App struct { - TwilioConfig *config.TwiloConfig + TwilioConfig *config.TwilioConfig } diff --git a/internal/config/config.go b/internal/config/config.go index aabedb2..f330faa 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "strconv" + "strings" "git.kundeng.us/phoenix/textsender-models/tx0/config" "github.com/joho/godotenv" @@ -13,11 +14,12 @@ import ( ) type Config struct { - DBConnString string - ServerPort string - ResetDB bool - JWTSecret string `env:"JWT_SECRET" required:"true"` - TwilioConfig *config.TwiloConfig + DBConnString string + ServerPort string + ResetDB bool + JWTSecret string `env:"JWT_SECRET" required:"true"` + TwilioConfig *config.TwilioConfig + AllowedOrigins []string } type ConnectionInfo struct { @@ -47,7 +49,7 @@ func PrintName() { fmt.Println(version.String()) } -func Load() (*Config, *config.TwiloConfig, error) { +func Load() (*Config, *config.TwilioConfig, error) { versionFlag := flag.Bool("version", false, "Print version information") resetDb := flag.Bool("reset-db", false, "Reset the database schema and exit") port := flag.String("port", PORT, "Server port") @@ -68,11 +70,13 @@ func Load() (*Config, *config.TwiloConfig, error) { if cfg, err := TwilioConfig(); err != nil { return nil, nil, err } else { + allowedOrigins := unpackAllowedOrigins() return &Config{ - DBConnString: dbConnString, - ServerPort: *port, - ResetDB: *resetDb, - JWTSecret: os.Getenv("JWT_SECRET"), + DBConnString: dbConnString, + ServerPort: *port, + ResetDB: *resetDb, + JWTSecret: os.Getenv("JWT_SECRET"), + AllowedOrigins: allowedOrigins, }, cfg, nil } } @@ -128,7 +132,13 @@ func UnpackDBConnString() (connInfo ConnectionInfo) { return } -func TwilioConfig() (*config.TwiloConfig, error) { +func unpackAllowedOrigins() []string { + allowedOriginsRaw := os.Getenv("ALLOWED_ORIGINS") + allowedOriginsSplit := strings.Split(allowedOriginsRaw, ",") + return allowedOriginsSplit +} + +func TwilioConfig() (*config.TwilioConfig, error) { authSid := os.Getenv("TWILIO_AUTH_SID") serviceSid := os.Getenv("TWILIO_SERVICE_SID") authToken := os.Getenv("TWILIO_AUTH_TOKEN") @@ -143,7 +153,7 @@ func TwilioConfig() (*config.TwiloConfig, error) { } else if len(phoneNumber) == 0 { return nil, fmt.Errorf("Twilio config phone number not found") } else { - var cfg config.TwiloConfig + var cfg config.TwilioConfig cfg.AccountSID = authSid cfg.ServiceSID = serviceSid cfg.AuthToken = authToken diff --git a/internal/handler/contact.go b/internal/handler/contact.go index 55c6097..bacd14f 100644 --- a/internal/handler/contact.go +++ b/internal/handler/contact.go @@ -54,14 +54,14 @@ func (c *ContactHandler) AddContact(w http.ResponseWriter, r *http.Request) { } defer r.Body.Close() - newContact := contact.Contact{PhoneNumber: req.PhoneNumber, UserId: req.UserId} + newContact := contact.Contact{PhoneNumber: req.PhoneNumber, UserId: &req.UserId} var statusCode int var resp AddContactResponse 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) statusCode = http.StatusInternalServerError resp.Message = err.Error() @@ -157,7 +157,7 @@ func (c *ContactHandler) GetContact(w http.ResponseWriter, r *http.Request) { fmt.Println("Checking with User Id") if contacts, err := c.ContactStore.GetAllContacts(ctx); err == nil { for _, con := range contacts { - if con.UserId == userId { + if *con.UserId == userId { resp.Data = append(resp.Data, *con) } } diff --git a/internal/handler/contact_test.go b/internal/handler/contact_test.go index 298260e..1ee4388 100644 --- a/internal/handler/contact_test.go +++ b/internal/handler/contact_test.go @@ -54,7 +54,7 @@ func TestGetContactWithMock(t *testing.T) { mockstore := mock.NewMockContactStore() testUserId := uuid.New() - testCon := contact.Contact{PhoneNumber: "+12335403383", UserId: testUserId} + testCon := contact.Contact{PhoneNumber: "+12335403383", UserId: &testUserId} ctx := t.Context() if err := mockstore.CreateContact(ctx, &testCon); err != nil { assert.NoError(t, err, "Error creating contact") diff --git a/internal/handler/instant_message.go b/internal/handler/instant_message.go index deafa46..3160d4b 100644 --- a/internal/handler/instant_message.go +++ b/internal/handler/instant_message.go @@ -110,6 +110,9 @@ func (s *SendInstantMessageHandler) Send(w http.ResponseWriter, r *http.Request) mer.Response = data mer.UserId = req.UserId mer.Sent = parsedTime + mer.Status = message.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 resp.Message = err.Error() diff --git a/internal/handler/message_event_response.go b/internal/handler/message_event_response.go index 8fbe6ec..cd9646b 100644 --- a/internal/handler/message_event_response.go +++ b/internal/handler/message_event_response.go @@ -26,6 +26,9 @@ type RecordEventRequest struct { Response types.JSONB `json:"response"` UserId uuid.UUID `json:"user_id"` 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 { @@ -62,6 +65,14 @@ func (e *EventResponseHandler) RecordResponse(w http.ResponseWriter, r *http.Req rp.UserId = req.UserId rp.Sent = *req.Sent + if req.Status == message.Message_Event_Response_Status_Instant { + rp.Status = req.Status + rp.MessageId = req.MessageId + rp.ContactId = req.ContactId + } else if req.Status == message.Message_Event_Response_Status_Scheduled { + rp.Status = req.Status + } + ctx := r.Context() if err := e.MessageEventResponseStore.Create(ctx, &rp); err != nil { resp.Message = err.Error() diff --git a/internal/handler/message_event_response_test.go b/internal/handler/message_event_response_test.go index 879ab9f..210099e 100644 --- a/internal/handler/message_event_response_test.go +++ b/internal/handler/message_event_response_test.go @@ -51,7 +51,7 @@ func TestRecordMessageEventResponseWithMock(t *testing.T) { } event.MessageId = msg.Id - event.RecipientId = con.Id + event.RecipientId = *con.Id event.ScheduledMessageId = schMsg.Id if err := mockStore.CreateScheduledMessageEvent(ctx, &event); err != nil { assert.NoError(t, err, "Error creating scheduled message event: %v", err) @@ -60,7 +60,7 @@ func TestRecordMessageEventResponseWithMock(t *testing.T) { 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\"}}") - testReq := RecordEventRequest{ScheduledMessageEventId: event.Id, Response: bytes, UserId: schMsg.UserId, Sent: &sent} + testReq := RecordEventRequest{ScheduledMessageEventId: event.Id, Response: bytes, UserId: schMsg.UserId, Sent: &sent, Status: message.Message_Event_Response_Status_Scheduled} jsonValue, _ := json.Marshal(testReq) req, _ := http.NewRequest("POST", endpoint.RecordEventResponse, strings.NewReader(string(jsonValue))) diff --git a/internal/handler/schedule_message_status_test.go b/internal/handler/schedule_message_status_test.go index 93240ea..10dcbe4 100644 --- a/internal/handler/schedule_message_status_test.go +++ b/internal/handler/schedule_message_status_test.go @@ -32,7 +32,7 @@ func TestUpdateScheduledMessageStatusWithMock(t *testing.T) { con := testContact(recipientId, testUserId) msg := testMessage(messageId, testUserId) schMsg := testScheduledMessage(scheduledMessageId, testUserId, now) - event := testScheduledMessageEvent(msg.Id, con.Id, schMsg.Id) + event := testScheduledMessageEvent(msg.Id, *con.Id, schMsg.Id) ctx := t.Context() diff --git a/internal/handler/scheduled_message_event_test.go b/internal/handler/scheduled_message_event_test.go index 13d6047..56fed9f 100644 --- a/internal/handler/scheduled_message_event_test.go +++ b/internal/handler/scheduled_message_event_test.go @@ -104,7 +104,7 @@ func TestGetScheduledMessageEventWithMock(t *testing.T) { con := testContact(recipientId, testUserId) msg := testMessage(messageId, testUserId) schMsg := testScheduledMessage(scheduledMessageId, testUserId, now) - event := testScheduledMessageEvent(msg.Id, con.Id, schMsg.Id) + event := testScheduledMessageEvent(msg.Id, *con.Id, schMsg.Id) ctx := t.Context() @@ -150,7 +150,7 @@ func TestDeleteScheduledMessageEventWithMock(t *testing.T) { con := testContact(recipientId, testUserId) msg := testMessage(messageId, testUserId) schMsg := testScheduledMessage(scheduledMessageId, testUserId, now) - event := testScheduledMessageEvent(msg.Id, con.Id, schMsg.Id) + event := testScheduledMessageEvent(msg.Id, *con.Id, schMsg.Id) ctx := t.Context() @@ -185,9 +185,10 @@ func TestDeleteScheduledMessageEventWithMock(t *testing.T) { func testContact(id uuid.UUID, userId uuid.UUID) contact.Contact { if id == uuid.Nil { - return contact.Contact{Id: uuid.New(), PhoneNumber: "+10123456789", UserId: userId} + id = uuid.New() + return contact.Contact{Id: &id, PhoneNumber: "+10123456789", UserId: &userId} } else { - return contact.Contact{Id: id, PhoneNumber: "+10123456789", UserId: userId} + return contact.Contact{Id: &id, PhoneNumber: "+10123456789", UserId: &userId} } } diff --git a/internal/handler/scheduled_message_test.go b/internal/handler/scheduled_message_test.go index 026acb7..b3ca5c3 100644 --- a/internal/handler/scheduled_message_test.go +++ b/internal/handler/scheduled_message_test.go @@ -97,7 +97,7 @@ func TestFetchScheduledMessageWithMock(t *testing.T) { con := testContact(recipientId, testUserId) msg := testMessage(messageId, testUserId) schMsg := testScheduledMessage(scheduledMessageId, testUserId, now) - event := testScheduledMessageEvent(msg.Id, con.Id, schMsg.Id) + event := testScheduledMessageEvent(msg.Id, *con.Id, schMsg.Id) ctx := t.Context() diff --git a/internal/store/message_event_response_store.go b/internal/store/message_event_response_store.go index 9eae924..031d25e 100644 --- a/internal/store/message_event_response_store.go +++ b/internal/store/message_event_response_store.go @@ -27,10 +27,10 @@ func (m *PGMessageEventResponseStore) Create(ctx context.Context, mer *message.M queryReturn := "RETURNING id" if mer.ScheduledMessageEventId == uuid.Nil { - query = fmt.Sprintf("%s (response, user_id, sent) VALUES ($1, $2, $3) %s", queryBase, queryReturn) - return m.db.QueryRow(ctx, query, mer.Response, mer.UserId, mer.Sent).Scan(&mer.Id) + query = fmt.Sprintf("%s (response, user_id, sent, contact_id, message_id) VALUES ($1, $2, $3, $4, $5) %s", queryBase, queryReturn) + return m.db.QueryRow(ctx, query, mer.Response, mer.UserId, mer.Sent, mer.ContactId, mer.MessageId).Scan(&mer.Id) } else { - query = fmt.Sprintf("%s (scheduled_message_event_id, response, user_id, sent) %s", queryBase, queryReturn) - return m.db.QueryRow(ctx, query, mer.ScheduledMessageEventId, mer.Response, mer.UserId, mer.Sent).Scan(&mer.Id) + query = fmt.Sprintf("%s (scheduled_message_event_id, response, user_id, sent, contact_id, message_id) VALUES ($1, $2, $3, $4, $5, $6) %s", queryBase, queryReturn) + return m.db.QueryRow(ctx, query, mer.ScheduledMessageEventId, mer.Response, mer.UserId, mer.Sent, mer.ContactId, mer.MessageId).Scan(&mer.Id) } } diff --git a/internal/store/mock/contact_store.go b/internal/store/mock/contact_store.go new file mode 100644 index 0000000..f99b1b0 --- /dev/null +++ b/internal/store/mock/contact_store.go @@ -0,0 +1,118 @@ +package mock + +import ( + "context" + "errors" + "sync" + + "git.kundeng.us/phoenix/textsender-models/tx0/contact" + "github.com/google/uuid" +) + +type Key struct { + PhoneNumber string + UserId uuid.UUID +} + +type MockContactStore struct { + Contacts map[uuid.UUID]*contact.Contact + ContactsByKey map[Key]*contact.Contact + mu sync.RWMutex + Error error // Optional: simulate errors +} + +func NewMockContactStore() *MockContactStore { + return &MockContactStore{ + Contacts: make(map[uuid.UUID]*contact.Contact), + ContactsByKey: make(map[Key]*contact.Contact), + } +} + +func (m *MockContactStore) CreateContact(ctx context.Context, con *contact.Contact) error { + m.mu.Lock() + defer m.mu.Unlock() + + if m.Error != nil { + return m.Error + } + + var id uuid.UUID + if con.Id == nil { + id = uuid.New() + con.Id = &id + } + + key := Key{PhoneNumber: con.PhoneNumber, UserId: *con.UserId} + if _, exists := m.ContactsByKey[key]; exists { + return errors.New("Contact with phone number already exists") + } + + m.Contacts[*con.Id] = con + m.ContactsByKey[key] = con + return nil +} + +func (m *MockContactStore) GetContactByID(ctx context.Context, id uuid.UUID) (*contact.Contact, error) { + m.mu.Lock() + defer m.mu.Unlock() + + if m.Error != nil { + return nil, m.Error + } + + if m.Error != nil { + return nil, m.Error + } + + con, exists := m.Contacts[id] + if !exists { + return nil, errors.New("Contact not found") + } + + return con, nil +} + +func (m *MockContactStore) GetContactByPhone(ctx context.Context, phoneNumber string, userId uuid.UUID) (*contact.Contact, error) { + m.mu.Lock() + defer m.mu.Unlock() + + if m.Error != nil { + return nil, m.Error + } + + con, exists := m.ContactsByKey[Key{PhoneNumber: phoneNumber, UserId: userId}] + if !exists { + return nil, errors.New("Contact not found") + } + + return con, nil +} + +func (m *MockContactStore) GetAllContacts(ctx context.Context) ([]*contact.Contact, error) { + m.mu.Lock() + defer m.mu.Unlock() + + if m.Error != nil { + return nil, m.Error + } + + cons := make([]*contact.Contact, 0, len(m.Contacts)) + for _, con := range m.Contacts { + cons = append(cons, con) + } + + return cons, nil +} + +func (m *MockContactStore) ContactExists(ctx context.Context, phoneNumber string, userId uuid.UUID) (bool, error) { + m.mu.Lock() + defer m.mu.Unlock() + + if m.Error != nil { + return false, m.Error + } + + _, exists := m.ContactsByKey[Key{PhoneNumber: phoneNumber, UserId: userId}] + + return exists, nil +} diff --git a/internal/store/mock/store.go b/internal/store/mock/store.go index d1c571c..aa2b014 100644 --- a/internal/store/mock/store.go +++ b/internal/store/mock/store.go @@ -5,117 +5,10 @@ import ( "errors" "sync" - "git.kundeng.us/phoenix/textsender-models/tx0/contact" "git.kundeng.us/phoenix/textsender-models/tx0/message" "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 { Content string UserId uuid.UUID diff --git a/migrations/schema.sql b/migrations/schema.sql index b37adad..f7a8a1a 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -39,5 +39,8 @@ CREATE TABLE IF NOT EXISTS message_event_responses ( scheduled_message_event_id UUID NULL, response JSONB 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')) );