Escribir un bot de Slack para Scrum Poker en Go

¡Hola! Hoy escribiremos un bot de Slack para Scrum Poker en lenguaje Go. Escribiremos, si es posible, sin frameworks y librerías externas, ya que nuestro objetivo es entender el lenguaje de programación Go y comprobar qué tan conveniente es este lenguaje para desarrollar este tipo de proyectos.





Descargo de responsabilidad

Go . Python. , Python - . , , , " " Go, .





, , ( ), "" . .





, !





, , — .





. (web), Slack UI Block Kit (ui), / (storage), (config). :





config/
storage/
ui/
web/
-- clients/
-- server/
main.go
      
      



http



. Server



web -> server



:





server.go
package server

import (
	"context"
	"log"
	"net/http"
	"os"
	"os/signal"
	"sync/atomic"
	"time"
)

type Server struct {
  //                main.go
	healthy        int32
	logger         *log.Logger
}

func NewServer(logger *log.Logger) *Server {
	return &Server{
		logger: logger,
	}
}

      
      



. . , main.go



, . . . . . , . :





server.go
func (s *Server) setupRouter() http.Handler {  // TODO
	router := http.NewServeMux()
  return router
}

func (s *Server) Serve(address string) {
	server := &http.Server{
		Addr:         address,
    Handler:      s.setupRouter(),
		ErrorLog:     s.logger, //  
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 10 * time.Second,
		IdleTimeout:  15 * time.Second,
	}

  //      
	done := make(chan bool)
	quit := make(chan os.Signal, 1)
  //      
	signal.Notify(quit, os.Interrupt)

	go func() {
		<-quit
		s.logger.Println("Server is shutting down...")
    //     healthcheck' 
		atomic.StoreInt32(&s.healthy, 0)

    //   30     ,     
		ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
		defer cancel()

    //    ,      
		server.SetKeepAlivesEnabled(false)
    //  
		if err := server.Shutdown(ctx); err != nil {
			s.logger.Fatalf("Could not gracefully shutdown the server: %v\n", err)
		}
		close(done)
	}()

	s.logger.Println("Server is ready to handle requests at", address)
  //    ,      
	atomic.StoreInt32(&s.healthy, 1)
  //  
	if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
		s.logger.Fatalf("Could not listen on %s: %v\n", address, err)
	}

  //      ,      
	<-done
	s.logger.Println("Server stopped")
}

      
      



. web -> server -> handlers



:





healthcheck.go
package handlers

import (
	"net/http"
)

func Healthcheck() http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Write("OK")
	})
}

      
      



:





server.go
//   

func (s *Server) setupRouter() http.Handler {
	router := http.NewServeMux()
	router.Handle(
		"/healthcheck",
		handlers.Healthcheck(),
	)
  return router
}

//   
      
      



main.go



:





package main

import (
	"log"
  "os"
  "go-scrum-poker-bot/web/server"
)

func main() {
  //        "INFO:". 
  //      stdout
	logger := log.New(os.Stdout, "INFO: ", log.LstdFlags)
	app := server.NewServer(logger)

	app.Serve(":8000")
}
      
      



:





go run main.go
      
      



, :8000



. . , . ;) , , Slack .





NGROK

, , ngrok. , . Slack . , , :





ngrok http 8000
      
      



, - :





ngrok by @inconshreveable                                                                                                            (Ctrl+C to quit)
                                                                                                                                                     
Session Status                online                                                                                                                 
Account                       Sayakhov Ilya (Plan: Free)                                                                                             
Version                       2.3.35                                                                                                                 
Region                        United States (us)                                                                                                     
Web Interface                 http://127.0.0.1:4040                                                                                                  
Forwarding                    http://ffd3cfcc460c.ngrok.io -> http://localhost:8000                                                                  
Forwarding                    https://ffd3cfcc460c.ngrok.io -> http://localhost:8000                                                                 
                                                                                                                                                     
Connections                   ttl     opn     rt1     rt5     p50     p90                                                                            
                              0       0       0.00    0.00    0.00    0.00     
      
      



https://ffd3cfcc460c.ngrok.io



. .





Slash commands

Slack. -> Create New App



. GoScrumPokerBot



Workspace



. , . OAuth & Permissions -> Scopes



: chat:write



, commands



. , , slash . Reinstall to Workspace



. ! Slash commands



/poker



.





Request URL + . : https://ffd3cfcc460c.ngrok.io/play-poker



.





Slash command handler

. web -> server -> handlers



play_poker.go



:





func PlayPokerCommand() http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.Write([]byte(`{"response_type": "ephemeral", "text": "Hello world!"}`))
	})
}
      
      



:





server.go
func (s *Server) setupRouter() http.Handler {
	router := http.NewServeMux()
	router.Handle(
		"/healthcheck",
		handlers.Healthcheck(),
	)
	router.Handle(
		"/play-poker",
		handlers.PlayPokerCommand(),
	)
  return router
}
      
      



Slack : /poker



. - :





Slack. . . ( ). http



. web -> clients



. client.go



:





client.go
package clients

//      
type Handler func(request *Request) *Response

//     middleware (   )
type Middleware func(handler Handler, request *Request) Handler

//   http 
type Client interface {
	Make(request *Request) *Response
}

//   
type BasicClient struct {
	client     *http.Client
	middleware []Middleware
}

func NewBasicClient(client *http.Client, middleware []Middleware) Client {
	return &BasicClient{client: client, middleware: middleware}
}

//      
func (c *BasicClient) makeRequest(request *Request) *Response {
	payload, err := request.ToBytes() // TODO
	if err != nil {
		return &Response{Error: err}
	}

  //   request,    
	req, err := http.NewRequest(request.Method, request.URL, bytes.NewBuffer(payload))
	if err != nil {
		return &Response{Error: err}
	}

  //  
	for name, value := range request.Headers {
		req.Header.Add(name, value)
	}

  //  
	resp, err := c.client.Do(req)
	if err != nil {
		return &Response{Error: err}
	}
	defer resp.Body.Close()

  //   
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return &Response{Error: err}
	}

	err = nil
  //   -     20x,  
	if resp.StatusCode > http.StatusIMUsed || resp.StatusCode < http.StatusOK {
		err = fmt.Errorf("Bad response. Status: %d, Body: %s", resp.StatusCode, string(body))
	}

	return &Response{
		Status:  resp.StatusCode,
		Body:    body,
		Headers: resp.Header,
		Error:   err,
	}
}

//     
func (c *BasicClient) Make(request *Request) *Response {
	if request.Headers == nil {
		request.Headers = make(map[string]string)
	}
  
  //  middleware
	handler := c.makeRequest
	for _, middleware := range c.middleware {
		handler = middleware(handler, request)
	}

	return handler(request)
}

      
      



web -> clients



:





request.go
package clients

import "encoding/json"

type Request struct {
	URL     string
	Method  string
	Headers map[string]string
	Json    interface{}
}

func (r *Request) ToBytes() ([]byte, error) {
	if r.Json != nil {
		result, err := json.Marshal(r.Json)
		if err != nil {
			return []byte{}, err
		}
		return result, nil
	}

	return []byte{}, nil
}

      
      



ToBytes()



. testify/assert, if', :) . , pytest



assert



, - :





request_test.go
package clients_test

import (
	"encoding/json"
	"go-scrum-poker-bot/web/clients"
	"reflect"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestRequestToBytes(t *testing.T) {
  //    -  pytest.parametrize (,   Go    ,    )
	testCases := []struct {
		json interface{}
		data []byte
		err  error
	}{
		{map[string]string{"test_key": "test_value"}, []byte("{\"test_key\":\"test_value\"}"), nil},
		{nil, []byte{}, nil},
		{make(chan int), []byte{}, &json.UnsupportedTypeError{Type: reflect.TypeOf(make(chan int))}},
	}

  //     
	for _, testCase := range testCases {
		request := clients.Request{
			URL:     "https://example.com",
			Method:  "GET",
			Headers: nil,
			Json:    testCase.json,
		}

		actual, err := request.ToBytes()

    //  
		assert.Equal(t, testCase.err, err)
		assert.Equal(t, testCase.data, actual)
	}
}

      
      



web -> clients:







response.go
package clients

import "encoding/json"

type Response struct {
	Status  int
	Headers map[string][]string
	Body    []byte
	Error   error
}

//     ,                if err != nil
func (r *Response) Json(to interface{}) error {
	if r.Error != nil {
		return r.Error
	}
	return json.Unmarshal(r.Body, to)
}

      
      



, Json(to interface{})



:





response_test.go
package clients_test

import (
	"errors"
	"go-scrum-poker-bot/web/clients"
	"testing"

	"github.com/stretchr/testify/assert"
)

//     
func TestResponseJson(t *testing.T) {
	to := struct {
		TestKey string `json:"test_key"`
	}{}
	response := clients.Response{
		Status:  200,
		Headers: nil,
		Body:    []byte(`{"test_key": "test_value"}`),
		Error:   nil,
	}

	err := response.Json(&to)

	assert.Equal(t, nil, err)
	assert.Equal(t, "test_value", to.TestKey)
}

//    
func TestResponseJsonError(t *testing.T) {
	expectedErr := errors.New("Error!")
	response := clients.Response{
		Status:  200,
		Headers: nil,
		Body:    nil,
		Error:   expectedErr,
	}

	err := response.Json(map[string]string{})

	assert.Equal(t, expectedErr, err)
}

      
      



, , . http



. http



. , :





client_test.go
package clients_test

import (
	"bytes"
	"go-scrum-poker-bot/web/clients"
	"io/ioutil"
	"net/http"
	"testing"

	"github.com/stretchr/testify/assert"
)

//     
type RoundTripFunc func(request *http.Request) *http.Response

func (f RoundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) {
	return f(request), nil
}

//  mock  
func NewTestClient(fn RoundTripFunc) *http.Client {
	return &http.Client{
		Transport: RoundTripFunc(fn),
	}
}

//  
func TestMakeRequest(t *testing.T) {
	url := "https://example.com/ok"

  //  mock      
	httpClient := NewTestClient(func(req *http.Request) *http.Response {
		assert.Equal(t, req.URL.String(), url)

		return &http.Response{
			StatusCode: http.StatusOK,
			Body:       ioutil.NopCloser(bytes.NewBufferString("OK")),
			Header:     make(http.Header),
		}
	})

  //   http    http 
	webClient := clients.NewBasicClient(httpClient, nil)
	response := webClient.Make(&clients.Request{
		URL:     url,
		Method:  "GET",
		Headers: map[string]string{"Content-Type": "application/json"},
		Json:    nil,
	})

	assert.Equal(t, http.StatusOK, response.Status)
}

//    response
func TestMakeRequestError(t *testing.T) {
	url := "https://example.com/error"

	httpClient := NewTestClient(func(req *http.Request) *http.Response {
		assert.Equal(t, req.URL.String(), url)

		return &http.Response{
			StatusCode: http.StatusBadGateway,
			Body:       ioutil.NopCloser(bytes.NewBufferString("Bad gateway")),
			Header:     make(http.Header),
		}
	})

	webClient := clients.NewBasicClient(httpClient, nil)
	response := webClient.Make(&clients.Request{
		URL:     url,
		Method:  "GET",
		Headers: map[string]string{"Content-Type": "application/json"},
		Json:    nil,
	})

	assert.Equal(t, http.StatusBadGateway, response.Status)
}

      
      



! middleware



. , , middleware



. / API / . Slack Authorization



, OAuth & Permissions



. web -> clients -> middleware



:





auth.go
package middleware

import (
	"fmt"
	"go-scrum-poker-bot/web/clients"
)

//      middleware    
func Auth(token string) clients.Middleware {
	return func(handler clients.Handler, request *clients.Request) clients.Handler {
		return func(request *clients.Request) *clients.Response {
			request.Headers["Authorization"] = fmt.Sprintf("Bearer %s", token)
			return handler(request)
		}
	}
}

      
      



:





auth_test.go
package middleware_test

import (
	"fmt"
	"go-scrum-poker-bot/web/clients"
	"go-scrum-poker-bot/web/clients/middleware"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestAuthMiddleware(t *testing.T) {
	token := "test"
	request := &clients.Request{
		Headers: map[string]string{},
	}
	handler := middleware.Auth(token)(
		func(request *clients.Request) *clients.Response {
			return &clients.Response{}
		},
		request,
	)
	handler(request)

	assert.Equal(t, map[string]string{"Authorization": fmt.Sprintf("Bearer %s", token)}, request.Headers)
}

      
      



middleware



Content-Type: application/json



. :).





PlayPoker



:





play_poker.go
package handlers

import (
	"errors"
	"go-scrum-poker-bot/ui"
	"go-scrum-poker-bot/web/clients"
	"go-scrum-poker-bot/web/server/models"
	"net/http"

	"github.com/google/uuid"
)

func PlayPokerCommand(webClient clients.Client, uiBuilder *ui.Builder) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    //  ,      POST Form    ID 
		if r.PostFormValue("channel_id") == "" || r.PostFormValue("text") == "" {
			w.Write(models.ResponseError(errors.New("Please write correct subject"))) // TODO
			return
		}

		resp := webClient.Make(&clients.Request{
			URL:    "https://slack.com/api/chat.postMessage",
			Method: "POST",
      Json: uiBuilder.Build( // TODO:  builder 
				r.PostFormValue("channel_id"),
				uuid.New().String(),
				r.PostFormValue("text"),
				nil,
				false,
			),
		})
		if resp.Error != nil {
			w.Write(models.ResponseError(resp.Error)) // TODO
			return
		}
	})
}

      
      



web -> server -> models



. errors.go



:





errors.go
package models

import (
	"encoding/json"
	"fmt"
)

type SlackError struct {
	ResponseType string `json:"response_type"`
	Text         string `json:"text"`
}

func ResponseError(err error) []byte {
	resp, err := json.Marshal(
		SlackError{
			ResponseType: "ephemeral",
			Text:         fmt.Sprintf("Sorry, there is some error happened. Error: %s", err.Error()),
		},
	)
	if err != nil {
		return []byte("Sorry. Some error happened")
	}
	return resp
}

      
      



:





play_poker_test.go
package handlers_test

import (
	"errors"
	"go-scrum-poker-bot/config"
	"go-scrum-poker-bot/ui"
	"go-scrum-poker-bot/web/server/handlers"
	"go-scrum-poker-bot/web/server/models"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestPlayPokerHandler(t *testing.T) {
	config := config.NewConfig() // TODO
	mockClient := &MockClient{}
	uiBuilder := ui.NewBuilder(config) // TODO

	responseRec := httptest.NewRecorder()

	router := http.NewServeMux()
	router.Handle("/play-poker", handlers.PlayPokerCommand(mockClient, uiBuilder))

	payload := url.Values{"channel_id": {"test"}, "text": {"test"}}.Encode()
	request, err := http.NewRequest("POST", "/play-poker", strings.NewReader(payload))
	request.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	router.ServeHTTP(responseRec, request)

	assert.Nil(t, err)
	assert.Equal(t, http.StatusOK, responseRec.Code)
	assert.Empty(t, responseRec.Body.String())
	assert.Equal(t, true, mockClient.Called)
}

func TestPlayPokerHandlerEmptyBodyError(t *testing.T) {
	config := config.NewConfig()
	mockClient := &MockClient{}
	uiBuilder := ui.NewBuilder(config)

	responseRec := httptest.NewRecorder()

	router := http.NewServeMux()
	router.Handle("/play-poker", handlers.PlayPokerCommand(mockClient, uiBuilder))

	payload := url.Values{}.Encode()
	request, _ := http.NewRequest("POST", "/play-poker", strings.NewReader(payload))
	request.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	router.ServeHTTP(responseRec, request)

	expected := string(models.ResponseError(errors.New("Please write correct subject")))

	assert.Equal(t, http.StatusOK, responseRec.Code)
	assert.Equal(t, expected, responseRec.Body.String())
	assert.Equal(t, false, mockClient.Called)
}

func TestPlayPokerHandlerRequestError(t *testing.T) {
	errMsg := "Error msg"
	config := config.NewConfig() // TODO
	mockClient := &MockClient{Error: errMsg}
	uiBuilder := ui.NewBuilder(config) // TODO

	responseRec := httptest.NewRecorder()

	router := http.NewServeMux()
	router.Handle("/play-poker", handlers.PlayPokerCommand(mockClient, uiBuilder))

	payload := url.Values{"channel_id": {"test"}, "text": {"test"}}.Encode()
	request, _ := http.NewRequest("POST", "/play-poker", strings.NewReader(payload))
	request.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	router.ServeHTTP(responseRec, request)

	expected := string(models.ResponseError(errors.New(errMsg)))

	assert.Equal(t, http.StatusOK, responseRec.Code)
	assert.Equal(t, expected, responseRec.Body.String())
	assert.Equal(t, true, mockClient.Called)
}

      
      



mock



http



:





common_test.go
package handlers_test

import (
	"errors"
	"go-scrum-poker-bot/web/clients"
)

type MockClient struct {
	Called bool
	Error  string
}

func (c *MockClient) Make(request *clients.Request) *clients.Response {
	c.Called = true

	var err error = nil
	if c.Error != "" {
		err = errors.New(c.Error)
	}
	return &clients.Response{Error: err}
}
      
      



, PlayPoker



.





UI Slack UI Block Kit. , . , Slack API . UI Builder



ui . , , . , , ( , ) block_id



. action_id



.





. config :





config.go
package config

type Config struct {
	App   *App
	Slack *Slack
	Redis *Redis
}

func NewConfig() *Config {
	return &Config{
		App: &App{
			ServerAddress: getStrEnv("WEB_SERVER_ADDRESS", ":8000"),
			PokerRanks:    getListStrEnv("POKER_RANKS", "?,0,0.5,1,2,3,5,8,13,20,40,100"),
		},
		Slack: &Slack{
			Token: getStrEnv("SLACK_TOKEN", "FILL_ME"),
		},
    //  
		Redis: &Redis{
			Host: getStrEnv("REDIS_HOST", "0.0.0.0"),
			Port: getIntEnv("REDIS_PORT", "6379"),
			DB:   getIntEnv("REDIS_DB", "0"),
		},
	}
}

//    env   default
func getStrEnv(key string, defaultValue string) string {
	if value, ok := os.LookupEnv(key); ok {
		return value
	}
	return defaultValue
}

//  int   env   default
func getIntEnv(key string, defaultValue string) int {
	value, err := strconv.Atoi(getStrEnv(key, defaultValue))
	if err != nil {
		panic(fmt.Sprintf("Incorrect env value for %s", key))
	}

	return value
}

//   (e.g. 0,1,2,3,4,5)  env   default
func getListStrEnv(key string, defaultValue string) []string {
	value := []string{}
	for _, item := range strings.Split(getStrEnv(key, defaultValue), ",") {
		value = append(value, strings.TrimSpace(item))
	}
	return value
}

      
      



. :





config_test.go
package config_test

import (
    "go-scrum-poker-bot/config"
    "os"
    "testing"

    "github.com/stretchr/testify/assert"
)

func TestNewConfig(t *testing.T) {
    c := config.NewConfig()

    assert.Equal(t, "0.0.0.0", c.Redis.Host)
    assert.Equal(t, 6379, c.Redis.Port)
    assert.Equal(t, 0, c.Redis.DB)
    assert.Equal(t, []string{"?", "0", "0.5", "1", "2", "3", "5", "8", "13", "20", "40", "100"}, c.App.PokerRanks)
}

func TestNewConfigIncorrectIntFromEnv(t *testing.T) {
    os.Setenv("REDIS_PORT", "-")

    assert.Panics(t, func() { config.NewConfig() })
}

      
      



, . main.go



:





main.go
package main

import (
	"fmt"
	"go-scrum-poker-bot/config"
	"go-scrum-poker-bot/ui"
	"go-scrum-poker-bot/web/clients"
	clients_middleware "go-scrum-poker-bot/web/clients/middleware"
	"go-scrum-poker-bot/web/server"
  "log"
	"net/http"
	"os"
	"time"
)

func main() {
	logger := log.New(os.Stdout, "INFO: ", log.LstdFlags)
	config := config.NewConfig()
	builder := ui.NewBuilder(config)
	webClient := clients.NewBasicClient(
		&http.Client{
			Timeout: 5 * time.Second,
		},
		[]clients.Middleware{ //  middleware
			clients_middleware.Auth(config.Slack.Token),
			clients_middleware.JsonContentType,
			clients_middleware.Log(logger),
		},
	)

	app := server.NewServer(
		logger,
		webClient,
		builder,
	)
	app.Serve(config.App.ServerAddress)
}

      
      



/poker



.





Slack Interactivity

. Your apps -> -> Interactivity & Shortcuts



. Request URL :





https://ffd3cfcc460c.ngrok.io/interactivity
      
      



InteractionCallback



web -> server -> handlers



:





interaction_callback.go
package handlers

import (
	"go-scrum-poker-bot/storage"
	"go-scrum-poker-bot/ui"
	"go-scrum-poker-bot/ui/blocks"
	"go-scrum-poker-bot/web/clients"
	"go-scrum-poker-bot/web/server/models"
	"net/http"
)

func InteractionCallback(
	userStorage storage.UserStorage,
	sessionStorage storage.SessionStorage,
	uiBuilder *ui.Builder,
	webClient clients.Client,
) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		var callback models.Callback
    //   
		data, err := callback.SerializedData([]byte(r.PostFormValue("payload")))
		if err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}

    // TODO:    
		users := userStorage.All(data.SessionID)
		visible := sessionStorage.GetVisibility(data.SessionID)

		err = nil
    //             
		switch data.Action.ActionID {
		case ui.VOTE_ACTION_ID:
			users[callback.User.Username] = data.Action.SelectedOption.Value
			err = userStorage.Save(data.SessionID, callback.User.Username, data.Action.SelectedOption.Value)
		case ui.RESULTS_VISIBILITY_ACTION_ID:
			visible = !visible
			err = sessionStorage.SetVisibility(data.SessionID, visible)
		}
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

    //       response URL.     
		resp := webClient.Make(&clients.Request{
			URL:    callback.ResponseURL,
			Method: "POST",
			Json: &blocks.Interactive{
				ReplaceOriginal: true,
				Blocks:          uiBuilder.BuildBlocks(data.Subject, users, data.SessionID, visible),
				LinkNames:       true,
			},
		})
		if resp.Error != nil {
			http.Error(w, resp.Error.Error(), http.StatusInternalServerError)
			return
		}
	})
}

      
      



. . storage



:





storage.go
package storage

type UserStorage interface {
	All(sessionID string) map[string]string
	Save(sessionID string, username string, value string) error
}

type SessionStorage interface {
	GetVisibility(sessionID string) bool
	SetVisibility(sessionID string, state bool) error
}

      
      



, , , Redis ( ).





Callback



. web -> server -> models



:





callback.go
package models

import (
	"encoding/json"
	"errors"
	"go-scrum-poker-bot/ui"
)

type User struct {
	Username string `json:"username"`
}

type Text struct {
	Type string `json:"type"`
	Text string `json:"text"`
}

type Block struct {
	Type    string `json:"type"`
	BlockID string `json:"block_id"`
	Text    *Text  `json:"text,omitempty"`
}

type Message struct {
	Blocks []*Block `json:"blocks,omitempty"`
}

type SelectedOption struct {
	Value string `json:"value"`
}

type Action struct {
	BlockID        string          `json:"block_id"`
	ActionID       string          `json:"action_id"`
	Value          string          `json:"value,omitempty"`
	SelectedOption *SelectedOption `json:"selected_option,omitempty"`
}

type SerializedData struct {
	SessionID string
	Subject   string
	Action    *Action
}

type Callback struct {
	ResponseURL string    `json:"response_url"`
	User        *User     `json:"user"`
	Actions     []*Action `json:"actions"`
	Message     *Message  `json:"message,omitempty"`
}

//   ID ,       
func (c *Callback) getSessionID() (string, error) {
	for _, action := range c.Actions {
		if action.BlockID != "" {
			return action.BlockID, nil
		}
	}

	return "", errors.New("Invalid session ID")
}

//   
func (c *Callback) getSubject() (string, error) {
	for _, block := range c.Message.Blocks {
		if block.BlockID == ui.SUBJECT_BLOCK_ID && block.Text != nil {
			return block.Text.Text, nil
		}
	}

	return "", errors.New("Invalid subject")
}

//     
func (c *Callback) getAction() (*Action, error) {
	for _, action := range c.Actions {
		if action.ActionID == ui.VOTE_ACTION_ID || action.ActionID == ui.RESULTS_VISIBILITY_ACTION_ID {
			return action, nil
		}
	}

	return nil, errors.New("Invalid action")
}

func (c *Callback) SerializedData(data []byte) (*SerializedData, error) {
	err := json.Unmarshal(data, c)
	if err != nil {
		return nil, err
	}

	sessionID, err := c.getSessionID()
	if err != nil {
		return nil, err
	}

	subject, err := c.getSubject()
	if err != nil {
		return nil, err
	}

	action, err := c.getAction()
	if err != nil {
		return nil, err
	}

	return &SerializedData{
		SessionID: sessionID,
		Subject:   subject,
		Action:    action,
	}, nil
}

      
      



:





interaction_callback_test.go
package handlers_test

import (
	"encoding/json"
	"go-scrum-poker-bot/config"
	"go-scrum-poker-bot/ui"
	"go-scrum-poker-bot/web/server/handlers"
	"go-scrum-poker-bot/web/server/models"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestInteractionCallbackHandlerActions(t *testing.T) {
	config := config.NewConfig()
	mockClient := &MockClient{}
	mockUserStorage := &MockUserStorage{}
	mockSessionStorage := &MockSessionStorage{}
	uiBuilder := ui.NewBuilder(config)

	router := http.NewServeMux()
	router.Handle(
		"/interactivity",
		handlers.InteractionCallback(mockUserStorage, mockSessionStorage, uiBuilder, mockClient),
	)

	actions := []*models.Action{
		{
			BlockID:        "test",
			ActionID:       ui.RESULTS_VISIBILITY_ACTION_ID,
			Value:          "test",
			SelectedOption: nil,
		},
		{
			BlockID:        "test",
			ActionID:       ui.VOTE_ACTION_ID,
			Value:          "test",
			SelectedOption: &models.SelectedOption{Value: "1"},
		},
	}

  //      
	for _, action := range actions {
		responseRec := httptest.NewRecorder()

		data, _ := json.Marshal(models.Callback{
			ResponseURL: "test",
			User:        &models.User{Username: "test"},
			Actions:     []*models.Action{action},
			Message: &models.Message{
				Blocks: []*models.Block{
					{
						Type:    "test",
						BlockID: ui.SUBJECT_BLOCK_ID,
						Text:    &models.Text{Type: "test", Text: "test"},
					},
				},
			},
		})
		payload := url.Values{"payload": {string(data)}}.Encode()
		request, err := http.NewRequest("POST", "/interactivity", strings.NewReader(payload))
		request.Header.Set("Content-Type", "application/x-www-form-urlencoded")

		router.ServeHTTP(responseRec, request)

		assert.Nil(t, err)
		assert.Equal(t, http.StatusOK, responseRec.Code)
		assert.Empty(t, responseRec.Body.String())
		assert.Equal(t, true, mockClient.Called)
	}
}

      
      



mock



. common_test.go



:





common_test.go
//  

type MockUserStorage struct{}

func (s *MockUserStorage) All(sessionID string) map[string]string {
	return map[string]string{"user": "1"}
}

func (s *MockUserStorage) Save(sessionID string, username string, value string) error {
	return nil
}

type MockSessionStorage struct{}

func (s *MockSessionStorage) GetVisibility(sessionID string) bool {
	return true
}

func (s *MockSessionStorage) SetVisibility(sessionID string, state bool) error {
	return nil
}

      
      



:





server.go
//  

func (s *Server) setupRouter() http.Handler {
	router := http.NewServeMux()
	router.Handle(
		"/healthcheck",
		handlers.Healthcheck(),
	)
	router.Handle(
		"/play-poker",
		handlers.PlayPokerCommand(s.webClient, s.uiBuilder),
	)
	router.Handle(
		"/interactivity",
		handlers.InteractionCallback(s.userStorage, s.sessionStorage, s.uiBuilder, s.webClient),
	)

	return router
}

//  
      
      



, , + - , . middleware



. web -> server -> middleware



:





log.go
package middleware

import (
	"log"
	"net/http"
)

func Log(logger *log.Logger) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			defer func() {
				logger.Printf(
					"Handle request: [%s]: %s - %s - %s",
					r.Method, r.URL.Path, r.RemoteAddr, r.UserAgent(),
				)
			}()
			next.ServeHTTP(w, r)
		})
	}
}

      
      



:





log_test.go
package middleware_test

import (
	"bytes"
	"go-scrum-poker-bot/web/server/middleware"
	"log"
	"net/http"
	"net/http/httptest"
	"os"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
)

type logHandler struct{}

func (h *logHandler) ServeHTTP(http.ResponseWriter, *http.Request) {}

func TestLogMiddleware(t *testing.T) {
	var buf bytes.Buffer
	logger := log.New(os.Stdout, "INFO: ", log.LstdFlags)
  //    output  ,     
	logger.SetOutput(&buf)

	handler := &logHandler{}
  //  mock recorder    Go
	responseRec := httptest.NewRecorder()

	router := http.NewServeMux()
	router.Handle("/test", middleware.Log(logger)(handler))

	request, err := http.NewRequest("GET", "/test", strings.NewReader(""))

	router.ServeHTTP(responseRec, request)

	assert.Nil(t, err)
	assert.Equal(t, http.StatusOK, responseRec.Code)
  // ,    - .   ,  ,  middleware  
	assert.NotEmpty(t, buf.String())
}

      
      



middleware



.





. Redis, , - , . go-redis redismock .





Scrum Poker . storage



:





users.go
package storage

import (
	"context"
	"fmt"

	"github.com/go-redis/redis/v8"
)

//  
const SESSION_USERS_TPL = "SESSION:%s:USERS"
const USER_VOTE_TPL = "SESSION:%s:USERNAME:%s:VOTE"

type UserRedisStorage struct {
	redis   *redis.Client
	context context.Context
}

func NewUserRedisStorage(redisClient *redis.Client) *UserRedisStorage {
	return &UserRedisStorage{
		redis:   redisClient,
		context: context.Background(),
	}
}

func (s *UserRedisStorage) All(sessionID string) map[string]string {
	users := make(map[string]string)

  //     set,       . 
  //      
	for _, username := range s.redis.SMembers(s.context, fmt.Sprintf(SESSION_USERS_TPL, sessionID)).Val() {
		users[username] = s.redis.Get(s.context, fmt.Sprintf(USER_VOTE_TPL, sessionID, username)).Val()
	}
	return users
}

func (s *UserRedisStorage) Save(sessionID string, username string, value string) error {
	err := s.redis.SAdd(s.context, fmt.Sprintf(SESSION_USERS_TPL, sessionID), username).Err()
	if err != nil {
		return err
	}

  //       . 
  //    ,     ,  -1   
	err = s.redis.Set(s.context, fmt.Sprintf(USER_VOTE_TPL, sessionID, username), value, -1).Err()
	if err != nil {
		return err
	}

	return nil
}

      
      



:





users_test.go
package storage_test

import (
	"errors"
	"fmt"
	"go-scrum-poker-bot/storage"
	"testing"

	"github.com/go-redis/redismock/v8"
	"github.com/stretchr/testify/assert"
)

func TestAll(t *testing.T) {
	sessionID, username, value := "test", "user", "1"

	redisClient, mock := redismock.NewClientMock()
	usersStorage := storage.NewUserRedisStorage(redisClient)

  // Redis mock          
	mock.ExpectSMembers(
		fmt.Sprintf(storage.SESSION_USERS_TPL, sessionID),
	).SetVal([]string{username})
	mock.ExpectGet(
		fmt.Sprintf(storage.USER_VOTE_TPL, sessionID, username),
	).SetVal(value)

	assert.Equal(t, map[string]string{username: value}, usersStorage.All(sessionID))
}

func TestSave(t *testing.T) {
	sessionID, username, value := "test", "user", "1"

	redisClient, mock := redismock.NewClientMock()
	usersStorage := storage.NewUserRedisStorage(redisClient)

	mock.ExpectSAdd(
		fmt.Sprintf(storage.SESSION_USERS_TPL, sessionID),
		username,
	).SetVal(1)
	mock.ExpectSet(
		fmt.Sprintf(storage.USER_VOTE_TPL, sessionID, username),
		value,
		-1,
	).SetVal(value)

	assert.Equal(t, nil, usersStorage.Save(sessionID, username, value))
}

func TestSaveSAddErr(t *testing.T) {
	sessionID, username, value, err := "test", "user", "1", errors.New("ERROR")

	redisClient, mock := redismock.NewClientMock()
	usersStorage := storage.NewUserRedisStorage(redisClient)

	mock.ExpectSAdd(
		fmt.Sprintf(storage.SESSION_USERS_TPL, sessionID),
		username,
	).SetErr(err)

	assert.Equal(t, err, usersStorage.Save(sessionID, username, value))
}

func TestSaveSetErr(t *testing.T) {
	sessionID, username, value, err := "test", "user", "1", errors.New("ERROR")

	redisClient, mock := redismock.NewClientMock()
	usersStorage := storage.NewUserRedisStorage(redisClient)

	mock.ExpectSAdd(
		fmt.Sprintf(storage.SESSION_USERS_TPL, sessionID),
		username,
	).SetVal(1)
	mock.ExpectSet(
		fmt.Sprintf(storage.USER_VOTE_TPL, sessionID, username),
		value,
		-1,
	).SetErr(err)

	assert.Equal(t, err, usersStorage.Save(sessionID, username, value))
}

      
      



"" . :





sessions.go
package storage

import (
	"context"
	"fmt"
	"strconv"

	"github.com/go-redis/redis/v8"
)

//   
const SESSION_VOTES_HIDDEN_TPL = "SESSION:%s:VOTES_HIDDEN"

type SessionRedisStorage struct {
	redis   *redis.Client
	context context.Context
}

func NewSessionRedisStorage(redisClient *redis.Client) *SessionRedisStorage {
	return &SessionRedisStorage{
		redis:   redisClient,
		context: context.Background(),
	}
}

func (s *SessionRedisStorage) GetVisibility(sessionID string) bool {
	value, _ := strconv.ParseBool(
		s.redis.Get(s.context, fmt.Sprintf(SESSION_VOTES_HIDDEN_TPL, sessionID)).Val(),
	)

	return value
}

func (s *SessionRedisStorage) SetVisibility(sessionID string, state bool) error {
	return s.redis.Set(
		s.context,
		fmt.Sprintf(SESSION_VOTES_HIDDEN_TPL, sessionID),
		strconv.FormatBool(state),
		-1,
	).Err()
}

      
      



:





sessions_test.go
package storage_test

import (
	"errors"
	"fmt"
	"go-scrum-poker-bot/storage"
	"strconv"
	"testing"

	"github.com/go-redis/redismock/v8"
	"github.com/stretchr/testify/assert"
)

func TestGetVisibility(t *testing.T) {
	sessionID, state := "test", true

	redisClient, mock := redismock.NewClientMock()

	mock.ExpectGet(
		fmt.Sprintf(storage.SESSION_VOTES_HIDDEN_TPL, sessionID),
	).SetVal(strconv.FormatBool(state))

	sessionStorage := storage.NewSessionRedisStorage(redisClient)

	assert.Equal(t, state, sessionStorage.GetVisibility(sessionID))
}

func TestSetVisibility(t *testing.T) {
	sessionID, state := "test", true

	redisClient, mock := redismock.NewClientMock()

	mock.ExpectSet(
		fmt.Sprintf(storage.SESSION_VOTES_HIDDEN_TPL, sessionID),
		strconv.FormatBool(state),
		-1,
	).SetVal("1")

	sessionStorage := storage.NewSessionRedisStorage(redisClient)

	assert.Equal(t, nil, sessionStorage.SetVisibility(sessionID, state))
}

func TestSetVisibilityErr(t *testing.T) {
	sessionID, state, err := "test", true, errors.New("ERROR")

	redisClient, mock := redismock.NewClientMock()

	mock.ExpectSet(
		fmt.Sprintf(storage.SESSION_VOTES_HIDDEN_TPL, sessionID),
		strconv.FormatBool(state),
		-1,
	).SetErr(err)

	sessionStorage := storage.NewSessionRedisStorage(redisClient)

	assert.Equal(t, err, sessionStorage.SetVisibility(sessionID, state))
}

      
      



! main.go server.go:





server.go
package server

import (
	"context"
	"go-scrum-poker-bot/storage"
	"go-scrum-poker-bot/ui"
	"go-scrum-poker-bot/web/clients"
	"go-scrum-poker-bot/web/server/handlers"
	"log"
	"net/http"
	"os"
	"os/signal"
	"sync/atomic"
	"time"
)

//    middleware
type Middleware func(next http.Handler) http.Handler

//   
type Server struct {
	healthy        int32
	middleware     []Middleware
	logger         *log.Logger
	webClient      clients.Client
	uiBuilder      *ui.Builder
	userStorage    storage.UserStorage
	sessionStorage storage.SessionStorage
}

//     
func NewServer(
	logger *log.Logger,
	webClient clients.Client,
	uiBuilder *ui.Builder,
	userStorage storage.UserStorage,
	sessionStorage storage.SessionStorage,
	middleware []Middleware,
) *Server {
	return &Server{
		logger:         logger,
		webClient:      webClient,
		uiBuilder:      uiBuilder,
		userStorage:    userStorage,
		sessionStorage: sessionStorage,
		middleware:     middleware,
	}
}

func (s *Server) setupRouter() http.Handler {
	router := http.NewServeMux()
	router.Handle(
		"/healthcheck",
		handlers.Healthcheck(),
	)
	router.Handle(
		"/play-poker",
		handlers.PlayPokerCommand(s.webClient, s.uiBuilder),
	)
	router.Handle(
		"/interactivity",
		handlers.InteractionCallback(s.userStorage, s.sessionStorage, s.uiBuilder, s.webClient),
	)

	return router
}

func (s *Server) setupMiddleware(router http.Handler) http.Handler {
	handler := router
	for _, middleware := range s.middleware {
		handler = middleware(handler)
	}

	return handler
}

func (s *Server) Serve(address string) {
	server := &http.Server{
		Addr:         address,
		Handler:      s.setupMiddleware(s.setupRouter()),
		ErrorLog:     s.logger,
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 10 * time.Second,
		IdleTimeout:  15 * time.Second,
	}

	done := make(chan bool)
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, os.Interrupt)

	go func() {
		<-quit
		s.logger.Println("Server is shutting down...")
		atomic.StoreInt32(&s.healthy, 0)

		ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
		defer cancel()

		server.SetKeepAlivesEnabled(false)
		if err := server.Shutdown(ctx); err != nil {
			s.logger.Fatalf("Could not gracefully shutdown the server: %v\n", err)
		}
		close(done)
	}()

	s.logger.Println("Server is ready to handle requests at", address)
	atomic.StoreInt32(&s.healthy, 1)
	if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
		s.logger.Fatalf("Could not listen on %s: %v\n", address, err)
	}

	<-done
	s.logger.Println("Server stopped")
}

      
      



main.go
package main

import (
	"fmt"
	"go-scrum-poker-bot/config"
	"go-scrum-poker-bot/storage"
	"go-scrum-poker-bot/ui"
	"go-scrum-poker-bot/web/clients"
	clients_middleware "go-scrum-poker-bot/web/clients/middleware"
	"go-scrum-poker-bot/web/server"
	server_middleware "go-scrum-poker-bot/web/server/middleware"
	"log"
	"net/http"
	"os"
	"time"

	"github.com/go-redis/redis/v8"
)

func main() {
	logger := log.New(os.Stdout, "INFO: ", log.LstdFlags)
	config := config.NewConfig()
  //  Redis 
	redisCLI := redis.NewClient(&redis.Options{
		Addr: fmt.Sprintf("%s:%d", config.Redis.Host, config.Redis.Port),
		DB:   config.Redis.DB,
	})
  //  users storage
	userStorage := storage.NewUserRedisStorage(redisCLI)
  //  sessions storage
	sessionStorage := storage.NewSessionRedisStorage(redisCLI)
	builder := ui.NewBuilder(config)
	webClient := clients.NewBasicClient(
		&http.Client{
			Timeout: 5 * time.Second,
		},
		[]clients.Middleware{
			clients_middleware.Auth(config.Slack.Token),
			clients_middleware.JsonContentType,
			clients_middleware.Log(logger),
		},
	)

  //  Server   middleware
	app := server.NewServer(
		logger,
		webClient,
		builder,
		userStorage,
		sessionStorage,
		[]server.Middleware{server_middleware.Recover(logger), server_middleware.Log(logger), server_middleware.Json},
	)
	app.Serve(config.App.ServerAddress)
}

      
      



:





go test ./... -race -coverpkg=./... -coverprofile=coverage.txt -covermode=atomic
      
      



:





go tool cover -func coverage.txt
$ go tool cover -func coverage.txt

go-scrum-poker-bot/config/config.go:9:                                  NewConfig               100.0%
go-scrum-poker-bot/config/helpers.go:10:                                getStrEnv               100.0%
go-scrum-poker-bot/config/helpers.go:17:                                getIntEnv               100.0%
go-scrum-poker-bot/config/helpers.go:26:                                getListStrEnv           100.0%
go-scrum-poker-bot/main.go:22:                                          main                    0.0%
go-scrum-poker-bot/storage/sessions.go:18:                              NewSessionRedisStorage  100.0%
go-scrum-poker-bot/storage/sessions.go:25:                              GetVisibility           100.0%
go-scrum-poker-bot/storage/sessions.go:33:                              SetVisibility           100.0%
go-scrum-poker-bot/storage/users.go:18:                                 NewUserRedisStorage     100.0%
go-scrum-poker-bot/storage/users.go:25:                                 All                     100.0%
go-scrum-poker-bot/storage/users.go:34:                                 Save                    100.0%
go-scrum-poker-bot/ui/blocks/action.go:9:                               BlockType               100.0%
go-scrum-poker-bot/ui/blocks/button.go:11:                              BlockType               100.0%
go-scrum-poker-bot/ui/blocks/context.go:9:                              BlockType               100.0%
go-scrum-poker-bot/ui/blocks/section.go:9:                              BlockType               100.0%
go-scrum-poker-bot/ui/blocks/select.go:10:                              BlockType               100.0%
go-scrum-poker-bot/ui/builder.go:14:                                    NewBuilder              100.0%
go-scrum-poker-bot/ui/builder.go:18:                                    getGetResultsText       100.0%
go-scrum-poker-bot/ui/builder.go:26:                                    getResults              100.0%
go-scrum-poker-bot/ui/builder.go:41:                                    getOptions              100.0%
go-scrum-poker-bot/ui/builder.go:50:                                    BuildBlocks             100.0%
go-scrum-poker-bot/ui/builder.go:100:                                   Build                   100.0%
go-scrum-poker-bot/web/clients/client.go:22:                            NewBasicClient          100.0%
go-scrum-poker-bot/web/clients/client.go:26:                            makeRequest             78.9%
go-scrum-poker-bot/web/clients/client.go:65:                            Make                    66.7%
go-scrum-poker-bot/web/clients/middleware/auth.go:8:                    Auth                    100.0%
go-scrum-poker-bot/web/clients/middleware/json.go:5:                    JsonContentType         100.0%
go-scrum-poker-bot/web/clients/middleware/log.go:8:                     Log                     87.5%
go-scrum-poker-bot/web/clients/request.go:12:                           ToBytes                 100.0%
go-scrum-poker-bot/web/clients/response.go:12:                          Json                    100.0%
go-scrum-poker-bot/web/server/handlers/healthcheck.go:10:               Healthcheck             66.7%
go-scrum-poker-bot/web/server/handlers/interaction_callback.go:12:      InteractionCallback     71.4%
go-scrum-poker-bot/web/server/handlers/play_poker.go:13:                PlayPokerCommand        100.0%
go-scrum-poker-bot/web/server/middleware/json.go:5:                     Json                    100.0%
go-scrum-poker-bot/web/server/middleware/log.go:8:                      Log                     100.0%
go-scrum-poker-bot/web/server/middleware/recover.go:9:                  Recover                 100.0%
go-scrum-poker-bot/web/server/models/callback.go:52:                    getSessionID            100.0%
go-scrum-poker-bot/web/server/models/callback.go:62:                    getSubject              100.0%
go-scrum-poker-bot/web/server/models/callback.go:72:                    getAction               100.0%
go-scrum-poker-bot/web/server/models/callback.go:82:                    SerializedData          92.3%
go-scrum-poker-bot/web/server/models/errors.go:13:                      ResponseError           75.0%
go-scrum-poker-bot/web/server/server.go:31:                             NewServer               0.0%
go-scrum-poker-bot/web/server/server.go:49:                             setupRouter             0.0%
go-scrum-poker-bot/web/server/server.go:67:                             setupMiddleware         0.0%
go-scrum-poker-bot/web/server/server.go:76:                             Serve                   0.0%
total:                                                                  (statements)            75.1%
      
      



No está mal, pero no necesitamos tener en cuenta la cobertura main.go



(mi opinión) y server.go



(aquí puedes argumentar), así que hay un truco :). Necesitamos agregar la siguiente línea con etiquetas al comienzo de los archivos que queremos excluir de la evaluación:





//+build !test
      
      



Reinicie con la etiqueta:





go test ./... -race -coverpkg=./... -coverprofile=coverage.txt -covermode=atomic -tags=test
      
      



Resultado:





go tool cover -funccover.txt
$ go tool cover -func coverage.txt

go-scrum-poker-bot/config/config.go:9:                                  NewConfig               100.0%
go-scrum-poker-bot/config/helpers.go:10:                                getStrEnv               100.0%
go-scrum-poker-bot/config/helpers.go:17:                                getIntEnv               100.0%
go-scrum-poker-bot/config/helpers.go:26:                                getListStrEnv           100.0%
go-scrum-poker-bot/storage/sessions.go:18:                              NewSessionRedisStorage  100.0%
go-scrum-poker-bot/storage/sessions.go:25:                              GetVisibility           100.0%
go-scrum-poker-bot/storage/sessions.go:33:                              SetVisibility           100.0%
go-scrum-poker-bot/storage/users.go:18:                                 NewUserRedisStorage     100.0%
go-scrum-poker-bot/storage/users.go:25:                                 All                     100.0%
go-scrum-poker-bot/storage/users.go:34:                                 Save                    100.0%
go-scrum-poker-bot/ui/blocks/action.go:9:                               BlockType               100.0%
go-scrum-poker-bot/ui/blocks/button.go:11:                              BlockType               100.0%
go-scrum-poker-bot/ui/blocks/context.go:9:                              BlockType               100.0%
go-scrum-poker-bot/ui/blocks/section.go:9:                              BlockType               100.0%
go-scrum-poker-bot/ui/blocks/select.go:10:                              BlockType               100.0%
go-scrum-poker-bot/ui/builder.go:14:                                    NewBuilder              100.0%
go-scrum-poker-bot/ui/builder.go:18:                                    getGetResultsText       100.0%
go-scrum-poker-bot/ui/builder.go:26:                                    getResults              100.0%
go-scrum-poker-bot/ui/builder.go:41:                                    getOptions              100.0%
go-scrum-poker-bot/ui/builder.go:50:                                    BuildBlocks             100.0%
go-scrum-poker-bot/ui/builder.go:100:                                   Build                   100.0%
go-scrum-poker-bot/web/clients/client.go:22:                            NewBasicClient          100.0%
go-scrum-poker-bot/web/clients/client.go:26:                            makeRequest             78.9%
go-scrum-poker-bot/web/clients/client.go:65:                            Make                    66.7%
go-scrum-poker-bot/web/clients/middleware/auth.go:8:                    Auth                    100.0%
go-scrum-poker-bot/web/clients/middleware/json.go:5:                    JsonContentType         100.0%
go-scrum-poker-bot/web/clients/middleware/log.go:8:                     Log                     87.5%
go-scrum-poker-bot/web/clients/request.go:12:                           ToBytes                 100.0%
go-scrum-poker-bot/web/clients/response.go:12:                          Json                    100.0%
go-scrum-poker-bot/web/server/handlers/healthcheck.go:10:               Healthcheck             66.7%
go-scrum-poker-bot/web/server/handlers/interaction_callback.go:12:      InteractionCallback     71.4%
go-scrum-poker-bot/web/server/handlers/play_poker.go:13:                PlayPokerCommand        100.0%
go-scrum-poker-bot/web/server/middleware/json.go:5:                     Json                    100.0%
go-scrum-poker-bot/web/server/middleware/log.go:8:                      Log                     100.0%
go-scrum-poker-bot/web/server/middleware/recover.go:9:                  Recover                 100.0%
go-scrum-poker-bot/web/server/models/callback.go:52:                    getSessionID            100.0%
go-scrum-poker-bot/web/server/models/callback.go:62:                    getSubject              100.0%
go-scrum-poker-bot/web/server/models/callback.go:72:                    getAction               100.0%
go-scrum-poker-bot/web/server/models/callback.go:82:                    SerializedData          92.3%
go-scrum-poker-bot/web/server/models/errors.go:13:                      ResponseError           75.0%
total:                                                                  (statements)            90.9%
      
      



Me gusta más este resultado :)





Me detendré aquí. Puedes encontrar todo el código aquí . ¡Gracias por la atención!





ACTUALIZACIÓN: Pensé que habría material para dos artículos, pero no funcionó, por lo que no habrá una segunda parte.








All Articles