From 5bd5bd248682fcc54832fe36fdf416fd3bfa98d5 Mon Sep 17 00:00:00 2001 From: Christopher Purta Date: Thu, 21 Nov 2019 13:01:48 -0800 Subject: [PATCH] Add mocks and unit tests to http and msgraph packages (#2) * command + http framework * Got a 404 on the hardcoded Mattermost Org - no Outlook * `/msoffice viewcal` works on a personal account - added OAuth2 credentials a config values * PR feedback * Add mocks and unit tests to http and msgraph packages Added a mocks package that mocks out the functionality of some the interfaces needed by the oauth handlers. This allows testing of each error check within the oauth handler functions. I also added github.com/jarcoal/httpmock as a dependecy to test both the oauth code to token exchange and each of the calls that will be made out to the microsoft graph API. The tests cover how all errors are encountered for the oauth connection. They also ensure that access token refreshing happens when the token is expired. This tests to make sure the golang oauth client works out of the box for this plugin. * Update http.go and plugin.go with upstream/dev branch * Address PR review comments This includes moving all of the tests to their own subpackages to allow for a cleaner workspace. Move all the mock interfaces to the test_http package as they are only needed there currently. Update mock interfaces to use github.com/stretchr/testify/mock to conform to Mattermost mock standards. Also addresses any naming convention comments in this commit as well. * Migrate mocks to use gomock Per @cpoile we are migrating the mocked interfaces from stretchr/testify/mock to instead be generated and used via gomock. I have added a new makefile target to generate the mock interfaces using mockgen. In addition I have generated those mocks to be used in the unit tests in the test_http package. All of those unit tests have been updated to use the generated mocks as well. * Use getUserRequest for oauth connect tests * Refactor how mocks are built for tests * Refactor test case mocks, fix minor nitpicks Refactored the mocks to be a part of the testcase struct and then pass those to a `setupMocks` function which allow for custom mock behavior. This sigificantly reduces the code for setup and allows for more customizable mocks. Also addressing some of @levb comments on package names for the test folders and naming convention. * Create mocks in test run function and pass to tc struct after setup * Move redundant test case fields to testing block --- Makefile | 9 + go.mod | 2 + go.sum | 33 ++ server/http/http.go | 5 +- server/http/oauth2_complete.go | 5 +- server/http/oauth2_connect.go | 6 +- server/http/testhttp/mock_response_writer.go | 41 +++ server/http/testhttp/oauth2_complete_test.go | 281 ++++++++++++++++++ server/http/testhttp/oauth2_connect_test.go | 79 +++++ server/kvstore/mock_kvstore/mock_kvstore.go | 77 +++++ server/msgraph/testmsgraph/client_test.go | 46 +++ server/msgraph/testmsgraph/get_me_test.go | 123 ++++++++ .../testmsgraph/get_user_calendar_test.go | 106 +++++++ server/plugin/plugin.go | 1 + server/user/mock_user/mock_oauth2_store.go | 62 ++++ server/utils/mock_utils/mock_bot_poster.go | 60 ++++ 16 files changed, 927 insertions(+), 9 deletions(-) create mode 100644 server/http/testhttp/mock_response_writer.go create mode 100644 server/http/testhttp/oauth2_complete_test.go create mode 100644 server/http/testhttp/oauth2_connect_test.go create mode 100644 server/kvstore/mock_kvstore/mock_kvstore.go create mode 100644 server/msgraph/testmsgraph/client_test.go create mode 100644 server/msgraph/testmsgraph/get_me_test.go create mode 100644 server/msgraph/testmsgraph/get_user_calendar_test.go create mode 100644 server/user/mock_user/mock_oauth2_store.go create mode 100644 server/utils/mock_utils/mock_bot_poster.go diff --git a/Makefile b/Makefile index 8ae654cc..ec518e4e 100644 --- a/Makefile +++ b/Makefile @@ -79,6 +79,15 @@ golint: $(GOPATH)/bin/golint -set_exit_status ./... @echo lint success +## Generates mock golang interfaces for testing +mock: +ifneq ($(HAS_SERVER),) + go install github.com/golang/mock/mockgen + mockgen -destination server/user/mock_user/mock_oauth2_store.go github.com/mattermost/mattermost-plugin-msoffice/server/user OAuth2StateStore + mockgen -destination server/utils/mock_utils/mock_bot_poster.go github.com/mattermost/mattermost-plugin-msoffice/server/utils BotPoster + mockgen -destination server/kvstore/mock_kvstore/mock_kvstore.go github.com/mattermost/mattermost-plugin-msoffice/server/kvstore KVStore +endif + ## Builds the server, if it exists, including support for multiple architectures. .PHONY: server server: diff --git a/go.mod b/go.mod index 0deecbb9..5f27b042 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,9 @@ module github.com/mattermost/mattermost-plugin-msoffice go 1.13 require ( + github.com/golang/mock v1.2.0 github.com/gorilla/mux v1.7.3 + github.com/jarcoal/httpmock v1.0.4 github.com/jkrecek/msgraph-go v0.0.0-20190328140430-9f7466d8cb1f github.com/mattermost/mattermost-server v0.0.0-20190927121038-340287890a78 github.com/pkg/errors v0.8.1 diff --git a/go.sum b/go.sum index 220f25e3..2996b5de 100644 --- a/go.sum +++ b/go.sum @@ -29,6 +29,7 @@ github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878/go.mod h1:3AMJUQhVx52RsWOnlkpikZr01T/yAVN2gn0861vByNg= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/avct/uasurfer v0.0.0-20190308134847-43c6f9a90eeb h1:Y3aZuokc2y31S/oh04f5893WST38EefizkMaP4qS6DM= github.com/avct/uasurfer v0.0.0-20190308134847-43c6f9a90eeb/go.mod h1:noBAuukeYOXa0aXGqxr24tADqkwDO2KRD15FsuaZ5a8= github.com/aws/aws-sdk-go v1.19.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -54,8 +55,10 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3 h1:AqeKSZIG/NIC75MNQlPy/LM3LxfpLwahICJBHwSMFNc= github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3/go.mod h1:hEfFauPHz7+NnjR/yHJGhrKo1Za+zStgwUETx3yzqgY= github.com/die-net/lrucache v0.0.0-20181227122439-19a39ef22a11/go.mod h1:ew0MSjCVDdtGMjF3kzLK9hwdgF5mOE8SbYVF3Rc7mkU= +github.com/disintegration/imaging v1.6.0 h1:nVPXRUUQ36Z7MNf0O77UzgnOb1mkMMor7lmJMJXc/mA= github.com/disintegration/imaging v1.6.0/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= @@ -87,11 +90,13 @@ github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= @@ -127,6 +132,7 @@ github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= github.com/grpc-ecosystem/grpc-gateway v1.6.2/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= @@ -150,7 +156,9 @@ github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjG github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/memberlist v0.1.4/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= @@ -161,12 +169,15 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO github.com/icrowley/fake v0.0.0-20180203215853-4178557ae428/go.mod h1:uhpZMVGznybq1itEKXj6RYw9I71qK4kH+OGMjRC4KEo= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jamiealquiza/envy v1.1.0/go.mod h1:MP36BriGCLwEHhi1OU8E9569JNZrjWfCvzG7RsPnHus= +github.com/jarcoal/httpmock v1.0.4 h1:jp+dy/+nonJE4g4xbVtl9QdrUNbn6/3hDT5R4nDIZnA= +github.com/jarcoal/httpmock v1.0.4/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43 h1:jTkyeF7NZ5oIr0ESmcrpiDgAfoidCBF4F5kJhjtaRwE= github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= github.com/jkrecek/msgraph-go v0.0.0-20190328140430-9f7466d8cb1f h1:uqEdY/CRO5gpKubf8vgCIUOL9TLy0dWBKDtjlYddkvo= github.com/jkrecek/msgraph-go v0.0.0-20190328140430-9f7466d8cb1f/go.mod h1:RBokY5v/eWZg8XHsAUPtyqoCA646rL/NV8Br2XISja0= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= @@ -190,6 +201,7 @@ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.0.0-20180730094502-03f2033d19d5/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -202,7 +214,9 @@ github.com/mattermost/ldap v3.0.4+incompatible h1:SOeNnz+JNR+foQ3yHkYqijb9MLPhXN github.com/mattermost/ldap v3.0.4+incompatible/go.mod h1:b4reDCcGpBxJ4WX0f224KFY+OR0npin7or7EFpeIko4= github.com/mattermost/mattermost-server v0.0.0-20190927121038-340287890a78 h1:2O+HJDCLWxCQMsoUSpuJdN5L/zF7Ng8piqoe5PB0JAY= github.com/mattermost/mattermost-server v0.0.0-20190927121038-340287890a78/go.mod h1:M8AZ113Nuu+XvJMPOIzNx55xH1zGVltmcP3A3FAHHtw= +github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0 h1:G9tL6JXRBMzjuD1kkBtcnd42kUiT6QDwxfFYu7adM6o= github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0/go.mod h1:nV5bfVpT//+B1RPD2JvRnxbkLmJEYXmRaaVl15fsXjs= +github.com/mattermost/viper v1.0.4 h1:cMYOz4PhguscGSPxrSokUtib5HrG4gCpiUh27wyA3d0= github.com/mattermost/viper v1.0.4/go.mod h1:uc5hKG9lv4/KRwPOt2c1omOyirS/UnuA2TytiZQSFHM= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= @@ -215,19 +229,24 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.15/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/minio/cli v1.20.0/go.mod h1:bYxnK0uS629N3Bq+AOZZ+6lwF77Sodk4+UL9vNuXhOY= +github.com/minio/minio-go/v6 v6.0.34 h1:ESPDlIg8Pe2BRvsxPomd0xB72uLmsXrkDNoze36yb90= github.com/minio/minio-go/v6 v6.0.34/go.mod h1:vaNT59cWULS37E+E9zkuN/BVnKHyXtVGS+b04Boc66Y= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/muesli/smartcrop v0.2.1-0.20181030220600-548bbf0c0965/go.mod h1:i2fCI/UorTfgEpPPLWiFBv4pye+YAG78RwcQLUkocpI= +github.com/muesli/smartcrop v0.3.0 h1:JTlSkmxWg/oQ1TcLDoypuirdE8Y/jzNirQeLkxpA6Oc= github.com/muesli/smartcrop v0.3.0/go.mod h1:i2fCI/UorTfgEpPPLWiFBv4pye+YAG78RwcQLUkocpI= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/ngdinhtoan/glide-cleanup v0.2.0/go.mod h1:UQzsmiDOb8YV3nOsCxK/c9zPpCZVNoHScRE3EO9pVMM= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= @@ -280,14 +299,18 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rs/cors v1.6.0 h1:G9tHG9lebljV9mfp9SNPDL36nCDxmo3zTlAf1YgvzmI= github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/rwcarlsen/goexif v0.0.0-20190318171057-76e3344f7516/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= +github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc= github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/satori/go.uuid v0.0.0-20180103174451-36e9d2ebbde5/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/segmentio/analytics-go v3.0.1+incompatible h1:W7T3ieNQjPFMb+SE8SAVYo6mPkKK/Y37wYdiNf5lCVg= github.com/segmentio/analytics-go v3.0.1+incompatible/go.mod h1:C7CYBtQWk4vRk2RyLu0qOcbHJ18E3F1HV2C/8JvKN48= +github.com/segmentio/backo-go v0.0.0-20160424052352-204274ad699c h1:rsRTAcCR5CeNLkvgBVSjQoDGRRt6kggsE6XYBqCv2KQ= github.com/segmentio/backo-go v0.0.0-20160424052352-204274ad699c/go.mod h1:kJ9mm9YmoWSkk+oQ+5Cj8DEoRCX2JT6As4kEtIIOp1M= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= @@ -297,10 +320,13 @@ github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v0.0.0-20190710185942-9d28bd7c0945/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -318,11 +344,13 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/throttled/throttled v2.2.4+incompatible h1:aVKdoH/qT5Mo1Lm/678OkX2pFg7aRpHlTn1tfgaSKxs= github.com/throttled/throttled v2.2.4+incompatible/go.mod h1:0BjlrEGQmvxps+HuXLsyRdqpSRvJpq0PNIsOtqP9Nos= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/tylerb/graceful v1.2.15/go.mod h1:LPYTbOYmUTdabwRt0TGhLllQ0MUNbs0Y5q1WXJOI9II= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g= github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM= github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= @@ -350,6 +378,7 @@ golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8U golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9 h1:uc17S921SPw5F2gJo7slQ3aqvr2RwpL7eb3+DZncu3s= golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -465,7 +494,9 @@ gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.44.0 h1:YRJzTUp0kSYWUVFF5XAbDFfyiqwsl0Vb9R8TVP5eRi0= gopkg.in/ini.v1 v1.44.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk= gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= @@ -484,5 +515,7 @@ honnef.co/go/tools v0.0.0-20180920025451-e3ad64cb4ed3/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +willnorris.com/go/gifresize v1.0.0 h1:GKS68zjNhHMqkgNTv4iFAO/j/sNcVSOHQ7SqmDAIAmM= willnorris.com/go/gifresize v1.0.0/go.mod h1:eBM8gogBGCcaH603vxSpnfjwXIpq6nmnj/jauBDKtAk= +willnorris.com/go/imageproxy v0.9.0 h1:pjhb8K4co5Xo0Q/uCeDBPwAtXxZzgvb/Ue0jgYqDapE= willnorris.com/go/imageproxy v0.9.0/go.mod h1:SVC/wfHtCS4kjk3llMeuV4KlTN3a8XTgFWI8o7i3Avg= diff --git a/server/http/http.go b/server/http/http.go index cc53b2f9..4d9a48ca 100644 --- a/server/http/http.go +++ b/server/http/http.go @@ -21,6 +21,7 @@ type Handler struct { Config *config.Config UserStore user.Store API plugin.API + OAuth2StateStore user.OAuth2StateStore BotPoster utils.BotPoster IsAuthorizedAdmin func(userId string) (bool, error) root *mux.Router @@ -35,8 +36,8 @@ func (h *Handler) InitRouter() { oauth2 := h.root.PathPrefix("/oauth2").Subrouter() oauth2.Use(authorizationRequired) - oauth2.HandleFunc("/connect", h.oauth2Connect).Methods("GET") - oauth2.HandleFunc("/complete", h.oauth2Complete).Methods("GET") + oauth2.HandleFunc("/connect", h.OAuth2Connect).Methods("GET") + oauth2.HandleFunc("/complete", h.OAuth2Complete).Methods("GET") h.root.Handle("{anything:.*}", http.NotFoundHandler()) return diff --git a/server/http/oauth2_complete.go b/server/http/oauth2_complete.go index aec3d3ca..29c62d91 100644 --- a/server/http/oauth2_complete.go +++ b/server/http/oauth2_complete.go @@ -14,7 +14,7 @@ import ( "github.com/mattermost/mattermost-plugin-msoffice/server/user" ) -func (h *Handler) oauth2Complete(w http.ResponseWriter, r *http.Request) { +func (h *Handler) OAuth2Complete(w http.ResponseWriter, r *http.Request) { authedUserID := r.Header.Get("Mattermost-User-ID") if authedUserID == "" { http.Error(w, "Not authorized", http.StatusUnauthorized) @@ -31,8 +31,7 @@ func (h *Handler) oauth2Complete(w http.ResponseWriter, r *http.Request) { } state := r.URL.Query().Get("state") - stateStore := user.NewOAuth2StateStore(h.API) - err := stateStore.Verify(state) + err := h.OAuth2StateStore.Verify(state) if err != nil { http.Error(w, "missing stored state: "+err.Error(), http.StatusBadRequest) return diff --git a/server/http/oauth2_connect.go b/server/http/oauth2_connect.go index 5312907e..cb87c292 100644 --- a/server/http/oauth2_connect.go +++ b/server/http/oauth2_connect.go @@ -12,10 +12,9 @@ import ( "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-plugin-msoffice/server/msgraph" - "github.com/mattermost/mattermost-plugin-msoffice/server/user" ) -func (h *Handler) oauth2Connect(w http.ResponseWriter, r *http.Request) { +func (h *Handler) OAuth2Connect(w http.ResponseWriter, r *http.Request) { userID := r.Header.Get("Mattermost-User-ID") if userID == "" { http.Error(w, "Not authorized", http.StatusUnauthorized) @@ -24,8 +23,7 @@ func (h *Handler) oauth2Connect(w http.ResponseWriter, r *http.Request) { conf := msgraph.GetOAuth2Config(h.Config) state := fmt.Sprintf("%v_%v", model.NewId()[0:15], userID) - stateStore := user.NewOAuth2StateStore(h.API) - err := stateStore.Store(state) + err := h.OAuth2StateStore.Store(state) if err != nil { h.jsonError(w, err) return diff --git a/server/http/testhttp/mock_response_writer.go b/server/http/testhttp/mock_response_writer.go new file mode 100644 index 00000000..b940e938 --- /dev/null +++ b/server/http/testhttp/mock_response_writer.go @@ -0,0 +1,41 @@ +package testhttp + +import "net/http" + +var ( + _ http.ResponseWriter = &mockResponseWriter{} +) + +func defaultMockResponseWriter() *mockResponseWriter { + return &mockResponseWriter{ + HeaderMap: make(http.Header), + Bytes: make([]byte, 0), + Err: nil, + StatusCode: http.StatusOK, + } +} + +type mockResponseWriter struct { + HeaderMap http.Header + Bytes []byte + Err error + StatusCode int +} + +func (rw *mockResponseWriter) Header() http.Header { + return rw.HeaderMap +} + +func (rw *mockResponseWriter) Write(bytes []byte) (int, error) { + if rw.Err != nil { + return 0, rw.Err + } + + rw.Bytes = append(rw.Bytes, bytes...) + + return len(rw.Bytes), nil +} + +func (rw *mockResponseWriter) WriteHeader(statusCode int) { + rw.StatusCode = statusCode +} diff --git a/server/http/testhttp/oauth2_complete_test.go b/server/http/testhttp/oauth2_complete_test.go new file mode 100644 index 00000000..a950a3aa --- /dev/null +++ b/server/http/testhttp/oauth2_complete_test.go @@ -0,0 +1,281 @@ +package testhttp + +import ( + "errors" + "fmt" + "net/http" + "net/url" + "testing" + + "github.com/golang/mock/gomock" + "github.com/jarcoal/httpmock" + "github.com/mattermost/mattermost-plugin-msoffice/server/config" + shttp "github.com/mattermost/mattermost-plugin-msoffice/server/http" + "github.com/mattermost/mattermost-plugin-msoffice/server/kvstore/mock_kvstore" + "github.com/mattermost/mattermost-plugin-msoffice/server/user" + "github.com/mattermost/mattermost-plugin-msoffice/server/user/mock_user" + "github.com/mattermost/mattermost-plugin-msoffice/server/utils/mock_utils" + "github.com/mattermost/mattermost-server/app" + "github.com/stretchr/testify/assert" +) + +func makeUserRequest(userID, rawQuery string) *http.Request { + r := &http.Request{ + Header: make(http.Header), + } + + r.URL = &url.URL{ + RawQuery: rawQuery, + } + r.Header.Add("Mattermost-User-ID", userID) + + return r +} + +func TestOAuth2Complete(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + api := &app.PluginAPI{} + + config := &config.Config{} + + config.OAuth2Authority = "common" + config.OAuth2ClientId = "fakeclientid" + config.OAuth2ClientSecret = "fakeclientsecret" + config.PluginURL = "http://localhost" + + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + tcs := []struct { + name string + r *http.Request + setupMocks func(*mock_kvstore.MockKVStore, *mock_user.MockOAuth2StateStore, *mock_utils.MockBotPoster) + registerResponderFunc func() + expectedHTTPResponse string + expectedHTTPCode int + }{ + { + name: "unauthorized user", + setupMocks: func(kv *mock_kvstore.MockKVStore, ss *mock_user.MockOAuth2StateStore, bp *mock_utils.MockBotPoster) {}, + r: &http.Request{}, + registerResponderFunc: func() {}, + expectedHTTPResponse: "Not authorized\n", + expectedHTTPCode: http.StatusUnauthorized, + }, + { + name: "missing authorization code", + setupMocks: func(kv *mock_kvstore.MockKVStore, ss *mock_user.MockOAuth2StateStore, bp *mock_utils.MockBotPoster) {}, + r: makeUserRequest("fake@mattermost.com", "code="), + registerResponderFunc: func() {}, + expectedHTTPResponse: "missing authorization code\n", + expectedHTTPCode: http.StatusBadRequest, + }, + { + name: "missing state", + setupMocks: func(kv *mock_kvstore.MockKVStore, ss *mock_user.MockOAuth2StateStore, bp *mock_utils.MockBotPoster) { + ss.EXPECT().Verify(gomock.Eq("")).Return(errors.New("unable to verify state")).Times(1) + }, + r: makeUserRequest("fake@mattermost.com", "code=fakecode&state="), + registerResponderFunc: func() {}, + expectedHTTPResponse: "missing stored state: unable to verify state\n", + expectedHTTPCode: http.StatusBadRequest, + }, + { + name: "user state not authorized", + setupMocks: func(kv *mock_kvstore.MockKVStore, ss *mock_user.MockOAuth2StateStore, bp *mock_utils.MockBotPoster) { + ss.EXPECT().Verify(gomock.Eq("user_nomatch@mattermost.com")).Return(nil).Times(1) + }, + r: makeUserRequest("fake@mattermost.com", "code=fakecode&state=user_nomatch@mattermost.com"), + registerResponderFunc: func() {}, + expectedHTTPResponse: "Not authorized, user ID mismatch.\n", + expectedHTTPCode: http.StatusUnauthorized, + }, + { + name: "unable to exchange auth code for token", + setupMocks: func(kv *mock_kvstore.MockKVStore, ss *mock_user.MockOAuth2StateStore, bp *mock_utils.MockBotPoster) { + ss.EXPECT().Verify(gomock.Eq("user_fake@mattermost.com")).Return(nil).Times(1) + }, + r: makeUserRequest("fake@mattermost.com", "code=fakecode&state=user_fake@mattermost.com"), + registerResponderFunc: badTokenExchangeResponderFunc, + expectedHTTPResponse: "oauth2: cannot fetch token: 400\nResponse: {\"error\":\"invalid request\"}\n", + expectedHTTPCode: http.StatusInternalServerError, + }, + { + name: "microsoft graph api client unable to get user info", + setupMocks: func(kv *mock_kvstore.MockKVStore, ss *mock_user.MockOAuth2StateStore, bp *mock_utils.MockBotPoster) { + ss.EXPECT().Verify(gomock.Eq("user_fake@mattermost.com")).Return(nil).Times(1) + }, + r: makeUserRequest("fake@mattermost.com", "code=fakecode&state=user_fake@mattermost.com"), + registerResponderFunc: unauthorizedTokenGraphAPIResponderFunc, + expectedHTTPResponse: `Request to url 'https://graph.microsoft.com/v1.0/me' returned error. + Code: InvalidAuthenticationToken + Message: Access token is empty. +`, + expectedHTTPCode: http.StatusInternalServerError, + }, + { + name: "UserStore unable to store user info", + setupMocks: func(kv *mock_kvstore.MockKVStore, ss *mock_user.MockOAuth2StateStore, bp *mock_utils.MockBotPoster) { + kv.EXPECT().Store(gomock.Any(), gomock.Any()).Return(errors.New("forced kvstore error")).Times(1) + ss.EXPECT().Verify(gomock.Eq("user_fake@mattermost.com")).Return(nil).Times(1) + }, + r: makeUserRequest("fake@mattermost.com", "code=fakecode&state=user_fake@mattermost.com"), + registerResponderFunc: statusOKGraphAPIResponderFunc, + expectedHTTPResponse: "Unable to connect: forced kvstore error\n", + expectedHTTPCode: http.StatusInternalServerError, + }, + { + name: "successfully completed oauth2 login", + setupMocks: func(kv *mock_kvstore.MockKVStore, ss *mock_user.MockOAuth2StateStore, bp *mock_utils.MockBotPoster) { + kv. + EXPECT(). + Store(gomock.Any(), gomock.Any()). + Return(nil). + Times(2) + + ss. + EXPECT(). + Verify(gomock.Eq("user_fake@mattermost.com")). + Return(nil). + Times(1) + + bp. + EXPECT(). + PostDirect(gomock.Eq("fake@mattermost.com"), gomock.Eq(getBotPosterMessage("displayName-value")), gomock.Eq("custom_TODO")). + Return(nil). + Times(1) + }, + r: makeUserRequest("fake@mattermost.com", "code=fakecode&state=user_fake@mattermost.com"), + registerResponderFunc: statusOKGraphAPIResponderFunc, + expectedHTTPResponse: ` + + + + + + +

Completed connecting to Microsoft Office. Please close this window.

+ + + `, + expectedHTTPCode: http.StatusOK, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + tc.registerResponderFunc() + + mockKVStore := mock_kvstore.NewMockKVStore(ctrl) + mockOAuth2StateStore := mock_user.NewMockOAuth2StateStore(ctrl) + mockBotPoster := mock_utils.NewMockBotPoster(ctrl) + + tc.setupMocks(mockKVStore, mockOAuth2StateStore, mockBotPoster) + + handler := shttp.Handler{ + Config: config, + API: api, + } + + handler.UserStore = user.NewStore(mockKVStore) + handler.OAuth2StateStore = mockOAuth2StateStore + handler.BotPoster = mockBotPoster + + w := defaultMockResponseWriter() + + handler.OAuth2Complete(w, tc.r) + + assert.Equal(t, tc.expectedHTTPCode, w.StatusCode) + assert.Equal(t, tc.expectedHTTPResponse, string(w.Bytes)) + }) + } +} + +func badTokenExchangeResponderFunc() { + url := "https://login.microsoftonline.com/common/oauth2/v2.0/token" + + responder := httpmock.NewStringResponder(http.StatusBadRequest, `{"error":"invalid request"}`) + + httpmock.RegisterResponder("POST", url, responder) +} + +func unauthorizedTokenGraphAPIResponderFunc() { + tokenURL := "https://login.microsoftonline.com/common/oauth2/v2.0/token" + + tokenResponse := `{ + "token_type": "Bearer", + "scope": "user.read%20Fmail.read", + "expires_in": 3600, + "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ik5HVEZ2ZEstZnl0aEV1Q...", + "refresh_token": "AwABAAAAvPM1KaPlrEqdFSBzjqfTGAMxZGUTdM0t4B4..." +}` + + tokenResponder := httpmock.NewStringResponder(http.StatusOK, tokenResponse) + + httpmock.RegisterResponder("POST", tokenURL, tokenResponder) + + meRequestURL := "https://graph.microsoft.com/v1.0/me" + + meResponse := `{ + "error": { + "code": "InvalidAuthenticationToken", + "message": "Access token is empty.", + "innerError": { + "request-id": "d1a6e016-c7c4-4caf-9a7f-2d7079dc05d2", + "date": "2019-11-12T00:49:46" + } + } +}` + + meResponder := httpmock.NewStringResponder(http.StatusUnauthorized, meResponse) + + httpmock.RegisterResponder("GET", meRequestURL, meResponder) +} + +func statusOKGraphAPIResponderFunc() { + tokenURL := "https://login.microsoftonline.com/common/oauth2/v2.0/token" + + tokenResponse := `{ + "token_type": "Bearer", + "scope": "user.read%20Fmail.read", + "expires_in": 3600, + "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ik5HVEZ2ZEstZnl0aEV1Q...", + "refresh_token": "AwABAAAAvPM1KaPlrEqdFSBzjqfTGAMxZGUTdM0t4B4..." +}` + + tokenResponder := httpmock.NewStringResponder(http.StatusOK, tokenResponse) + + httpmock.RegisterResponder("POST", tokenURL, tokenResponder) + + meRequestURL := "https://graph.microsoft.com/v1.0/me" + + meResponse := `{ + "businessPhones": [ + "businessPhones-value" + ], + "displayName": "displayName-value", + "givenName": "givenName-value", + "jobTitle": "jobTitle-value", + "mail": "mail-value", + "mobilePhone": "mobilePhone-value", + "officeLocation": "officeLocation-value", + "preferredLanguage": "preferredLanguage-value", + "surname": "surname-value", + "userPrincipalName": "userPrincipalName-value", + "id": "id-value" +}` + + meResponder := httpmock.NewStringResponder(http.StatusOK, meResponse) + + httpmock.RegisterResponder("GET", meRequestURL, meResponder) +} + +func getBotPosterMessage(displayName string) string { + return fmt.Sprintf("### Welcome to the Microsoft Office plugin!\n"+ + "Here is some info to prove we got you logged in\n"+ + "Name: %s \n", displayName) +} diff --git a/server/http/testhttp/oauth2_connect_test.go b/server/http/testhttp/oauth2_connect_test.go new file mode 100644 index 00000000..c39d0d9a --- /dev/null +++ b/server/http/testhttp/oauth2_connect_test.go @@ -0,0 +1,79 @@ +package testhttp + +import ( + "errors" + "net/http" + "testing" + + "github.com/golang/mock/gomock" + "github.com/mattermost/mattermost-plugin-msoffice/server/config" + shttp "github.com/mattermost/mattermost-plugin-msoffice/server/http" + "github.com/mattermost/mattermost-plugin-msoffice/server/kvstore/mock_kvstore" + "github.com/mattermost/mattermost-plugin-msoffice/server/user" + "github.com/mattermost/mattermost-plugin-msoffice/server/user/mock_user" + "github.com/mattermost/mattermost-plugin-msoffice/server/utils/mock_utils" + "github.com/stretchr/testify/assert" +) + +func TestOAuth2Connect(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + tcs := []struct { + name string + r *http.Request + setupMocks func(*mock_kvstore.MockKVStore, *mock_user.MockOAuth2StateStore, *mock_utils.MockBotPoster) + expectedHTTPResponse string + expectedHTTPCode int + }{ + { + name: "unauthorized user", + r: &http.Request{}, + setupMocks: func(kv *mock_kvstore.MockKVStore, ss *mock_user.MockOAuth2StateStore, bp *mock_utils.MockBotPoster) {}, + expectedHTTPResponse: "Not authorized\n", + expectedHTTPCode: http.StatusUnauthorized, + }, + { + name: "unable to store user state", + r: makeUserRequest("fake@mattermost.com", ""), + setupMocks: func(kv *mock_kvstore.MockKVStore, ss *mock_user.MockOAuth2StateStore, bp *mock_utils.MockBotPoster) { + ss.EXPECT().Store(gomock.Any()).Return(errors.New("unable to store state")).Times(1) + }, + expectedHTTPResponse: "{\"error\":\"An internal error has occurred. Check app server logs for details.\",\"details\":\"unable to store state\"}", + expectedHTTPCode: http.StatusInternalServerError, + }, + { + name: "successful redirect", + r: makeUserRequest("fake@mattermost.com", ""), + setupMocks: func(kv *mock_kvstore.MockKVStore, ss *mock_user.MockOAuth2StateStore, bp *mock_utils.MockBotPoster) { + ss.EXPECT().Store(gomock.Any()).Return(nil).Times(1) + }, + expectedHTTPResponse: "", + expectedHTTPCode: http.StatusFound, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + mockKVStore := mock_kvstore.NewMockKVStore(ctrl) + mockOAuth2StateStore := mock_user.NewMockOAuth2StateStore(ctrl) + mockBotPoster := mock_utils.NewMockBotPoster(ctrl) + + tc.setupMocks(mockKVStore, mockOAuth2StateStore, mockBotPoster) + + handler := shttp.Handler{ + Config: &config.Config{}, + } + + w := defaultMockResponseWriter() + + handler.UserStore = user.NewStore(mockKVStore) + handler.OAuth2StateStore = mockOAuth2StateStore + + handler.OAuth2Connect(w, tc.r) + + assert.Equal(t, tc.expectedHTTPCode, w.StatusCode) + assert.Equal(t, tc.expectedHTTPResponse, string(w.Bytes)) + }) + } +} diff --git a/server/kvstore/mock_kvstore/mock_kvstore.go b/server/kvstore/mock_kvstore/mock_kvstore.go new file mode 100644 index 00000000..c83c82ee --- /dev/null +++ b/server/kvstore/mock_kvstore/mock_kvstore.go @@ -0,0 +1,77 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/mattermost/mattermost-plugin-msoffice/server/kvstore (interfaces: KVStore) + +// Package mock_kvstore is a generated GoMock package. +package mock_kvstore + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// MockKVStore is a mock of KVStore interface +type MockKVStore struct { + ctrl *gomock.Controller + recorder *MockKVStoreMockRecorder +} + +// MockKVStoreMockRecorder is the mock recorder for MockKVStore +type MockKVStoreMockRecorder struct { + mock *MockKVStore +} + +// NewMockKVStore creates a new mock instance +func NewMockKVStore(ctrl *gomock.Controller) *MockKVStore { + mock := &MockKVStore{ctrl: ctrl} + mock.recorder = &MockKVStoreMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockKVStore) EXPECT() *MockKVStoreMockRecorder { + return m.recorder +} + +// Delete mocks base method +func (m *MockKVStore) Delete(arg0 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete +func (mr *MockKVStoreMockRecorder) Delete(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockKVStore)(nil).Delete), arg0) +} + +// Load mocks base method +func (m *MockKVStore) Load(arg0 string) ([]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Load", arg0) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Load indicates an expected call of Load +func (mr *MockKVStoreMockRecorder) Load(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Load", reflect.TypeOf((*MockKVStore)(nil).Load), arg0) +} + +// Store mocks base method +func (m *MockKVStore) Store(arg0 string, arg1 []byte) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Store", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// Store indicates an expected call of Store +func (mr *MockKVStoreMockRecorder) Store(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Store", reflect.TypeOf((*MockKVStore)(nil).Store), arg0, arg1) +} diff --git a/server/msgraph/testmsgraph/client_test.go b/server/msgraph/testmsgraph/client_test.go new file mode 100644 index 00000000..951e1ea8 --- /dev/null +++ b/server/msgraph/testmsgraph/client_test.go @@ -0,0 +1,46 @@ +package testmsgraph + +import ( + "testing" + "time" + + "github.com/mattermost/mattermost-plugin-msoffice/server/config" + "github.com/mattermost/mattermost-plugin-msoffice/server/msgraph" + "golang.org/x/oauth2" +) + +const ( + tokenURLEndpoint = "https://login.microsoftonline.com/%s/oauth2/v2.0/token" +) + +func testConfig() *config.Config { + conf := &config.Config{} + + conf.OAuth2Authority = "common" + conf.OAuth2ClientId = "fakeclientid" + conf.OAuth2ClientSecret = "fakeclientsecret" + conf.PluginURL = "http://localhost" + + return conf +} + +func getToken(expiry time.Time) *oauth2.Token { + return &oauth2.Token{ + AccessToken: "fake_access_token", + TokenType: "bearer", + RefreshToken: "fake_refresh_token", + Expiry: expiry, + } +} + +func TestNewClient(t *testing.T) { + conf := testConfig() + + token := getToken(time.Now().Add(time.Hour)) + + client := msgraph.NewClient(conf, token) + + if client == nil { + t.Errorf("expected client to be non-nil but got: %+v", client) + } +} diff --git a/server/msgraph/testmsgraph/get_me_test.go b/server/msgraph/testmsgraph/get_me_test.go new file mode 100644 index 00000000..39da509b --- /dev/null +++ b/server/msgraph/testmsgraph/get_me_test.go @@ -0,0 +1,123 @@ +package testmsgraph + +import ( + "errors" + "fmt" + "net/http" + "net/url" + "testing" + "time" + + "github.com/jarcoal/httpmock" + graph "github.com/jkrecek/msgraph-go" + "github.com/mattermost/mattermost-plugin-msoffice/server/msgraph" + "github.com/stretchr/testify/assert" +) + +func TestGetMe(t *testing.T) { + tcs := []struct { + name string + client msgraph.Client + registerResponderFunc func() + expectedMe *graph.Me + expectedErr error + }{ + { + name: "successful get calendar api call (no token refresh)", + client: msgraph.NewClient(testConfig(), getToken(time.Now().Add(time.Hour))), + registerResponderFunc: statusOKGraphAPIMeResponderFunc, + expectedMe: &graph.Me{ + Id: "id-value", + UserPrincipalName: "userPrincipalName-value", + GivenName: "givenName-value", + DisplayName: "displayName-value", + Surname: "surname-value", + }, + expectedErr: nil, + }, + { + name: "unsuccessful get calendar api call (token refresh needed)", + client: msgraph.NewClient(testConfig(), getToken(time.Now())), + registerResponderFunc: statusOKGraphAPIMeResponderFunc, + expectedMe: nil, + expectedErr: &url.Error{ + Op: "Get", + URL: "https://graph.microsoft.com/v1.0/me", + Err: &url.Error{ + Op: "Post", + URL: fmt.Sprintf(tokenURLEndpoint, testConfig().OAuth2Authority), + Err: errors.New("no responder found"), + }, + }, + }, + { + name: "successful get calendar api call (with token refresh)", + client: msgraph.NewClient(testConfig(), getToken(time.Now())), + registerResponderFunc: func() { + statusOKTokenRefreshResponderFunc() + statusOKGraphAPIMeResponderFunc() + }, + expectedMe: &graph.Me{ + Id: "id-value", + UserPrincipalName: "userPrincipalName-value", + GivenName: "givenName-value", + DisplayName: "displayName-value", + Surname: "surname-value", + }, + expectedErr: nil, + }, + } + + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + tc.registerResponderFunc() + + me, err := tc.client.GetMe() + if err != nil { + t.Log(err.Error()) + } + + assert.Equal(t, tc.expectedErr, err) + assert.Equal(t, tc.expectedMe, me) + }) + } +} + +func statusOKGraphAPIMeResponderFunc() { + meRequestURL := "https://graph.microsoft.com/v1.0/me" + + meResponder := httpmock.NewStringResponder(http.StatusOK, `{ + "businessPhones": [ + "businessPhones-value" + ], + "displayName": "displayName-value", + "givenName": "givenName-value", + "jobTitle": "jobTitle-value", + "mail": "mail-value", + "mobilePhone": "mobilePhone-value", + "officeLocation": "officeLocation-value", + "preferredLanguage": "preferredLanguage-value", + "surname": "surname-value", + "userPrincipalName": "userPrincipalName-value", + "id": "id-value" +}`) + + httpmock.RegisterResponder("GET", meRequestURL, meResponder) +} + +func statusOKTokenRefreshResponderFunc() { + tokenURL := "https://login.microsoftonline.com/common/oauth2/v2.0/token" + + tokenResponder := httpmock.NewStringResponder(http.StatusOK, `{ + "token_type": "Bearer", + "scope": "user.read%20Fmail.read", + "expires_in": 3600, + "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ik5HVEZ2ZEstZnl0aEV1Q...", + "refresh_token": "AwABAAAAvPM1KaPlrEqdFSBzjqfTGAMxZGUTdM0t4B4..." +}`) + + httpmock.RegisterResponder("POST", tokenURL, tokenResponder) +} diff --git a/server/msgraph/testmsgraph/get_user_calendar_test.go b/server/msgraph/testmsgraph/get_user_calendar_test.go new file mode 100644 index 00000000..29f7e50a --- /dev/null +++ b/server/msgraph/testmsgraph/get_user_calendar_test.go @@ -0,0 +1,106 @@ +package testmsgraph + +import ( + "errors" + "fmt" + "net/http" + "net/url" + "testing" + "time" + + "github.com/jarcoal/httpmock" + graph "github.com/jkrecek/msgraph-go" + "github.com/mattermost/mattermost-plugin-msoffice/server/msgraph" + "github.com/stretchr/testify/assert" +) + +func TestGetUserCalendar(t *testing.T) { + tcs := []struct { + name string + client msgraph.Client + registerResponderFunc func() + expectedCalendars []*graph.Calendar + expectedErr error + }{ + { + name: "successful get calendars api call (no token refresh)", + client: msgraph.NewClient(testConfig(), getToken(time.Now().Add(time.Hour))), + registerResponderFunc: statusOKGraphAPICalendarResponderFunc, + expectedCalendars: []*graph.Calendar{ + &graph.Calendar{ + Id: "id-value", + Name: "name-value", + Color: "color-value", + ChangeKey: "changeKey-value", + }, + }, + expectedErr: nil, + }, + { + name: "unsuccessful get calendars api call (token refresh needed)", + client: msgraph.NewClient(testConfig(), getToken(time.Now())), + registerResponderFunc: statusOKGraphAPICalendarResponderFunc, + expectedCalendars: nil, + expectedErr: &url.Error{ + Op: "Get", + URL: "https://graph.microsoft.com/v1.0/me/calendars", + Err: &url.Error{ + Op: "Post", + URL: fmt.Sprintf(tokenURLEndpoint, testConfig().OAuth2Authority), + Err: errors.New("no responder found"), + }, + }, + }, + { + name: "successful get calendars api call (with token refresh)", + client: msgraph.NewClient(testConfig(), getToken(time.Now())), + registerResponderFunc: func() { + statusOKTokenRefreshResponderFunc() + statusOKGraphAPICalendarResponderFunc() + }, + expectedCalendars: []*graph.Calendar{ + &graph.Calendar{ + Id: "id-value", + Name: "name-value", + Color: "color-value", + ChangeKey: "changeKey-value", + }, + }, + expectedErr: nil, + }, + } + + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + tc.registerResponderFunc() + + calendars, err := tc.client.GetUserCalendar("") + if err != nil { + t.Log(err.Error()) + } + + assert.Equal(t, tc.expectedErr, err) + assert.Equal(t, tc.expectedCalendars, calendars) + }) + } +} + +func statusOKGraphAPICalendarResponderFunc() { + meRequestURL := "https://graph.microsoft.com/v1.0/me/calendars" + + meResponder := httpmock.NewStringResponder(http.StatusOK, `{ + "value": [ + { + "changeKey": "changeKey-value", + "name": "name-value", + "color": "color-value", + "id": "id-value" + } + ] +}`) + + httpmock.RegisterResponder("GET", meRequestURL, meResponder) +} diff --git a/server/plugin/plugin.go b/server/plugin/plugin.go index 74c441aa..f95bf87e 100644 --- a/server/plugin/plugin.go +++ b/server/plugin/plugin.go @@ -156,6 +156,7 @@ func (p *Plugin) newHTTPHandler(conf *config.Config) *http.Handler { API: p.API, BotPoster: utils.NewBotPoster(conf, p.API), IsAuthorizedAdmin: p.IsAuthorizedAdmin, + OAuth2StateStore: p.OAuth2StateStore, } h.InitRouter() return h diff --git a/server/user/mock_user/mock_oauth2_store.go b/server/user/mock_user/mock_oauth2_store.go new file mode 100644 index 00000000..e416503a --- /dev/null +++ b/server/user/mock_user/mock_oauth2_store.go @@ -0,0 +1,62 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/mattermost/mattermost-plugin-msoffice/server/user (interfaces: OAuth2StateStore) + +// Package mock_user is a generated GoMock package. +package mock_user + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// MockOAuth2StateStore is a mock of OAuth2StateStore interface +type MockOAuth2StateStore struct { + ctrl *gomock.Controller + recorder *MockOAuth2StateStoreMockRecorder +} + +// MockOAuth2StateStoreMockRecorder is the mock recorder for MockOAuth2StateStore +type MockOAuth2StateStoreMockRecorder struct { + mock *MockOAuth2StateStore +} + +// NewMockOAuth2StateStore creates a new mock instance +func NewMockOAuth2StateStore(ctrl *gomock.Controller) *MockOAuth2StateStore { + mock := &MockOAuth2StateStore{ctrl: ctrl} + mock.recorder = &MockOAuth2StateStoreMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockOAuth2StateStore) EXPECT() *MockOAuth2StateStoreMockRecorder { + return m.recorder +} + +// Store mocks base method +func (m *MockOAuth2StateStore) Store(arg0 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Store", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// Store indicates an expected call of Store +func (mr *MockOAuth2StateStoreMockRecorder) Store(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Store", reflect.TypeOf((*MockOAuth2StateStore)(nil).Store), arg0) +} + +// Verify mocks base method +func (m *MockOAuth2StateStore) Verify(arg0 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Verify", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// Verify indicates an expected call of Verify +func (mr *MockOAuth2StateStoreMockRecorder) Verify(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Verify", reflect.TypeOf((*MockOAuth2StateStore)(nil).Verify), arg0) +} diff --git a/server/utils/mock_utils/mock_bot_poster.go b/server/utils/mock_utils/mock_bot_poster.go new file mode 100644 index 00000000..53db0cec --- /dev/null +++ b/server/utils/mock_utils/mock_bot_poster.go @@ -0,0 +1,60 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/mattermost/mattermost-plugin-msoffice/server/utils (interfaces: BotPoster) + +// Package mock_utils is a generated GoMock package. +package mock_utils + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// MockBotPoster is a mock of BotPoster interface +type MockBotPoster struct { + ctrl *gomock.Controller + recorder *MockBotPosterMockRecorder +} + +// MockBotPosterMockRecorder is the mock recorder for MockBotPoster +type MockBotPosterMockRecorder struct { + mock *MockBotPoster +} + +// NewMockBotPoster creates a new mock instance +func NewMockBotPoster(ctrl *gomock.Controller) *MockBotPoster { + mock := &MockBotPoster{ctrl: ctrl} + mock.recorder = &MockBotPosterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockBotPoster) EXPECT() *MockBotPosterMockRecorder { + return m.recorder +} + +// PostDirect mocks base method +func (m *MockBotPoster) PostDirect(arg0, arg1, arg2 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PostDirect", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// PostDirect indicates an expected call of PostDirect +func (mr *MockBotPosterMockRecorder) PostDirect(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostDirect", reflect.TypeOf((*MockBotPoster)(nil).PostDirect), arg0, arg1, arg2) +} + +// PostEphemeral mocks base method +func (m *MockBotPoster) PostEphemeral(arg0, arg1, arg2 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "PostEphemeral", arg0, arg1, arg2) +} + +// PostEphemeral indicates an expected call of PostEphemeral +func (mr *MockBotPosterMockRecorder) PostEphemeral(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostEphemeral", reflect.TypeOf((*MockBotPoster)(nil).PostEphemeral), arg0, arg1, arg2) +}