diff --git a/.gitignore b/.gitignore index 2e1af13..37ea85c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ vendor *.swp hosts .idea -.venv \ No newline at end of file +.venv +coverage.out \ No newline at end of file diff --git a/cmd/go-mtg-vk/main.go b/cmd/go-mtg-vk/main.go index 690413e..519cf91 100644 --- a/cmd/go-mtg-vk/main.go +++ b/cmd/go-mtg-vk/main.go @@ -20,14 +20,14 @@ func main() { groupId, _ := strconv.ParseInt(os.Getenv("VK_GROUP_ID"), 10, 64) handler := vk.Handler{ Sender: &vk.ApiSender{ - Token: os.Getenv("VK_TOKEN"), + Token: os.Getenv("VK_TOKEN"), }, - Logger: log.New(os.Stdout, "", 0), + Logger: log.New(os.Stdout, "", 0), SecretKey: os.Getenv("VK_SECRET_KEY"), GroupId: groupId, ConfirmationString: os.Getenv("VK_CONFIRMATION_STRING"), - DictPath: "./assets/additional_cards.json", - CachingClient: caching.GetClient(), + DictPath: "./assets/additional_cards.json", + Cache: caching.GetClient(), } r.POST("callback/message", handler.HandleMessage) diff --git a/go.sum b/go.sum index 9676ca6..d5b0d6b 100644 --- a/go.sum +++ b/go.sum @@ -61,6 +61,7 @@ github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGr github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ= github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da h1:NimzV1aGyq29m5ukMK0AMWEhFaL/lrEOaephfuoiARg= github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c h1:uOCk1iQW6Vc18bnC13MfzScl+wdKBmM9Y9kU7Z83/lw= diff --git a/internal/cardsinfo/price_format_test.go b/internal/cardsinfo/price_format_test.go deleted file mode 100644 index 2b50477..0000000 --- a/internal/cardsinfo/price_format_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package cardsinfo - -import ( - "github.com/stretchr/testify/assert" - "testing" -) - -func TestFormat(t *testing.T) { - data := []CardPrice{ - &TcgCardPrice{ - Name: "Green lotus", - PriceFoil: "22.8", - Link: "scg.com/1", - Edition: "alpha", - }, - &TcgCardPrice{ - Name: "White lotus", - Price: "3.22", - Link: "scg.com/2", - Edition: "gamma", - }, - } - res := FormatCardPrices("Black Lotus", data) - ans := "Оригинальное название: Black Lotus\nРезультатов: 2\n1. alpha\nRegular: -\nFoil: $22.8\nscg.com/1\n2. gamma\nRegular: $3.22\nFoil: -\nscg.com/2\n" - assert.Equal(t, ans, res) -} diff --git a/internal/cardsinfo/structs.go b/internal/cardsinfo/structs.go index 7d5000a..b6b2f80 100644 --- a/internal/cardsinfo/structs.go +++ b/internal/cardsinfo/structs.go @@ -9,26 +9,6 @@ type CardPrice interface { Format() string } -type TcgCardPrice struct { - FullArt bool - Name string - Price string - PriceFoil string - Link string - Edition string -} - -func (t *TcgCardPrice) Format() string { - return fmt.Sprintf("%v\nRegular: %v\nFoil: %v\n%v\n", t.Edition, formatTcgPrice(t.Price), formatTcgPrice(t.PriceFoil), t.Link) -} - -func formatTcgPrice(price string) string { - if price == "" { - return "-" - } - return fmt.Sprintf("$%v", price) -} - type ScgCardPrice struct { Price string Edition string diff --git a/internal/vk/handler.go b/internal/vk/handler.go index 0442f73..60fa484 100644 --- a/internal/vk/handler.go +++ b/internal/vk/handler.go @@ -2,16 +2,28 @@ package vk import ( "errors" + "io" "log" "net/http" "os" "strings" "github.com/gin-gonic/gin" - "gitlab.com/flygrounder/go-mtg-vk/internal/caching" "gitlab.com/flygrounder/go-mtg-vk/internal/cardsinfo" ) +type CardInfoFetcher interface { + GetPrices(name string) ([]cardsinfo.CardPrice, error) + FormatCardPrices(name string, prices []cardsinfo.CardPrice) string + GetNameByCardId(set string, number string) string + GetOriginalName(name string, dict io.Reader) string +} + +type CardCache interface { + Get(cardName string) (string, error) + Set(cardName string, message string) +} + type Handler struct { Sender Sender Logger *log.Logger @@ -19,7 +31,8 @@ type Handler struct { GroupId int64 ConfirmationString string DictPath string - CachingClient *caching.CacheClient + Cache CardCache + InfoFetcher CardInfoFetcher } type messageRequest struct { @@ -34,6 +47,12 @@ type userMessage struct { UserId int64 `json:"peer_id"` } +const ( + incorrectMessage = "Некорректная команда" + cardNotFoundMessage = "Карта не найдена" + pricesUnavailableMessage = "Цены временно недоступны, попробуйте позже" +) + func (h *Handler) HandleMessage(c *gin.Context) { var req messageRequest _ = c.BindJSON(&req) @@ -50,23 +69,17 @@ func (h *Handler) HandleMessage(c *gin.Context) { } func (h *Handler) handleSearch(req *messageRequest) { - defer func() { - if r := recover(); r != nil { - h.Logger.Printf("[error] Search panicked. Exception info: %s", r) - } - }() - cardName, err := h.getCardNameByCommand(req.Object.Body) if err != nil { - h.Sender.Send(req.Object.UserId, "Некорректная команда") + h.Sender.Send(req.Object.UserId, incorrectMessage) h.Logger.Printf("[info] Not correct command. Message: %s user input: %s", err.Error(), req.Object.Body) } else if cardName == "" { - h.Sender.Send(req.Object.UserId, "Карта не найдена") + h.Sender.Send(req.Object.UserId, cardNotFoundMessage) h.Logger.Printf("[info] Could not find card. User input: %s", req.Object.Body) } else { message, err := h.getMessage(cardName) if err != nil { - h.Sender.Send(req.Object.UserId, "Цены временно недоступны, попробуйте позже") + h.Sender.Send(req.Object.UserId, pricesUnavailableMessage) h.Logger.Printf("[error] Could not find SCG prices. Message: %s card name: %s", err.Error(), cardName) return } @@ -81,14 +94,14 @@ func (h *Handler) handleConfirmation(c *gin.Context, req *messageRequest) { } func (h *Handler) getMessage(cardName string) (string, error) { - val, err := h.CachingClient.Get(cardName) + val, err := h.Cache.Get(cardName) if err != nil { - prices, err := cardsinfo.GetPrices(cardName) + prices, err := h.InfoFetcher.GetPrices(cardName) if err != nil { return "", err } - message := cardsinfo.FormatCardPrices(cardName, prices) - h.CachingClient.Set(cardName, message) + message := h.InfoFetcher.FormatCardPrices(cardName, prices) + h.Cache.Set(cardName, message) return message, nil } return val, nil @@ -104,10 +117,10 @@ func (h *Handler) getCardNameByCommand(command string) (string, error) { } set := split[1] number := split[2] - name = cardsinfo.GetNameByCardId(set, number) + name = h.InfoFetcher.GetNameByCardId(set, number) default: dict, _ := os.Open(h.DictPath) - name = cardsinfo.GetOriginalName(command, dict) + name = h.InfoFetcher.GetOriginalName(command, dict) } return name, nil } diff --git a/internal/vk/handler_test.go b/internal/vk/handler_test.go new file mode 100644 index 0000000..6400883 --- /dev/null +++ b/internal/vk/handler_test.go @@ -0,0 +1,222 @@ +package vk + +import ( + "bytes" + "encoding/json" + "errors" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "gitlab.com/flygrounder/go-mtg-vk/internal/cardsinfo" + "io" + "log" + "net/http/httptest" + "strings" + "testing" +) + +func getTestRequestCtx(msgReq *messageRequest, recorder *httptest.ResponseRecorder) *gin.Context { + ctx, _ := gin.CreateTestContext(recorder) + body, _ := json.Marshal(msgReq) + ctx.Request = httptest.NewRequest("POST", "/", bytes.NewReader(body)) + return ctx +} + +type testCtx struct { + handler *Handler + recorder *httptest.ResponseRecorder + sender *testSender + logBuf *bytes.Buffer +} + +type testMessage struct { + userId int64 + message string +} + +type testSender struct { + sent []testMessage +} + +func (s *testSender) Send(userId int64, message string) { + s.sent = append(s.sent, testMessage{ + userId: userId, + message: message, + }) +} + +type testCache struct { + table map[string]string +} + +func (t *testCache) Get(cardName string) (string, error) { + msg, ok := t.table[cardName] + if !ok { + return "", errors.New("test") + } + return msg, nil +} + +func (t *testCache) Set(cardName string, message string) { + t.table[cardName] = message +} + +func getTestHandlerCtx() testCtx { + sender := &testSender{} + buf := &bytes.Buffer{} + return testCtx{ + logBuf: buf, + handler: &Handler{ + SecretKey: "sec", + GroupId: 10, + ConfirmationString: "con", + Sender: sender, + Logger: log.New(buf, "", 0), + InfoFetcher: &testInfoFetcher{}, + Cache: &testCache{ + table: map[string]string{ + "good": "good", + }, + }, + }, + sender: sender, + recorder: httptest.NewRecorder(), + } +} + +type testInfoFetcher struct{} + +func (t *testInfoFetcher) GetPrices(name string) ([]cardsinfo.CardPrice, error) { + if name == "good" || name == "uncached" { + return nil, nil + } + return nil, errors.New("test") +} + +func (t *testInfoFetcher) FormatCardPrices(name string, _ []cardsinfo.CardPrice) string { + return name +} + +func (t *testInfoFetcher) GetNameByCardId(_ string, _ string) string { + return "good" +} + +func (t *testInfoFetcher) GetOriginalName(name string, _ io.Reader) string { + if name == "good" || name == "bad" || name == "uncached" { + return name + } + return "" +} + +func TestHandler_HandleMessage_Confirm(t *testing.T) { + testCtx := getTestHandlerCtx() + ctx := getTestRequestCtx(&messageRequest{ + Type: "confirmation", + GroupId: testCtx.handler.GroupId, + Secret: testCtx.handler.SecretKey, + }, testCtx.recorder) + testCtx.handler.HandleMessage(ctx) + assert.Equal(t, testCtx.handler.ConfirmationString, testCtx.recorder.Body.String()) +} + +func TestHandler_HandleMessage_Message(t *testing.T) { + testCtx := getTestHandlerCtx() + ctx := getTestRequestCtx(&messageRequest{ + Type: "message_new", + Secret: testCtx.handler.SecretKey, + }, testCtx.recorder) + testCtx.handler.HandleMessage(ctx) + assert.Equal(t, "ok", testCtx.recorder.Body.String()) +} + +func TestHandler_HandleMessage_NoSecretKey(t *testing.T) { + testCtx := getTestHandlerCtx() + ctx := getTestRequestCtx(&messageRequest{ + Type: "message_new", + }, testCtx.recorder) + testCtx.handler.HandleMessage(ctx) + assert.Equal(t, "", testCtx.recorder.Body.String()) +} + +func TestHandler_handleSearch_BadCommand(t *testing.T) { + testCtx := getTestHandlerCtx() + testCtx.handler.handleSearch(&messageRequest{ + Object: userMessage{ + Body: "!s", + UserId: 1, + }, + }) + assert.Equal(t, []testMessage{ + { + userId: 1, + message: incorrectMessage, + }, + }, testCtx.sender.sent) + assert.True(t, strings.Contains(testCtx.logBuf.String(), "[info]")) +} + +func TestHandler_handleSearch_GoodCommand(t *testing.T) { + testCtx := getTestHandlerCtx() + testCtx.handler.handleSearch(&messageRequest{ + Object: userMessage{ + Body: "!s grn 228", + UserId: 1, + }, + }) + assert.Equal(t, []testMessage{ + { + userId: 1, + message: "good", + }, + }, testCtx.sender.sent) +} + +func TestHandler_handleSearch_NotFoundCard(t *testing.T) { + testCtx := getTestHandlerCtx() + testCtx.handler.handleSearch(&messageRequest{ + Object: userMessage{ + Body: "absolutely_random_card", + UserId: 1, + }, + }) + assert.Equal(t, []testMessage{ + { + userId: 1, + message: cardNotFoundMessage, + }, + }, testCtx.sender.sent) + assert.True(t, strings.Contains(testCtx.logBuf.String(), "[info]")) +} + +func TestHandler_handleSearch_BadCard(t *testing.T) { + testCtx := getTestHandlerCtx() + testCtx.handler.handleSearch(&messageRequest{ + Object: userMessage{ + Body: "bad", + UserId: 1, + }, + }) + assert.Equal(t, []testMessage{ + { + userId: 1, + message: pricesUnavailableMessage, + }, + }, testCtx.sender.sent) + assert.True(t, strings.Contains(testCtx.logBuf.String(), "[error]")) +} +func TestHandler_handleSearch_Uncached(t *testing.T) { + testCtx := getTestHandlerCtx() + testCtx.handler.handleSearch(&messageRequest{ + Object: userMessage{ + Body: "uncached", + UserId: 1, + }, + }) + assert.Equal(t, []testMessage{ + { + userId: 1, + message: "uncached", + }, + }, testCtx.sender.sent) + msg, _ := testCtx.handler.Cache.Get("uncached") + assert.Equal(t, "uncached", msg) +}