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>
This commit is contained in:
phoenix
2026-01-07 20:24:30 +00:00
committed by phoenix
parent 4e250d095c
commit a51979e724
11 changed files with 460 additions and 8 deletions
+1
View File
@@ -121,6 +121,7 @@ func main() {
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("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)),
+1
View File
@@ -83,6 +83,7 @@ func TestMain(m *testing.M) {
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("GET", endpoint.FetchMessageEventResponse, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(eventHandler.Fetch)))
testRouter.Method("POST", endpoint.SendInstantMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(instantMessageHandler.Send))) 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)
+94
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": [
@@ -990,6 +1041,15 @@ const docTemplate = `{
"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"
}, },
@@ -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": { "message.Message": {
"type": "object", "type": "object",
"properties": { "properties": {
+94
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": [
@@ -984,6 +1035,15 @@
"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"
}, },
@@ -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": { "message.Message": {
"type": "object", "type": "object",
"properties": { "properties": {
+60
View File
@@ -163,6 +163,12 @@ definitions:
type: object 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:
@@ -236,6 +242,28 @@ definitions:
message: message:
type: string type: string
type: object 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: message.Message:
properties: properties:
content: content:
@@ -348,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:
+65
View File
@@ -179,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)
}
+43
View File
@@ -78,3 +78,46 @@ 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 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")
}
+3 -2
View File
@@ -6,8 +6,9 @@ const (
ADD_MESSAGE = "/api/v1/message/new" ADD_MESSAGE = "/api/v1/message/new"
GET_MESSAGE = "/api/v1/message" GET_MESSAGE = "/api/v1/message"
GET_CONTACT = "/api/v1/contact" GET_CONTACT = "/api/v1/contact"
ADD_CONTACT_ENDPOINT = "/api/v1/contact/new" ADD_CONTACT_ENDPOINT = "/api/v1/contact/new"
Update_Names_Endpoint = "/api/v1/contact/update"
ScheduleMessageEndpoint = "/api/v1/schedule/message" ScheduleMessageEndpoint = "/api/v1/schedule/message"
GetScheduledMessageEndpoint = "/api/v1/schedule/message" GetScheduledMessageEndpoint = "/api/v1/schedule/message"
+1 -6
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)))
+70
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 {
@@ -110,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
}
}
}
}
+28
View File
@@ -3,6 +3,7 @@ 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/contact"
@@ -116,3 +117,30 @@ func (m *MockContactStore) ContactExists(ctx context.Context, phoneNumber string
return exists, nil 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
}