diff --git a/Gopkg.lock b/Gopkg.lock index af0556f..fda9889 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -47,6 +47,12 @@ revision = "4f810fa5d8869fcfca0a0a7f6a834ee9cfa22095" version = "v1.13.40" +[[projects]] + name = "github.com/davecgh/go-spew" + packages = ["spew"] + revision = "346938d642f2ec3594ed81d874461961cd0faa76" + version = "v1.1.0" + [[projects]] name = "github.com/go-ini/ini" packages = ["."] @@ -58,6 +64,18 @@ packages = ["."] revision = "0b12d6b5" +[[projects]] + name = "github.com/pmezard/go-difflib" + packages = ["difflib"] + revision = "792786c7400a136282c1664665ae0a8db921c6c2" + version = "v1.0.0" + +[[projects]] + name = "github.com/stretchr/testify" + packages = ["assert"] + revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71" + version = "v1.2.1" + [[projects]] name = "github.com/technoweenie/multipartstreamer" packages = ["."] @@ -73,6 +91,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "1f75a9d94feea9e2fd56617a4557d60c29b91ce83218c2230da00d2013baf3f9" + inputs-digest = "25b86146913839b262563bd7cbd5d2ad48d4480c76b25f2d388869c94fc5518c" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Makefile b/Makefile index be45fbf..a9566bd 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,4 @@ build: dep ensure - GOOS=linux go build -o bin/telegram telegram/main.go + GOOS=linux go build -o bin/telegram_lambda cmd/telegram/lambda/main.go + GOOS=linux go build -o bin/telegram_http cmd/telegram/http/main.go \ No newline at end of file diff --git a/Plan.md b/Plan.md new file mode 100644 index 0000000..08d9a87 --- /dev/null +++ b/Plan.md @@ -0,0 +1,78 @@ +# WhosInBot Plan + + +## Package whosinbot + WhosInBot + DataStore DataStore + HandleCommand(command Command) (Response, Error) + +## Package whosinbot/domain + Command struct + name string + params string[] + + Response struct + message string + + RollCall struct + ChatID string + Title string + + RollCallResponse struct + ChatID int64 + UserID int64 + Response string + Reasons string
 + + DataStore interface + SetRollCall(RollCall) + DeleteRollCall(RollCall) + SetResponse(RollCallResponse) + DeleteResponse(RollCallResponse) + +## Package whosinbot/dynamodb + DynamoDataStore + SetRollCall(RollCall) + DeleteRollCall(RollCall) + SetResponse(RollCallResponse) + DeleteResponse(RollCallResponse) + + NewDynamoDataStore(DynamoConfig) + +## Package whoinbot/helpers + Helpers + +## Package telegram + NewTelegram(TelegramConfig) + Telegram + BotApi + ParseUpdateRequest(string) (Command, error) + SendMessage(Response) (error) + +## Package cmd/telegram_lambda
 + Main + LoadDynamoConfig + LoadTelegramConfig + +## Package cmd/telegram_http
 + Main + LoadDynamoConfig + LoadTelegramConfig + LoadHttpConfig + +##Package whosinbot/config + TelegramConfig + HttpConfig + DynamoConfig + + +## Commands +- start_roll_call (title) +- in (reason) +- out (reason) +- set_title (title) +- set_in_for (name) +- end_roll_call +- ssh + + diff --git a/README.md b/README.md index ba3f110..9bfb694 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ curl -XPOST https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/getWebhookInfo Install AWS CLI tools -``` +``` brew install awscli aws configure --profile col.w.harris ``` @@ -40,7 +40,7 @@ make build ``` serverless deploy ``` - + ## Links - https://github.com/go-telegram-bot-api/telegram-bot-api diff --git a/cmd/telegram/http/main.go b/cmd/telegram/http/main.go new file mode 100644 index 0000000..f48ff22 --- /dev/null +++ b/cmd/telegram/http/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "github.com/col/whosinbot/dynamodb" + "net/http" + "fmt" + "github.com/col/whosinbot/whosinbot" + whttp "github.com/col/whosinbot/http" +) + +func main() { + dataStore := &dynamodb.DynamoDataStore{} + bot := &whosinbot.WhosInBot{ DataStore: dataStore } + + http.Handle("/webhook", &whttp.WebhookHandler{WhosInBot: bot}) + port := 8080 + serverConfig := fmt.Sprintf(":%d", port) + http.ListenAndServe(serverConfig, nil) +} + diff --git a/cmd/telegram/lambda/main.go b/cmd/telegram/lambda/main.go new file mode 100644 index 0000000..b3ab881 --- /dev/null +++ b/cmd/telegram/lambda/main.go @@ -0,0 +1,40 @@ +package main + +import ( + "github.com/aws/aws-lambda-go/lambda" + "github.com/aws/aws-lambda-go/events" + "github.com/col/whosinbot/dynamodb" + "context" + "github.com/col/whosinbot/telegram" + "github.com/col/whosinbot/whosinbot" +) + +func main() { + lambda.Start(Handler) +} + +func Handler(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { + + // Parse Command + command, err := telegram.ParseUpdate([]byte(request.Body)) + if err != nil { + return events.APIGatewayProxyResponse{StatusCode: 400}, err + } + + // Process Command + dataStore := &dynamodb.DynamoDataStore{} + bot := whosinbot.WhosInBot{ DataStore: dataStore } + response, err := bot.HandleCommand(command) + if err != nil { + return events.APIGatewayProxyResponse{StatusCode: 400}, err + } + + // Send Response + api := telegram.NewTelegram(request.PathParameters["token"]) + err = api.SendResponse(response) + if err != nil { + return events.APIGatewayProxyResponse{StatusCode: 400}, err + } + + return events.APIGatewayProxyResponse{StatusCode: 200}, nil +} \ No newline at end of file diff --git a/core/message_handler.go b/core/message_handler.go deleted file mode 100644 index e3118ac..0000000 --- a/core/message_handler.go +++ /dev/null @@ -1,81 +0,0 @@ -package core - -import ( - "errors" - "gopkg.in/telegram-bot-api.v4" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/dynamodb" - "fmt" - "os" - "strconv" -) - -func HandleMessage(message *tgbotapi.Message) (string, error) { - switch command := message.Command(); command { - case "start_roll_call": - return handleStart(message) - case "end_roll_call": - return handleEnd(message) - case "in": - return handleIn(message) - case "out": - return handleOut(message) - default: - return "", errors.New("Not a bot command") - } -} - -func handleStart(message *tgbotapi.Message) (string, error) { - // TODO: Create new roll call - - sess, _ := session.NewSession(&aws.Config{ - Region: aws.String("ap-southeast-1")}, - ) - - svc := dynamodb.New(sess) - - input := &dynamodb.UpdateItemInput{ - ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{ - ":title": { - S: aws.String("Testing"), - }, - }, - TableName: aws.String(os.Getenv("ROLLCALL_TABLE")), - Key: map[string]*dynamodb.AttributeValue{ - "chat_id": { - N: aws.String(strconv.Itoa(int(message.Chat.ID))), - }, - }, - ReturnValues: aws.String("UPDATED_NEW"), - UpdateExpression: aws.String("set title = :title"), - } - - _, err := svc.UpdateItem(input) - - if err != nil { - fmt.Println(err.Error()) - } - - return "Roll call started", nil -} - -func handleEnd(message *tgbotapi.Message) (string, error) { - // TODO: Delete roll call - return "Roll call ended", nil -} - -func handleIn(message *tgbotapi.Message) (string, error) { - // TODO: mark sender as in - return whosIn(message) -} - -func handleOut(message *tgbotapi.Message) (string, error) { - // TODO: mark sender as out - return whosIn(message) -} - -func whosIn(message *tgbotapi.Message) (string, error) { - // TODO: output the roll call - return "I don't know yet", nil -} diff --git a/domain/types.go b/domain/types.go new file mode 100644 index 0000000..2372533 --- /dev/null +++ b/domain/types.go @@ -0,0 +1,58 @@ +package domain + +import "strings" + +type Command struct { + ChatID int64 + Name string + Params []string + From User +} + +func (c Command) ParamsString() string { + return strings.Join(c.Params, " ") +} + +type User struct { + UserID int64 + Username string +} + +func EmptyCommand() Command { + return Command{ + ChatID: 0, + Name: "", + Params: nil, + From: User{}, + } +} + +type Response struct { + ChatID int64 + Text string +} + +type DataStore interface { + StartRollCall(rollCall RollCall) error + EndRollCall(rollCall RollCall) error + + SetResponse(rollCallResponse RollCallResponse) error + + GetRollCall(chatID int64) (*RollCall, error) +} + +type RollCall struct { + ChatID int64 + Title string + In []RollCallResponse + Out []RollCallResponse + Maybe []RollCallResponse +} + +type RollCallResponse struct { + ChatID int64 + UserID int64 + Name string + Response string + Reason string +} diff --git a/dynamodb/data_store.go b/dynamodb/data_store.go new file mode 100644 index 0000000..ca80a94 --- /dev/null +++ b/dynamodb/data_store.go @@ -0,0 +1,63 @@ +package dynamodb + +import ( + "github.com/col/whosinbot/domain" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/dynamodb" + "os" + "strconv" + "fmt" +) + +type DynamoDataStore struct { + +} + +func (d DynamoDataStore) GetRollCall(chatID int64) (*domain.RollCall, error) { + // TODO: ... + return nil, nil +} + +func (d DynamoDataStore) SetResponse(rollCallResponse domain.RollCallResponse) (error) { + // TODO: ... + return nil +} + +func (d DynamoDataStore) EndRollCall(rollCall domain.RollCall) error { + // TODO: ... + return nil +} + +func (d DynamoDataStore) StartRollCall(rollCall domain.RollCall) (error) { + sess, _ := session.NewSession(&aws.Config{ + Region: aws.String("ap-southeast-1")}, + ) + + svc := dynamodb.New(sess) + + input := &dynamodb.UpdateItemInput{ + ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{ + ":title": { + S: aws.String(rollCall.Title), + }, + }, + TableName: aws.String(os.Getenv("ROLLCALL_TABLE")), + Key: map[string]*dynamodb.AttributeValue{ + "chat_id": { + N: aws.String(strconv.Itoa(int(rollCall.ChatID))), + }, + }, + ReturnValues: aws.String("UPDATED_NEW"), + UpdateExpression: aws.String("set title = :title"), + } + + _, err := svc.UpdateItem(input) + + if err != nil { + fmt.Println(err.Error()) + return err + } + + return nil +} diff --git a/http/webhook_handler.go b/http/webhook_handler.go new file mode 100644 index 0000000..d957abb --- /dev/null +++ b/http/webhook_handler.go @@ -0,0 +1,42 @@ +package http + +import ( + "net/http" + "io/ioutil" + "github.com/col/whosinbot/whosinbot" + "github.com/col/whosinbot/telegram" + "os" +) + +type WebhookHandler struct { + WhosInBot *whosinbot.WhosInBot +} + +func (h *WebhookHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, "can't read body", http.StatusBadRequest) + return + } + + command, err := telegram.ParseUpdate(body) + if err != nil { + http.Error(w, "can't parse body", http.StatusBadRequest) + return + } + + response, err := h.WhosInBot.HandleCommand(command) + if err != nil { + http.Error(w, "can't parse body", http.StatusBadRequest) + return + } + + api := telegram.NewTelegram(os.Getenv("TELEGRAM_BOT_TOKEN")) + err = api.SendResponse(response) + if err != nil { + http.Error(w, "failed to send response", http.StatusBadRequest) + return + } + + w.WriteHeader(http.StatusOK) +} \ No newline at end of file diff --git a/main.go b/main.go new file mode 100644 index 0000000..c9ecbf5 --- /dev/null +++ b/main.go @@ -0,0 +1,2 @@ +package main + diff --git a/serverless.yml b/serverless.yml index a0665a5..9cf881b 100644 --- a/serverless.yml +++ b/serverless.yml @@ -48,7 +48,7 @@ package: functions: telegram: - handler: bin/telegram + handler: bin/telegram_lambda environment: TELEGRAM_BOT_TOKEN: ${env:TELEGRAM_BOT_TOKEN} events: diff --git a/telegram/helpers.go b/telegram/helpers.go new file mode 100644 index 0000000..9ff0579 --- /dev/null +++ b/telegram/helpers.go @@ -0,0 +1,22 @@ +package telegram + +import ( + "github.com/col/whosinbot/domain" + "gopkg.in/telegram-bot-api.v4" + "encoding/json" + "strings" +) + +func ParseUpdate(requestBody []byte) (domain.Command, error) { + update := &tgbotapi.Update{} + err := json.Unmarshal(requestBody, update) + if err != nil { + return domain.EmptyCommand(), err + } + command := domain.Command{ + ChatID: update.Message.Chat.ID, + Name: update.Message.Command(), + Params: strings.Fields(update.Message.CommandArguments()), + } + return command, err +} \ No newline at end of file diff --git a/telegram/main.go b/telegram/main.go deleted file mode 100644 index b370d4e..0000000 --- a/telegram/main.go +++ /dev/null @@ -1,67 +0,0 @@ -package main - -import ( - "os" - "fmt" - "errors" - "context" - "github.com/aws/aws-lambda-go/events" - "github.com/aws/aws-lambda-go/lambda" - "gopkg.in/telegram-bot-api.v4" - "encoding/json" - "github.com/col/whosinbot/core" -) - -func ValidateToken(requestToken string) (string, error) { - token := os.Getenv("TELEGRAM_BOT_TOKEN") - if token != requestToken { - message := fmt.Sprintf("ERROR: Bot token doesn't match! Expected: %v Received: %v", token, requestToken) - return "", errors.New(message) - } - return token, nil -} - -func ParseRequest(requestBody string) (*tgbotapi.Update, error) { - update := &tgbotapi.Update{} - err := json.Unmarshal([]byte(requestBody), update) - return update, err -} - -func HandleUpdate(update *tgbotapi.Update, bot *tgbotapi.BotAPI) { - response, _ := core.HandleMessage(update.Message) - msg := tgbotapi.NewMessage(update.Message.Chat.ID, response) - bot.Send(msg) -} - -func response(err error) (events.APIGatewayProxyResponse, error) { - if err != nil { - return events.APIGatewayProxyResponse{StatusCode: 400}, err - } else { - return events.APIGatewayProxyResponse{StatusCode: 200}, nil - } -} - -func Handler(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { - token, err := ValidateToken(request.PathParameters["token"]) - if err != nil { - return response(err) - } - - bot, err := tgbotapi.NewBotAPI(token) - if err != nil { - return response(err) - } - - update, err := ParseRequest(request.Body) - if err != nil { - return response(err) - } - - HandleUpdate(update, bot) - - return response(nil) -} - -func main() { - lambda.Start(Handler) -} diff --git a/telegram/telegram.go b/telegram/telegram.go new file mode 100644 index 0000000..8425673 --- /dev/null +++ b/telegram/telegram.go @@ -0,0 +1,49 @@ +package telegram + +import ( + "github.com/col/whosinbot/domain" + "gopkg.in/telegram-bot-api.v4" + "os" + "fmt" + "errors" +) + +type Telegram struct { + Token string +} + +func NewTelegram(token string) (*Telegram) { + return &Telegram{ Token: token } +} + +func (t *Telegram) SendResponse(response *domain.Response) (error) { + if response == nil { + return nil + } + + err := validateToken(t.Token) + if err != nil { + return err + } + + bot, err := tgbotapi.NewBotAPI(t.Token) + if err != nil { + return err + } + + _, err = bot.Send(tgbotapi.NewMessage(response.ChatID, response.Text)) + if err != nil { + return err + } + + return nil +} + +func validateToken(token string) (error) { + validToken := os.Getenv("TELEGRAM_BOT_TOKEN") + if token != validToken { + message := fmt.Sprintf("ERROR: Bot token doesn't match! Expected: %v Received: %v", validToken, token) + return errors.New(message) + } + return nil +} \ No newline at end of file diff --git a/whosinbot/whos_in_bot.go b/whosinbot/whos_in_bot.go new file mode 100644 index 0000000..56373fe --- /dev/null +++ b/whosinbot/whos_in_bot.go @@ -0,0 +1,141 @@ +package whosinbot + +import ( + "log" + "errors" + "github.com/col/whosinbot/domain" + "strings" + "fmt" +) + +type WhosInBot struct { + DataStore domain.DataStore +} + +func (b *WhosInBot) HandleCommand(command domain.Command) (*domain.Response, error) { + log.Printf("Command: %v", command.Name) + switch command.Name { + case "start_roll_call": + return b.handleStart(command) + case "end_roll_call": + return b.handleEnd(command) + case "in": + return b.handleResponse(command, "in") + case "out": + return b.handleResponse(command, "out") + case "maybe": + return b.handleResponse(command, "maybe") + case "whos_in": + return b.handleWhosIn(command) + default: + return nil, errors.New("Not a bot command") + } +} + +func (b *WhosInBot) handleStart(command domain.Command) (*domain.Response, error) { + roll_call := domain.RollCall{ + ChatID: command.ChatID, + Title: strings.Join(command.Params, " "), + } + err := b.DataStore.StartRollCall(roll_call) + if err != nil { + return nil, err + } + return &domain.Response{ChatID: command.ChatID, Text: "Roll call started"}, nil +} + +func (b *WhosInBot) handleEnd(command domain.Command) (*domain.Response, error) { + rollCall, err := b.DataStore.GetRollCall(command.ChatID) + if err != nil { + return nil, err + } + if rollCall == nil { + return &domain.Response{Text: "No roll call in progress", ChatID: command.ChatID}, nil + } + err = b.DataStore.EndRollCall(*rollCall) + if err != nil { + return nil, err + } + return &domain.Response{ChatID: command.ChatID, Text: "Roll call ended"}, nil +} + +func (b *WhosInBot) handleWhosIn(command domain.Command) (*domain.Response, error) { + rollCall, err := b.DataStore.GetRollCall(command.ChatID) + if err != nil { + return nil, err + } + if rollCall == nil { + return &domain.Response{Text: "No roll call in progress", ChatID: command.ChatID}, nil + } + return &domain.Response{ChatID: command.ChatID, Text: responsesList(rollCall)}, nil +} + +func (b *WhosInBot) handleResponse(command domain.Command, status string) (*domain.Response, error) { + rollCall, err := b.DataStore.GetRollCall(command.ChatID) + if err != nil { + return nil, err + } + if rollCall == nil { + return &domain.Response{Text: "No roll call in progress", ChatID: command.ChatID}, nil + } + + rollCallResponse := domain.RollCallResponse{ + ChatID: command.ChatID, + UserID: command.From.UserID, + Name: command.From.Username, + Response: status, + Reason: command.ParamsString(), + } + b.DataStore.SetResponse(rollCallResponse) + + return &domain.Response{ChatID: command.ChatID, Text: responsesList(rollCall)}, nil +} + +func (b *WhosInBot) handleOut(command domain.Command) (*domain.Response, error) { + return &domain.Response{}, nil +} + +func responsesList(rollCall *domain.RollCall) (string) { + var text = "" + + if len(rollCall.Title) > 0 { + text += rollCall.Title + } + + if len(rollCall.In) > 0 && len(text) > 0 { + text += "\n" + } + for index, response := range rollCall.In { + text += fmt.Sprintf("%d. %v", index+1, response.Name) + if len(response.Reason) > 0 { + text += fmt.Sprintf(" (%v)", response.Reason) + } + if index + 1 < len(rollCall.In) { + text += "\n" + } + } + + text = appendResponses(text, rollCall.Out, "Out") + text = appendResponses(text, rollCall.Maybe, "Maybe") + + return text +} + +func appendResponses(text string, responses []domain.RollCallResponse, status string) (string) { + if len(responses) > 0 { + if len(text) > 0 { + text += "\n\n" + } + text += fmt.Sprintf("%v\n", status) + } + for index, response := range responses { + text += fmt.Sprintf(" - %v", response.Name) + if len(response.Reason) > 0 { + text += fmt.Sprintf(" (%v)", response.Reason) + } + if index + 1 < len(responses) { + text += "\n" + } + } + return text +} \ No newline at end of file diff --git a/whosinbot/whos_in_bot_test.go b/whosinbot/whos_in_bot_test.go new file mode 100644 index 0000000..735b962 --- /dev/null +++ b/whosinbot/whos_in_bot_test.go @@ -0,0 +1,200 @@ +package whosinbot + +import ( + "github.com/col/whosinbot/domain" + "github.com/stretchr/testify/assert" + "testing" +) + +type MockDataStore struct { + startRollCallCalled bool + startRollCallWith *domain.RollCall + + endRollCallCalled bool + endRollCallWith *domain.RollCall + + setResponseCalled bool + setResponseWith *domain.RollCallResponse + + rollCall *domain.RollCall + rollCallResponses []domain.RollCallResponse +} + +func (d *MockDataStore) StartRollCall(rollCall domain.RollCall) error { + d.startRollCallCalled = true + d.startRollCallWith = &rollCall + return nil +} + +func (d *MockDataStore) EndRollCall(rollCall domain.RollCall) error { + d.endRollCallCalled = true + d.endRollCallWith = &rollCall + return nil +} + +func (d *MockDataStore) SetResponse(rollCallResponse domain.RollCallResponse) error { + d.setResponseCalled = true + d.setResponseWith = &rollCallResponse + switch rollCallResponse.Response { + case "in": + d.rollCall.In = append(d.rollCall.In, rollCallResponse) + case "out": + d.rollCall.Out = append(d.rollCall.Out, rollCallResponse) + case "maybe": + d.rollCall.Maybe = append(d.rollCall.Maybe, rollCallResponse) + } + return nil +} + +func (d *MockDataStore) GetRollCall(chatID int64) (*domain.RollCall, error) { + return d.rollCall, nil +} + +func (d *MockDataStore) GetRollCallResponses(chatID int64) ([]domain.RollCallResponse, error) { + return d.rollCallResponses, nil +} + +var mockDataStore *MockDataStore +var bot *WhosInBot + +func setUp() { + mockDataStore = &MockDataStore{ + startRollCallCalled: false, + startRollCallWith: nil, + endRollCallCalled: false, + endRollCallWith: nil, + setResponseCalled: false, + setResponseWith: nil, + rollCall: nil, + rollCallResponses: nil, + } + bot = &WhosInBot{DataStore: mockDataStore} +} + +func TestStartRollCall(t *testing.T) { + setUp() + command := domain.Command{ChatID: 123, Name: "start_roll_call", Params: []string{"sample title"}} + response, err := bot.HandleCommand(command) + // Validate data store + assert.True(t, mockDataStore.startRollCallCalled) + assert.NotNil(t, mockDataStore.startRollCallWith) + assert.Equal(t, int64(123), mockDataStore.startRollCallWith.ChatID) + assert.Equal(t, "sample title", mockDataStore.startRollCallWith.Title) + // Validate response + assertBotResponse(t, response, err, 123, "Roll call started", nil) +} + +func TestEndRollCallWhenRollCallExists(t *testing.T) { + setUp() + mockDataStore.rollCall = &domain.RollCall{ChatID: 123, Title: ""} + command := domain.Command{ChatID: 123, Name: "end_roll_call", Params: []string{}} + response, err := bot.HandleCommand(command) + // Validate data store + assert.True(t, mockDataStore.endRollCallCalled) + assert.NotNil(t, mockDataStore.endRollCallWith) + assert.Equal(t, int64(123), mockDataStore.endRollCallWith.ChatID) + // Validate response + assertBotResponse(t, response, err, 123, "Roll call ended", nil) +} + +func TestEndRollCallWhenRollCallDoesNotExists(t *testing.T) { + setUp() + mockDataStore.rollCall = nil + command := domain.Command{ChatID: 123, Name: "end_roll_call", Params: []string{}} + response, err := bot.HandleCommand(command) + // Validate data store + assert.False(t, mockDataStore.endRollCallCalled) + // Validate response + assertBotResponse(t, response, err, 123, "No roll call in progress", nil) +} + +func TestInWhenNoRollCallInProgress(t *testing.T) { + setUp() + response, err := bot.HandleCommand(responseCommand("in")) + assertBotResponse(t, response, err, 123, "No roll call in progress", nil) +} + +func TestInWhenRollCallIsInProgress(t *testing.T) { + setUp() + mockDataStore.rollCall = &domain.RollCall{ChatID: 123, Title: ""} + + response, err := bot.HandleCommand(responseCommand("in")) + + assertResponsePersisted(t, 123, "in", "JohnSmith") + assertBotResponse(t, response, err, 123, "1. JohnSmith (sample reason)", nil) +} + +func TestOutWhenNoRollCallInProgress(t *testing.T) { + setUp() + response, err := bot.HandleCommand(responseCommand("out")) + assert.False(t, mockDataStore.setResponseCalled) + assertBotResponse(t, response, err, 123, "No roll call in progress", nil) +} + +func TestOutWhenRollCallIsInProgress(t *testing.T) { + setUp() + mockDataStore.rollCall = &domain.RollCall{ChatID: 123, Title: ""} + response, err := bot.HandleCommand(responseCommand("out")) + assertResponsePersisted(t, 123, "out", "JohnSmith") + assertBotResponse(t, response, err, 123, "Out\n - JohnSmith (sample reason)", nil) +} + +func TestMaybeWhenNoRollCallInProgress(t *testing.T) { + setUp() + response, err := bot.HandleCommand(responseCommand("maybe")) + assert.False(t, mockDataStore.setResponseCalled) + assertBotResponse(t, response, err, 123, "No roll call in progress", nil) +} + +func TestMaybeWhenRollCallIsInProgress(t *testing.T) { + setUp() + mockDataStore.rollCall = &domain.RollCall{ChatID: 123, Title: ""} + response, err := bot.HandleCommand(responseCommand("maybe")) + assertResponsePersisted(t, 123, "maybe", "JohnSmith") + assertBotResponse(t, response, err, 123, "Maybe\n - JohnSmith (sample reason)", nil) +} + +func TestWhosIn(t *testing.T) { + setUp() + mockDataStore.rollCall = &domain.RollCall{ + ChatID: 123, + Title: "Test Title", + In: []domain.RollCallResponse{ + {ChatID: 123, UserID: 1, Name: "User 1", Response: "in", Reason: ""}, + }, + Out: []domain.RollCallResponse{ + {ChatID: 123, UserID: 1, Name: "User 2", Response: "out", Reason: ""}, + }, + Maybe: []domain.RollCallResponse{ + {ChatID: 123, UserID: 1, Name: "User 3", Response: "maybe", Reason: ""}, + }, + } + response, err := bot.HandleCommand(responseCommand("whos_in")) + assertBotResponse(t, response, err, 123, "Test Title\n1. User 1\n\nOut\n - User 2\n\nMaybe\n - User 3", nil) +} + +// Test Helpers +func responseCommand(status string) domain.Command { + return domain.Command{ + ChatID: 123, + Name: status, + Params: []string{"sample reason"}, + From: domain.User{UserID: 456, Username: "JohnSmith"}, + } +} + +func assertResponsePersisted(t *testing.T, chatID int, status string, name string) { + assert.True(t, mockDataStore.setResponseCalled, "should call setResponse") + assert.NotNil(t, mockDataStore.setResponseWith) + if mockDataStore.setResponseWith != nil { + assert.Equal(t, int64(chatID), mockDataStore.setResponseWith.ChatID) + assert.Equal(t, status, mockDataStore.setResponseWith.Response) + assert.Equal(t, name, mockDataStore.setResponseWith.Name) + } +} + +func assertBotResponse(t *testing.T, response *domain.Response, err error, chatID int, text string, error error) { + assert.Equal(t, int64(chatID), response.ChatID) + assert.Equal(t, text, response.Text) + assert.Equal(t, error, err) +}