add notification server, tests
This commit is contained in:
19
ozon/notifications/enums.go
Normal file
19
ozon/notifications/enums.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package notifications
|
||||||
|
|
||||||
|
type MessageType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PingType MessageType = "TYPE_PING"
|
||||||
|
NewPostingType MessageType = "TYPE_NEW_POSTING"
|
||||||
|
PostingCancelledType MessageType = "TYPE_POSTING_CANCELLED"
|
||||||
|
StateChangedType MessageType = "TYPE_STATE_CHANGED"
|
||||||
|
CutoffDateChangedType MessageType = "TYPE_CUTOFF_DATE_CHANGED"
|
||||||
|
DeliveryDateChangedType MessageType = "TYPE_DELIVERY_DATE_CHANGED"
|
||||||
|
CreateOrUpdateType MessageType = "TYPE_CREATE_OR_UPDATE_ITEM"
|
||||||
|
PriceIndexChangedType MessageType = "TYPE_PRICE_INDEX_CHANGED"
|
||||||
|
StocksChangedType MessageType = "TYPE_STOCKS_CHANGED"
|
||||||
|
NewMessageType MessageType = "TYPE_NEW_MESSAGE"
|
||||||
|
UpdateMessageType MessageType = "TYPE_UPDATE_MESSAGE"
|
||||||
|
MessageReadType MessageType = "TYPE_MESSAGE_READ"
|
||||||
|
ChatClosedType MessageType = "TYPE_CHAT_CLOSED"
|
||||||
|
)
|
||||||
@@ -1,21 +1,152 @@
|
|||||||
package notifications
|
package notifications
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type NotificationServer struct{}
|
type Handler func(req interface{}) error
|
||||||
|
|
||||||
func NewNotificationServer() *NotificationServer {
|
type NotificationServer struct {
|
||||||
return &NotificationServer{}
|
port int
|
||||||
|
handlers map[MessageType]Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNotificationServer(port int) *NotificationServer {
|
||||||
|
return &NotificationServer{
|
||||||
|
port: port,
|
||||||
|
handlers: map[MessageType]Handler{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ns *NotificationServer) Run() error {
|
func (ns *NotificationServer) Run() error {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/", ns.handler)
|
mux.HandleFunc("/", ns.handler)
|
||||||
return http.ListenAndServe("", mux)
|
server := http.Server{
|
||||||
|
Addr: fmt.Sprintf("0.0.0.0:%d", ns.port),
|
||||||
|
Handler: mux,
|
||||||
|
}
|
||||||
|
return server.ListenAndServe()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ns *NotificationServer) handler(rw http.ResponseWriter, req *http.Request) {
|
func (ns *NotificationServer) handler(rw http.ResponseWriter, httpReq *http.Request) {
|
||||||
|
mt := &Common{}
|
||||||
|
content, err := ioutil.ReadAll(httpReq.Body)
|
||||||
|
if err != nil {
|
||||||
|
ns.error(rw, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(content, mt); err != nil {
|
||||||
|
ns.error(rw, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if mt.MessageType == PingType {
|
||||||
|
resp := pingResponse{
|
||||||
|
Version: "1.0",
|
||||||
|
Name: "Ozon Seller API",
|
||||||
|
Time: time.Now(),
|
||||||
|
}
|
||||||
|
respJson, err := json.Marshal(resp)
|
||||||
|
if err != nil {
|
||||||
|
ns.error(rw, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rw.WriteHeader(http.StatusOK)
|
||||||
|
rw.Write(respJson)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := ns.unmarshal(mt.MessageType, content)
|
||||||
|
if err != nil {
|
||||||
|
ns.result(rw, false)
|
||||||
|
//ns.error(rw, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h, _ := ns.handlers[mt.MessageType]
|
||||||
|
if err := h(req); err != nil {
|
||||||
|
ns.result(rw, false)
|
||||||
|
//ns.error(rw, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ns *NotificationServer) Register(mt MessageType, handler func(req interface{}) error) {
|
||||||
|
ns.handlers[mt] = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ns *NotificationServer) unmarshal(messageType MessageType, content []byte) (interface{}, error) {
|
||||||
|
switch messageType {
|
||||||
|
case NewPostingType:
|
||||||
|
v := &NewPosting{}
|
||||||
|
err := json.Unmarshal(content, v)
|
||||||
|
return v, err
|
||||||
|
case PostingCancelledType:
|
||||||
|
v := &PostingCancelled{}
|
||||||
|
err := json.Unmarshal(content, v)
|
||||||
|
return v, err
|
||||||
|
case StateChangedType:
|
||||||
|
v := &StateChanged{}
|
||||||
|
err := json.Unmarshal(content, v)
|
||||||
|
return v, err
|
||||||
|
case CutoffDateChangedType:
|
||||||
|
v := &CutoffDateChanged{}
|
||||||
|
err := json.Unmarshal(content, v)
|
||||||
|
return v, err
|
||||||
|
case DeliveryDateChangedType:
|
||||||
|
v := &DeliveryDateChanged{}
|
||||||
|
err := json.Unmarshal(content, v)
|
||||||
|
return v, err
|
||||||
|
case CreateOrUpdateType:
|
||||||
|
v := &CreateOrUpdateItem{}
|
||||||
|
err := json.Unmarshal(content, v)
|
||||||
|
return v, err
|
||||||
|
case PriceIndexChangedType:
|
||||||
|
v := &PriceIndexChanged{}
|
||||||
|
err := json.Unmarshal(content, v)
|
||||||
|
return v, err
|
||||||
|
case StocksChangedType:
|
||||||
|
v := &StocksChanged{}
|
||||||
|
err := json.Unmarshal(content, v)
|
||||||
|
return v, err
|
||||||
|
case NewMessageType:
|
||||||
|
v := &NewMessage{}
|
||||||
|
err := json.Unmarshal(content, v)
|
||||||
|
return v, err
|
||||||
|
case UpdateMessageType:
|
||||||
|
v := &UpdateMessage{}
|
||||||
|
err := json.Unmarshal(content, v)
|
||||||
|
return v, err
|
||||||
|
case MessageReadType:
|
||||||
|
v := &MessageRead{}
|
||||||
|
err := json.Unmarshal(content, v)
|
||||||
|
return v, err
|
||||||
|
case ChatClosedType:
|
||||||
|
v := &ChatClosed{}
|
||||||
|
err := json.Unmarshal(content, v)
|
||||||
|
return v, err
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported type: %s", messageType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ns *NotificationServer) error(rw http.ResponseWriter, statusCode int, err error) {
|
||||||
|
errResp := errorResponse{
|
||||||
|
Data: errorData{
|
||||||
|
Code: fmt.Sprintf("%d", statusCode),
|
||||||
|
Message: err.Error(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
errJson, _ := json.Marshal(errResp)
|
||||||
|
rw.WriteHeader(statusCode)
|
||||||
|
rw.Write(errJson)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ns *NotificationServer) result(rw http.ResponseWriter, res bool) {
|
||||||
|
rw.WriteHeader(http.StatusOK)
|
||||||
|
rw.Write([]byte(fmt.Sprintf(`{"result": %t}`, res)))
|
||||||
}
|
}
|
||||||
|
|||||||
118
ozon/notifications/server_test.go
Normal file
118
ozon/notifications/server_test.go
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
package notifications
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
port = 5000
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNotificationServer(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
request string
|
||||||
|
response string
|
||||||
|
}{
|
||||||
|
// PING
|
||||||
|
{
|
||||||
|
`{
|
||||||
|
"message_type": "string",
|
||||||
|
"time": "2019-08-24T14:15:22Z"
|
||||||
|
}`,
|
||||||
|
`{
|
||||||
|
"version": "1.0",
|
||||||
|
"name": "Ozon Seller API"
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
// REGISTERED HANDLER
|
||||||
|
{
|
||||||
|
`{
|
||||||
|
"message_type": "TYPE_CHAT_CLOSED",
|
||||||
|
"chat_id": "b646d975-0c9c-4872-9f41-8b1e57181063",
|
||||||
|
"chat_type": "Buyer_Seller",
|
||||||
|
"user": {
|
||||||
|
"id": "115568",
|
||||||
|
"type": "Сustomer"
|
||||||
|
},
|
||||||
|
"seller_id": "7"
|
||||||
|
}`,
|
||||||
|
`{
|
||||||
|
"result": true
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
// UNREGISTERED HANDLER
|
||||||
|
{
|
||||||
|
`{
|
||||||
|
"message_type": "TYPE_MESSAGE_READ",
|
||||||
|
"chat_id": "b646d975-0c9c-4872-9f41-8b1e57181063",
|
||||||
|
"chat_type": "Buyer_Seller",
|
||||||
|
"message_id": "3000000000817031942",
|
||||||
|
"created_at": "2022-07-18T20:58:04.528Z",
|
||||||
|
"user": {
|
||||||
|
"id": "115568",
|
||||||
|
"type": "Сustomer"
|
||||||
|
},
|
||||||
|
"last_read_message_id": "3000000000817031942",
|
||||||
|
"seller_id": "7"
|
||||||
|
}`,
|
||||||
|
`{
|
||||||
|
"result": false
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
client := http.Client{}
|
||||||
|
server := NewNotificationServer(port)
|
||||||
|
server.Register(ChatClosedType, func(req interface{}) error {
|
||||||
|
_, ok := req.(*ChatClosed)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("req is not of ChatClosed type")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
go func() {
|
||||||
|
if err := server.Run(); err != nil {
|
||||||
|
t.Fatalf("notification server is down: %s", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
httpResp, err := client.Post(fmt.Sprintf("http://127.0.0.1:%d/", port), "application/json", strings.NewReader(testCase.request))
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
gotJson, err := ioutil.ReadAll(httpResp.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := map[string]string{}
|
||||||
|
got := map[string]string{}
|
||||||
|
json.Unmarshal(gotJson, got)
|
||||||
|
json.Unmarshal([]byte(testCase.response), expected)
|
||||||
|
|
||||||
|
if err := compare(expected, got); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func compare(expected map[string]string, got map[string]string) error {
|
||||||
|
for k, v := range expected {
|
||||||
|
if gotValue, ok := got[k]; !ok {
|
||||||
|
return fmt.Errorf("key %s is expected to present", k)
|
||||||
|
} else if gotValue != v {
|
||||||
|
return fmt.Errorf("key %s is not equal, got: %s, want: %s", k, gotValue, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -4,8 +4,7 @@ import "time"
|
|||||||
|
|
||||||
// Checking if the service is ready at initial connection and periodically after it
|
// Checking if the service is ready at initial connection and periodically after it
|
||||||
type pingRequest struct {
|
type pingRequest struct {
|
||||||
// Notification type: TYPE_PING
|
Common
|
||||||
MessageType string `json:"message_type"`
|
|
||||||
|
|
||||||
// Date and time when the notification was sent in UTC format
|
// Date and time when the notification was sent in UTC format
|
||||||
Time time.Time `json:"time"`
|
Time time.Time `json:"time"`
|
||||||
@@ -22,10 +21,13 @@ type pingResponse struct {
|
|||||||
Time time.Time `json:"time"`
|
Time time.Time `json:"time"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Common struct {
|
||||||
|
MessageType MessageType `json:"message_type"`
|
||||||
|
}
|
||||||
|
|
||||||
// New shipment
|
// New shipment
|
||||||
type NewPosting struct {
|
type NewPosting struct {
|
||||||
// Notification type: TYPE_NEW_POSTING
|
Common
|
||||||
MessageType string `json:"message_type"`
|
|
||||||
|
|
||||||
// Shipment number
|
// Shipment number
|
||||||
PostingNumber string `json:"posting_number"`
|
PostingNumber string `json:"posting_number"`
|
||||||
@@ -53,8 +55,7 @@ type Product struct {
|
|||||||
|
|
||||||
// Shipment cancellation
|
// Shipment cancellation
|
||||||
type PostingCancelled struct {
|
type PostingCancelled struct {
|
||||||
// Notification type: TYPE_POSTING_CANCELLED
|
Common
|
||||||
MessageType string `json:"message_type"`
|
|
||||||
|
|
||||||
// Shipment number
|
// Shipment number
|
||||||
PostingNumber string `json:"posting_number"`
|
PostingNumber string `json:"posting_number"`
|
||||||
@@ -91,8 +92,7 @@ type Reason struct {
|
|||||||
|
|
||||||
// Shipment status change
|
// Shipment status change
|
||||||
type StateChanged struct {
|
type StateChanged struct {
|
||||||
// Notification type: TYPE_STATE_CHANGED
|
Common
|
||||||
MessageType string `json:"message_type"`
|
|
||||||
|
|
||||||
// Shipment number
|
// Shipment number
|
||||||
PostingNumber string `json:"posting_number"`
|
PostingNumber string `json:"posting_number"`
|
||||||
@@ -112,8 +112,7 @@ type StateChanged struct {
|
|||||||
|
|
||||||
// Shipment shipping date change
|
// Shipment shipping date change
|
||||||
type CutoffDateChanged struct {
|
type CutoffDateChanged struct {
|
||||||
// Notification type: TYPE_CUTOFF_DATE_CHANGED
|
Common
|
||||||
MessageType string `json:"message_type"`
|
|
||||||
|
|
||||||
// Shipment number
|
// Shipment number
|
||||||
PostingNumber string `json:"posting_number"`
|
PostingNumber string `json:"posting_number"`
|
||||||
@@ -133,8 +132,7 @@ type CutoffDateChanged struct {
|
|||||||
|
|
||||||
// Shipment delivery date change
|
// Shipment delivery date change
|
||||||
type DeliveryDateChanged struct {
|
type DeliveryDateChanged struct {
|
||||||
// Notification type: TYPE_DELIVERY_DATE_CHANGED
|
Common
|
||||||
MessageType string `json:"message_type"`
|
|
||||||
|
|
||||||
// Shipment number
|
// Shipment number
|
||||||
PostingNumber string `json:"posting_number"`
|
PostingNumber string `json:"posting_number"`
|
||||||
@@ -160,8 +158,7 @@ type DeliveryDateChanged struct {
|
|||||||
|
|
||||||
// Product creation and update or processing error
|
// Product creation and update or processing error
|
||||||
type CreateOrUpdateItem struct {
|
type CreateOrUpdateItem struct {
|
||||||
// Notification type: TYPE_CREATE_OR_UPDATE_ITEM
|
Common
|
||||||
MessageType string `json:"message_type"`
|
|
||||||
|
|
||||||
// Product identifier in the seller's system
|
// Product identifier in the seller's system
|
||||||
OfferId string `json:"offer_id"`
|
OfferId string `json:"offer_id"`
|
||||||
@@ -181,8 +178,7 @@ type CreateOrUpdateItem struct {
|
|||||||
|
|
||||||
// Product price index change
|
// Product price index change
|
||||||
type PriceIndexChanged struct {
|
type PriceIndexChanged struct {
|
||||||
// Notification type: TYPE_PRICE_INDEX_CHANGED
|
Common
|
||||||
MessageType string `json:"message_type"`
|
|
||||||
|
|
||||||
// Date and time of price index change
|
// Date and time of price index change
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
@@ -202,8 +198,7 @@ type PriceIndexChanged struct {
|
|||||||
|
|
||||||
// Stock change at the seller's warehouse
|
// Stock change at the seller's warehouse
|
||||||
type StocksChanged struct {
|
type StocksChanged struct {
|
||||||
// Notification type: TYPE_STOCKS_CHANGED
|
Common
|
||||||
MessageType string `json:"message_type"`
|
|
||||||
|
|
||||||
// Array with products data
|
// Array with products data
|
||||||
Items []Item `json:"items"`
|
Items []Item `json:"items"`
|
||||||
@@ -239,8 +234,7 @@ type Stock struct {
|
|||||||
|
|
||||||
// New message in chat
|
// New message in chat
|
||||||
type NewMessage struct {
|
type NewMessage struct {
|
||||||
// Notification type: TYPE_NEW_MESSAGE
|
Common
|
||||||
MessageType string `json:"message_type"`
|
|
||||||
|
|
||||||
// Chat identifier
|
// Chat identifier
|
||||||
ChatId string `json:"chat_id"`
|
ChatId string `json:"chat_id"`
|
||||||
@@ -290,8 +284,7 @@ type MessageRead struct {
|
|||||||
|
|
||||||
// Chat is closed
|
// Chat is closed
|
||||||
type ChatClosed struct {
|
type ChatClosed struct {
|
||||||
// Notification type: TYPE_CHAT_CLOSED
|
Common
|
||||||
MessageType string `json:"message_type"`
|
|
||||||
|
|
||||||
// Chat identifier
|
// Chat identifier
|
||||||
ChatId string `json:"chat_id"`
|
ChatId string `json:"chat_id"`
|
||||||
|
|||||||
Reference in New Issue
Block a user