From a51979e724073d97b0309af4e7d1ae7e15961f7c Mon Sep 17 00:00:00 2001 From: phoenix Date: Wed, 7 Jan 2026 20:24:30 +0000 Subject: [PATCH] tsk-58: Update names of Contact endpoint (#67) Closes #58 Reviewed-on: https://git.kundeng.us/phoenix/textsender-api/pulls/67 Co-authored-by: phoenix Co-committed-by: phoenix --- cmd/api/main.go | 1 + cmd/api/main_test.go | 1 + docs/docs.go | 94 +++++++++++++++++++++++++++ docs/swagger.json | 94 +++++++++++++++++++++++++++ docs/swagger.yaml | 60 +++++++++++++++++ internal/handler/contact.go | 65 ++++++++++++++++++ internal/handler/contact_test.go | 43 ++++++++++++ internal/handler/endpoint/endpoint.go | 5 +- internal/handler/message_test.go | 7 +- internal/store/contact_store.go | 70 ++++++++++++++++++++ internal/store/mock/contact_store.go | 28 ++++++++ 11 files changed, 460 insertions(+), 8 deletions(-) diff --git a/cmd/api/main.go b/cmd/api/main.go index 731d263..f0f1fa6 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -121,6 +121,7 @@ func main() { 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("PATCH", endpoint.Update_Names_Endpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(contactHandler.UpdateName))) router.Method("GET", "/swagger/*", httpSwagger.Handler( httpSwagger.URL(fmt.Sprintf("http://localhost:%s/swagger/doc.json", config.PORT)), diff --git a/cmd/api/main_test.go b/cmd/api/main_test.go index 953fff1..c6b0c3d 100644 --- a/cmd/api/main_test.go +++ b/cmd/api/main_test.go @@ -83,6 +83,7 @@ func TestMain(m *testing.M) { 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() os.Exit(code) diff --git a/docs/docs.go b/docs/docs.go index a5cbb4c..64e9ff5 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -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": { "post": { "security": [ @@ -990,6 +1041,15 @@ const docTemplate = `{ "handler.RequestAddContact": { "type": "object", "properties": { + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "nickname": { + "type": "string" + }, "phone_number": { "type": "string" }, @@ -1104,6 +1164,40 @@ const docTemplate = `{ } } }, + "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": { + "type": "string" + } + } + }, "message.Message": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 52f8d48..c08fe8b 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -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": { "post": { "security": [ @@ -984,6 +1035,15 @@ "handler.RequestAddContact": { "type": "object", "properties": { + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "nickname": { + "type": "string" + }, "phone_number": { "type": "string" }, @@ -1098,6 +1158,40 @@ } } }, + "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": { + "type": "string" + } + } + }, "message.Message": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 6ebfb30..53252df 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -163,6 +163,12 @@ definitions: type: object handler.RequestAddContact: properties: + first_name: + type: string + last_name: + type: string + nickname: + type: string phone_number: type: string user_id: @@ -236,6 +242,28 @@ definitions: 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 + message: + type: string + type: object message.Message: properties: content: @@ -348,6 +376,38 @@ paths: summary: Add contact tags: - 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: post: consumes: diff --git a/internal/handler/contact.go b/internal/handler/contact.go index bdb6d3e..52a0095 100644 --- a/internal/handler/contact.go +++ b/internal/handler/contact.go @@ -179,3 +179,68 @@ func (c *ContactHandler) GetContact(w http.ResponseWriter, r *http.Request) { 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) +} diff --git a/internal/handler/contact_test.go b/internal/handler/contact_test.go index a132d85..218f08d 100644 --- a/internal/handler/contact_test.go +++ b/internal/handler/contact_test.go @@ -78,3 +78,46 @@ func TestGetContactWithMock(t *testing.T) { assert.NotNil(t, response.Data[0].Id, "Id should not be nil") } + +func TestUpdateContactNamesWithMock(t *testing.T) { + mockstore := mock.NewMockContactStore() + testUserId := uuid.New() + + 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") +} diff --git a/internal/handler/endpoint/endpoint.go b/internal/handler/endpoint/endpoint.go index 1c3676d..cefaa9e 100644 --- a/internal/handler/endpoint/endpoint.go +++ b/internal/handler/endpoint/endpoint.go @@ -6,8 +6,9 @@ const ( ADD_MESSAGE = "/api/v1/message/new" GET_MESSAGE = "/api/v1/message" - GET_CONTACT = "/api/v1/contact" - ADD_CONTACT_ENDPOINT = "/api/v1/contact/new" + GET_CONTACT = "/api/v1/contact" + ADD_CONTACT_ENDPOINT = "/api/v1/contact/new" + Update_Names_Endpoint = "/api/v1/contact/update" ScheduleMessageEndpoint = "/api/v1/schedule/message" GetScheduledMessageEndpoint = "/api/v1/schedule/message" diff --git a/internal/handler/message_test.go b/internal/handler/message_test.go index d162e78..1781073 100644 --- a/internal/handler/message_test.go +++ b/internal/handler/message_test.go @@ -16,11 +16,6 @@ import ( "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) { mockStore := mock.NewMockMessageStore() apiApp, err := GetApp() @@ -28,7 +23,7 @@ func TestCreateMessageWithMock(t *testing.T) { handler := NewMessageHandler(apiApp, mockStore) testUserId := uuid.New() - testBody := CreateMessageRequest{Content: "Who could tell?", UserId: testUserId} + testBody := RequestAddMessage{Content: "Who could tell?", UserId: testUserId} jsonValue, _ := json.Marshal(testBody) req, _ := http.NewRequest("POST", endpoint.ADD_MESSAGE, strings.NewReader(string(jsonValue))) diff --git a/internal/store/contact_store.go b/internal/store/contact_store.go index 31ab41a..bc67a10 100644 --- a/internal/store/contact_store.go +++ b/internal/store/contact_store.go @@ -16,6 +16,7 @@ type ContactStore interface { GetContactByPhone(ctx context.Context, phone string, userId uuid.UUID) (*contact.Contact, error) GetAllContacts(ctx context.Context) ([]*contact.Contact, 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 { @@ -110,3 +111,72 @@ func (s *PGContactStore) ContactExists(ctx context.Context, phone string, userId 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 + } + } + } +} diff --git a/internal/store/mock/contact_store.go b/internal/store/mock/contact_store.go index f99b1b0..d337c40 100644 --- a/internal/store/mock/contact_store.go +++ b/internal/store/mock/contact_store.go @@ -3,6 +3,7 @@ package mock import ( "context" "errors" + "fmt" "sync" "git.kundeng.us/phoenix/textsender-models/tx0/contact" @@ -116,3 +117,30 @@ func (m *MockContactStore) ContactExists(ctx context.Context, phoneNumber string 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 +}