Repository: mattermost/mattermost-plugin-github
Pull request #315 (2020-06-24 18:48:59)

Add API for other plugins to use
#### Summary
Expose two API endpoints for other plugins to use:
- GetConfig
- GetToken

For seamless integration into other plugins a new `client` package is exposed. In order to keep the code DRY. all plugin code expect `main.go` got moved into a separate package.

Also, a  `GetGitHubClient` method is added to avoid copy&pasting the same code in multiple plugins.

#### Ticket Link
Fixes https://github.com/mattermost/mattermost-plugin-github/issues/259



codecov-commenter: # [Codecov](https://codecov.io/gh/mattermost/mattermost-plugin-github/pull/315?src=pr&el=h1) Report
> Merging [#315](https://codecov.io/gh/mattermost/mattermost-plugin-github/pull/315?src=pr&el=desc) into [master](https://codecov.io/gh/mattermost/mattermost-plugin-github/commit/eee81144b7cc584f54504fd5f10ec6c7590c7369&el=desc) will **increase** coverage by `1.26%`.
> The diff coverage is `48.97%`.

[![Impacted file tree graph](https://codecov.io/gh/mattermost/mattermost-plugin-github/pull/315/graphs/tree.svg?width=650&height=150&src=pr&token=0uHZTjTrgl)](https://codecov.io/gh/mattermost/mattermost-plugin-github/pull/315?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #315      +/-   ##
==========================================
+ Coverage   18.69%   19.96%   +1.26%     
==========================================
  Files          10       10              
  Lines        2455     2540      +85     
==========================================
+ Hits          459      507      +48     
- Misses       1971     1998      +27     
- Partials       25       35      +10     
```


| [Impacted Files](https://codecov.io/gh/mattermost/mattermost-plugin-github/pull/315?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [server/plugin/command.go](https://codecov.io/gh/mattermost/mattermost-plugin-github/pull/315/diff?src=pr&el=tree#diff-c2VydmVyL3BsdWdpbi9jb21tYW5kLmdv) | `9.57% <ø> (ø)` | |
| [server/plugin/permalinks.go](https://codecov.io/gh/mattermost/mattermost-plugin-github/pull/315/diff?src=pr&el=tree#diff-c2VydmVyL3BsdWdpbi9wZXJtYWxpbmtzLmdv) | `77.14% <ø> (ø)` | |
| [server/plugin/plugin.go](https://codecov.io/gh/mattermost/mattermost-plugin-github/pull/315/diff?src=pr&el=tree#diff-c2VydmVyL3BsdWdpbi9wbHVnaW4uZ28=) | `4.20% <0.00%> (ø)` | |
| [server/plugin/subscriptions.go](https://codecov.io/gh/mattermost/mattermost-plugin-github/pull/315/diff?src=pr&el=tree#diff-c2VydmVyL3BsdWdpbi9zdWJzY3JpcHRpb25zLmdv) | `10.97% <ø> (ø)` | |
| [server/plugin/template.go](https://codecov.io/gh/mattermost/mattermost-plugin-github/pull/315/diff?src=pr&el=tree#diff-c2VydmVyL3BsdWdpbi90ZW1wbGF0ZS5nbw==) | `96.05% <ø> (ø)` | |
| [server/plugin/utils.go](https://codecov.io/gh/mattermost/mattermost-plugin-github/pull/315/diff?src=pr&el=tree#diff-c2VydmVyL3BsdWdpbi91dGlscy5nbw==) | `61.32% <ø> (ø)` | |
| [server/plugin/webhook.go](https://codecov.io/gh/mattermost/mattermost-plugin-github/pull/315/diff?src=pr&el=tree#diff-c2VydmVyL3BsdWdpbi93ZWJob29rLmdv) | `0.00% <ø> (ø)` | |
| [server/plugin/configuration.go](https://codecov.io/gh/mattermost/mattermost-plugin-github/pull/315/diff?src=pr&el=tree#diff-c2VydmVyL3BsdWdpbi9jb25maWd1cmF0aW9uLmdv) | `32.25% <50.00%> (ø)` | |
| [server/client/client.go](https://codecov.io/gh/mattermost/mattermost-plugin-github/pull/315/diff?src=pr&el=tree#diff-c2VydmVyL2NsaWVudC9jbGllbnQuZ28=) | `54.23% <54.23%> (ø)` | |
| [server/plugin/api.go](https://codecov.io/gh/mattermost/mattermost-plugin-github/pull/315/diff?src=pr&el=tree#diff-c2VydmVyL3BsdWdpbi9hcGkuZ28=) | `7.62% <56.52%> (ø)` | |
| ... and [2 more](https://codecov.io/gh/mattermost/mattermost-plugin-github/pull/315/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/mattermost/mattermost-plugin-github/pull/315?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/mattermost/mattermost-plugin-github/pull/315?src=pr&el=footer). Last update [eee8114...e32fa68](https://codecov.io/gh/mattermost/mattermost-plugin-github/pull/315?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

levb: Replaced myself with @crspeller for security review. I am generally ok with the mechanics of the implementation.
hanzei: @DHaussermann You can test this PR in the same way as https://github.com/mattermost/mattermost-server/pull/14905
DHaussermann: @hanzei I'm seeing an issue on this one where the plugin crashes when deployed due to minimum version not being met.

I see 
```
{"level":"info","ts":1594395927.3588865,"caller":"cluster/cluster.go:332","msg":"This node attempted to join the cluster, but could not find any nodes."}
{"level":"error","ts":1594395930.4746335,"caller":"mlog/log.go:175","msg":"Unable to restart plugin on upgrade.","path":"/api/v4/plugins","request_id":"fnrezpboo38r5x67xqxob15bph","ip_addr":"10.160.99.229","user_id":"s1jrzjbtsfffzjdkkjeztfdyue","method":"POST","err_where":"installExtractedPlugin","http_code":500,"err_details":"failed to register commands: failed to register agenda command: invalid autocomplete data in command: An empty command name in the autocomplete data"}
```

The server is running `5.25.0-rc1` 

Any thoughts to help isolate this? Could this be am incorrect error? 
hanzei: @DHaussermann `v5.25.0-rc1` does incorrectly report it's own version as `5.24.0`. This bug was fixed in https://github.com/mattermost/mattermost-server/pull/14948 and will not longer be present in `v5.25.0-rc2`. Could you please test with  `v5.25.0-rc2` or `master`?

Patch:
diff --git a/.golangci.yml b/.golangci.yml
index 4d5e63d..246bb25 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -47,19 +47,11 @@ linters:
 
 issues:
   exclude-rules:
-    - path: server/manifest.go
-      linters:
-        - deadcode
-        - unused
-        - varcheck
-    - path: server/configuration.go
-      linters:
-        - unused
     - path: _test\.go
       linters:
         - bodyclose
         - goconst
         - scopelint # https://github.com/kyoh86/scopelint/issues/4
-    - path: server/webhook.go
+    - path: server/plugin/webhook.go
       linters:
         - goconst
diff --git a/build/manifest/main.go b/build/manifest/main.go
index 89a397c..af39c6f 100644
--- a/build/manifest/main.go
+++ b/build/manifest/main.go
@@ -12,9 +12,9 @@ import (
 
 const pluginIDGoFileTemplate = `// This file is automatically generated. Do not modify it manually.
 
-package main
+package plugin
 
-var manifest = struct {
+var Manifest = struct {
 	ID      string
 	Version string
 }{
@@ -104,7 +104,7 @@ func dumpPluginVersion(manifest *model.Manifest) {
 func applyManifest(manifest *model.Manifest) error {
 	if manifest.HasServer() {
 		if err := ioutil.WriteFile(
-			"server/manifest.go",
+			"server/plugin/manifest.go",
 			[]byte(fmt.Sprintf(pluginIDGoFileTemplate, manifest.Id, manifest.Version)),
 			0644,
 		); err != nil {
diff --git a/plugin.json b/plugin.json
index 12884b7..5ce5544 100644
--- a/plugin.json
+++ b/plugin.json
@@ -6,7 +6,7 @@
     "support_url": "https://github.com/mattermost/mattermost-plugin-github/issues",
     "icon_path": "assets/icon.svg",
     "version": "1.0.0",
-    "min_server_version": "5.12.0",
+    "min_server_version": "5.25.0",
     "server": {
       "executables": {
           "linux-amd64": "server/dist/plugin-linux-amd64",
diff --git a/server/client/client.go b/server/client/client.go
new file mode 100644
index 0000000..d52c026
--- /dev/null
+++ b/server/client/client.go
@@ -0,0 +1,125 @@
+package client
+
+import (
+	"encoding/json"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+
+	"github.com/google/go-github/v31/github"
+	"github.com/pkg/errors"
+	"golang.org/x/oauth2"
+
+	"github.com/mattermost/mattermost-plugin-github/server/plugin"
+)
+
+type PluginAPI interface {
+	PluginHTTP(*http.Request) *http.Response
+}
+
+type Client struct {
+	httpClient http.Client
+}
+
+type pluginAPIRoundTripper struct {
+	api PluginAPI
+}
+
+func (p *pluginAPIRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
+	resp := p.api.PluginHTTP(req)
+	if resp == nil {
+		return nil, errors.Errorf("Failed to make interplugin request")
+	}
+
+	return resp, nil
+}
+
+func NewPluginClient(api PluginAPI) *Client {
+	client := &Client{}
+	client.httpClient.Transport = &pluginAPIRoundTripper{api}
+
+	return client
+}
+
+func (c *Client) GetGitHubClient(userID string) (*github.Client, error) {
+	token, err := c.GetToken(userID)
+	if err != nil {
+		return nil, err
+	}
+
+	config, err := c.GetConfiguration()
+	if err != nil {
+		return nil, err
+	}
+
+	client, err := plugin.GetGitHubClient(*token, config)
+	if err != nil {
+		return nil, err
+	}
+
+	return client, nil
+}
+
+func (c *Client) GetConfiguration() (*plugin.Configuration, error) {
+	req, err := http.NewRequest(http.MethodGet, "/"+plugin.Manifest.ID+"/api/v1/config", http.NoBody)
+	if err != nil {
+		return nil, err
+	}
+
+	resp, err := c.httpClient.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+
+	respBody, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+
+	if resp.StatusCode != http.StatusOK {
+		return nil, errors.Errorf("Unable to get GitHub config. Error: %v, %v", resp.StatusCode, string(respBody))
+	}
+
+	config := &plugin.Configuration{}
+	err = json.Unmarshal(respBody, config)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to decode GitHub config")
+	}
+
+	return config, nil
+}
+
+func (c *Client) GetToken(userID string) (*oauth2.Token, error) {
+	req, err := http.NewRequest(http.MethodGet, "/"+plugin.Manifest.ID+"/api/v1/token", http.NoBody)
+	if err != nil {
+		return nil, err
+	}
+
+	values := url.Values{}
+	values.Add("userID", userID)
+	req.URL.RawQuery = values.Encode()
+
+	resp, err := c.httpClient.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+
+	respBody, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+
+	if resp.StatusCode != http.StatusOK {
+		return nil, errors.Errorf("Unable to get GitHub token. Error: %v, %v", resp.StatusCode, string(respBody))
+	}
+
+	token := &oauth2.Token{}
+	err = json.Unmarshal(respBody, token)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to decode GitHub token")
+	}
+
+	return token, nil
+}
diff --git a/server/main.go b/server/main.go
index 45efa65..53b8264 100644
--- a/server/main.go
+++ b/server/main.go
@@ -1,9 +1,11 @@
 package main
 
 import (
-	"github.com/mattermost/mattermost-server/v5/plugin"
+	mmplugin "github.com/mattermost/mattermost-server/v5/plugin"
+
+	"github.com/mattermost/mattermost-plugin-github/server/plugin"
 )
 
 func main() {
-	plugin.ClientMain(NewPlugin())
+	mmplugin.ClientMain(plugin.NewPlugin())
 }
diff --git a/server/api.go b/server/plugin/api.go
similarity index 96%
rename from server/api.go
rename to server/plugin/api.go
index e7c273d..d84da8d 100644
--- a/server/api.go
+++ b/server/plugin/api.go
@@ -1,4 +1,4 @@
-package main
+package plugin
 
 import (
 	"context"
@@ -112,6 +112,9 @@ func (p *Plugin) initializeAPI() {
 	apiRouter.HandleFunc("/user", p.extractUserMiddleWare(p.getGitHubUser, true)).Methods(http.MethodPost)
 	apiRouter.HandleFunc("/issue", p.extractUserMiddleWare(p.getIssueByNumber, false)).Methods(http.MethodGet)
 	apiRouter.HandleFunc("/pr", p.extractUserMiddleWare(p.getPrByNumber, false)).Methods(http.MethodGet)
+
+	apiRouter.HandleFunc("/config", checkPluginRequest(p.getConfig)).Methods(http.MethodGet)
+	apiRouter.HandleFunc("/token", checkPluginRequest(p.getToken)).Methods(http.MethodGet)
 }
 
 func (p *Plugin) extractUserMiddleWare(handler HTTPHandlerFuncWithUser, jsonResponse bool) http.HandlerFunc {
@@ -130,6 +133,19 @@ func (p *Plugin) extractUserMiddleWare(handler HTTPHandlerFuncWithUser, jsonResp
 	}
 }
 
+func checkPluginRequest(next http.HandlerFunc) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		// All other plugins are allowed
+		pluginID := r.Header.Get("Mattermost-Plugin-ID")
+		if pluginID == "" {
+			http.Error(w, "Not authorized", http.StatusUnauthorized)
+			return
+		}
+
+		next(w, r)
+	}
+}
+
 func (p *Plugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
 	config := p.getConfiguration()
 
@@ -138,6 +154,7 @@ func (p *Plugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Req
 		return
 	}
 
+	r.Header.Add("Mattermost-Plugin-ID", c.SourcePluginId)
 	w.Header().Set("Content-Type", "application/json")
 
 	p.router.ServeHTTP(w, r)
@@ -1109,3 +1126,25 @@ func (p *Plugin) createIssue(w http.ResponseWriter, r *http.Request, userID stri
 
 	p.writeJSON(w, result)
 }
+
+func (p *Plugin) getConfig(w http.ResponseWriter, r *http.Request) {
+	config := p.getConfiguration()
+
+	p.writeJSON(w, config)
+}
+
+func (p *Plugin) getToken(w http.ResponseWriter, r *http.Request) {
+	userID := r.FormValue("userID")
+	if userID == "" {
+		http.Error(w, "please provide a userID", http.StatusBadRequest)
+		return
+	}
+
+	info, apiErr := p.getGitHubUserInfo(userID)
+	if apiErr != nil {
+		http.Error(w, apiErr.Error(), apiErr.StatusCode)
+		return
+	}
+
+	p.writeJSON(w, info.Token)
+}
diff --git a/server/command.go b/server/plugin/command.go
similarity index 99%
rename from server/command.go
rename to server/plugin/command.go
index c937416..72fca8e 100644
--- a/server/command.go
+++ b/server/plugin/command.go
@@ -1,4 +1,4 @@
-package main
+package plugin
 
 import (
 	"context"
diff --git a/server/configuration.go b/server/plugin/configuration.go
similarity index 91%
rename from server/configuration.go
rename to server/plugin/configuration.go
index c92b29d..0c1146d 100644
--- a/server/configuration.go
+++ b/server/plugin/configuration.go
@@ -1,4 +1,4 @@
-package main
+package plugin
 
 import (
 	"reflect"
@@ -17,7 +17,7 @@ import (
 //
 // If you add non-reference types to your configuration struct, be sure to rewrite Clone as a deep
 // copy appropriate for your types.
-type configuration struct {
+type Configuration struct {
 	GitHubOrg               string
 	GitHubOAuthClientID     string
 	GitHubOAuthClientSecret string
@@ -31,13 +31,13 @@ type configuration struct {
 
 // Clone shallow copies the configuration. Your implementation may require a deep copy if
 // your configuration has reference types.
-func (c *configuration) Clone() *configuration {
+func (c *Configuration) Clone() *Configuration {
 	var clone = *c
 	return &clone
 }
 
 // IsValid checks if all needed fields are set.
-func (c *configuration) IsValid() error {
+func (c *Configuration) IsValid() error {
 	if c.GitHubOAuthClientID == "" {
 		return errors.New("must have a github oauth client id")
 	}
@@ -56,12 +56,12 @@ func (c *configuration) IsValid() error {
 // getConfiguration retrieves the active configuration under lock, making it safe to use
 // concurrently. The active configuration may change underneath the client of this method, but
 // the struct returned by this API call is considered immutable.
-func (p *Plugin) getConfiguration() *configuration {
+func (p *Plugin) getConfiguration() *Configuration {
 	p.configurationLock.RLock()
 	defer p.configurationLock.RUnlock()
 
 	if p.configuration == nil {
-		return &configuration{}
+		return &Configuration{}
 	}
 
 	return p.configuration
@@ -76,7 +76,7 @@ func (p *Plugin) getConfiguration() *configuration {
 // This method panics if setConfiguration is called with the existing configuration. This almost
 // certainly means that the configuration was modified without being cloned and may result in
 // an unsafe access.
-func (p *Plugin) setConfiguration(configuration *configuration) {
+func (p *Plugin) setConfiguration(configuration *Configuration) {
 	p.configurationLock.Lock()
 	defer p.configurationLock.Unlock()
 
@@ -96,7 +96,7 @@ func (p *Plugin) setConfiguration(configuration *configuration) {
 
 // OnConfigurationChange is invoked when configuration changes may have been made.
 func (p *Plugin) OnConfigurationChange() error {
-	var configuration = new(configuration)
+	var configuration = new(Configuration)
 
 	// Load the public configuration fields from the Mattermost server configuration.
 	if err := p.API.LoadPluginConfiguration(configuration); err != nil {
diff --git a/server/manifest.go b/server/plugin/manifest.go
similarity index 78%
rename from server/manifest.go
rename to server/plugin/manifest.go
index 60a40d6..4ccf204 100644
--- a/server/manifest.go
+++ b/server/plugin/manifest.go
@@ -1,8 +1,8 @@
 // This file is automatically generated. Do not modify it manually.
 
-package main
+package plugin
 
-var manifest = struct {
+var Manifest = struct {
 	ID      string
 	Version string
 }{
diff --git a/server/permalinks.go b/server/plugin/permalinks.go
similarity index 99%
rename from server/permalinks.go
rename to server/plugin/permalinks.go
index 98a502b..063a8f8 100644
--- a/server/permalinks.go
+++ b/server/plugin/permalinks.go
@@ -1,4 +1,4 @@
-package main
+package plugin
 
 import (
 	"context"
diff --git a/server/plugin.go b/server/plugin/plugin.go
similarity index 97%
rename from server/plugin.go
rename to server/plugin/plugin.go
index 103c3c4..9c8bc42 100644
--- a/server/plugin.go
+++ b/server/plugin/plugin.go
@@ -1,4 +1,4 @@
-package main
+package plugin
 
 import (
 	"context"
@@ -54,7 +54,7 @@ type Plugin struct {
 
 	// configuration is the active plugin configuration. Consult getConfiguration and
 	// setConfiguration for usage.
-	configuration *configuration
+	configuration *Configuration
 
 	router *mux.Router
 }
@@ -82,11 +82,21 @@ func NewPlugin() *Plugin {
 func (p *Plugin) githubConnect(token oauth2.Token) *github.Client {
 	config := p.getConfiguration()
 
+	client, err := GetGitHubClient(token, config)
+	if err != nil {
+		p.API.LogError("failed to create GitHub client", "error", err.Error())
+		return nil
+	}
+
+	return client
+}
+
+func GetGitHubClient(token oauth2.Token, config *Configuration) (*github.Client, error) {
 	ts := oauth2.StaticTokenSource(&token)
 	tc := oauth2.NewClient(context.Background(), ts)
 
-	if len(config.EnterpriseBaseURL) == 0 || len(config.EnterpriseUploadURL) == 0 {
-		return github.NewClient(tc)
+	if config.EnterpriseBaseURL == "" || config.EnterpriseUploadURL == "" {
+		return github.NewClient(tc), nil
 	}
 
 	baseURL, _ := url.Parse(config.EnterpriseBaseURL)
@@ -97,10 +107,10 @@ func (p *Plugin) githubConnect(token oauth2.Token) *github.Client {
 
 	client, err := github.NewEnterpriseClient(baseURL.String(), uploadURL.String(), tc)
 	if err != nil {
-		mlog.Error(err.Error())
-		return github.NewClient(tc)
+		return nil, err
 	}
-	return client
+
+	return client, nil
 }
 
 func (p *Plugin) OnActivate() error {
diff --git a/server/subscriptions.go b/server/plugin/subscriptions.go
similarity index 99%
rename from server/subscriptions.go
rename to server/plugin/subscriptions.go
index aec098e..b7f0777 100644
--- a/server/subscriptions.go
+++ b/server/plugin/subscriptions.go
@@ -1,4 +1,4 @@
-package main
+package plugin
 
 import (
 	"bytes"
diff --git a/server/template.go b/server/plugin/template.go
similarity index 99%
rename from server/template.go
rename to server/plugin/template.go
index b02c54f..788f4e6 100644
--- a/server/template.go
+++ b/server/plugin/template.go
@@ -1,4 +1,4 @@
-package main
+package plugin
 
 import (
 	"bytes"
diff --git a/server/utils.go b/server/plugin/utils.go
similarity index 99%
rename from server/utils.go
rename to server/plugin/utils.go
index bf68289..ca40843 100644
--- a/server/utils.go
+++ b/server/plugin/utils.go
@@ -1,4 +1,4 @@
-package main
+package plugin
 
 import (
 	"bufio"
diff --git a/server/webhook.go b/server/plugin/webhook.go
similarity index 99%
rename from server/webhook.go
rename to server/plugin/webhook.go
index 3754422..a47805f 100644
--- a/server/webhook.go
+++ b/server/plugin/webhook.go
@@ -1,4 +1,4 @@
-package main
+package plugin
 
 import (
 	"context"


Test patch:
diff --git a/server/api_test.go b/server/api_test.go
deleted file mode 100644
index cc96177..0000000
--- a/server/api_test.go
+++ /dev/null
@@ -1,87 +0,0 @@
-package main
-
-import (
-	"net/http"
-	"net/http/httptest"
-	"testing"
-
-	"github.com/mattermost/mattermost-server/v5/plugin/plugintest"
-
-	"github.com/mattermost/mattermost-plugin-github/server/testutils"
-)
-
-func TestPlugin_ServeHTTP(t *testing.T) {
-	httpTestJSON := testutils.HTTPTest{
-		T:       t,
-		Encoder: testutils.EncodeJSON,
-	}
-
-	httpTestString := testutils.HTTPTest{
-		T:       t,
-		Encoder: testutils.EncodeString,
-	}
-
-	tests := []struct {
-		name             string
-		httpTest         testutils.HTTPTest
-		request          testutils.Request
-		expectedResponse testutils.ExpectedResponse
-		userID           string
-	}{
-		{
-			name:     "unauthorized test json",
-			httpTest: httpTestJSON,
-			request: testutils.Request{
-				Method: http.MethodPost,
-				URL:    "/api/v1/todo",
-				Body:   nil,
-			},
-			expectedResponse: testutils.ExpectedResponse{
-				StatusCode:   http.StatusUnauthorized,
-				ResponseType: testutils.ContentTypeJSON,
-				Body:         APIErrorResponse{ID: "", Message: "Not authorized.", StatusCode: http.StatusUnauthorized},
-			},
-			userID: "",
-		},
-		{
-			name:     "unauthorized test http",
-			httpTest: httpTestString,
-			request: testutils.Request{
-				Method: http.MethodGet,
-				URL:    "/api/v1/reviews",
-				Body:   nil,
-			},
-			expectedResponse: testutils.ExpectedResponse{
-				StatusCode:   http.StatusUnauthorized,
-				ResponseType: testutils.ContentTypePlain,
-				Body:         "Not authorized\n",
-			},
-			userID: "",
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			p := NewPlugin()
-			p.setConfiguration(
-				&configuration{
-					GitHubOrg:               "mockOrg",
-					GitHubOAuthClientID:     "mockID",
-					GitHubOAuthClientSecret: "mockSecret",
-					WebhookSecret:           "",
-					EnablePrivateRepo:       false,
-					EncryptionKey:           "mockKey",
-					EnterpriseBaseURL:       "",
-					EnterpriseUploadURL:     "",
-					EnableCodePreview:       false,
-				})
-			p.initializeAPI()
-			p.SetAPI(&plugintest.API{})
-
-			req := tt.httpTest.CreateHTTPRequest(tt.request)
-			req.Header.Add("Mattermost-User-ID", tt.userID)
-			rr := httptest.NewRecorder()
-			p.ServeHTTP(nil, rr, req)
-			tt.httpTest.CompareHTTPResponse(rr, tt.expectedResponse)
-		})
-	}
-}
diff --git a/server/client/client_test.go b/server/client/client_test.go
new file mode 100644
index 0000000..d888861
--- /dev/null
+++ b/server/client/client_test.go
@@ -0,0 +1,115 @@
+package client
+
+import (
+	"bytes"
+	"encoding/json"
+	"io/ioutil"
+	"net/http"
+	"testing"
+
+	"github.com/mattermost/mattermost-server/v5/plugin/plugintest"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+	"github.com/stretchr/testify/require"
+	"golang.org/x/oauth2"
+
+	"github.com/mattermost/mattermost-plugin-github/server/plugin"
+)
+
+func TestRoundTripper(t *testing.T) {
+	t.Run("Valid response", func(t *testing.T) {
+		pluginAPI := &plugintest.API{}
+		pluginAPI.On("PluginHTTP", mock.AnythingOfType("*http.Request")).Return(&http.Response{StatusCode: http.StatusOK})
+
+		roundTripper := pluginAPIRoundTripper{api: pluginAPI}
+		req, err := http.NewRequest(http.MethodPost, "url", nil)
+		require.NoError(t, err)
+
+		resp, err := roundTripper.RoundTrip(req)
+		require.NoError(t, err)
+		require.Equal(t, http.StatusOK, resp.StatusCode)
+	})
+
+	t.Run("Empty response", func(t *testing.T) {
+		pluginAPI := &plugintest.API{}
+		pluginAPI.On("PluginHTTP", mock.AnythingOfType("*http.Request")).Return(nil)
+		defer pluginAPI.AssertExpectations(t)
+
+		roundTripper := pluginAPIRoundTripper{api: pluginAPI}
+		req, err := http.NewRequest(http.MethodPost, "url", nil)
+		require.NoError(t, err)
+
+		resp, err := roundTripper.RoundTrip(req)
+		require.Nil(t, resp)
+		require.Error(t, err)
+	})
+}
+
+func TestGetConfiguration(t *testing.T) {
+	t.Run("Valid", func(t *testing.T) {
+		config := &plugin.Configuration{
+			EnterpriseBaseURL: "http://example.org",
+			GitHubOrg:         "someOrg",
+		}
+
+		b := new(bytes.Buffer)
+		err := json.NewEncoder(b).Encode(config)
+		require.NoError(t, err)
+
+		pluginAPI := &plugintest.API{}
+		pluginAPI.On("PluginHTTP", mock.AnythingOfType("*http.Request")).Return(&http.Response{StatusCode: http.StatusOK, Body: ioutil.NopCloser(b)})
+		defer pluginAPI.AssertExpectations(t)
+
+		client := NewPluginClient(pluginAPI)
+
+		rConfig, err := client.GetConfiguration()
+		assert.NoError(t, err)
+		assert.Equal(t, config, rConfig)
+	})
+
+	t.Run("Error", func(t *testing.T) {
+		pluginAPI := &plugintest.API{}
+		pluginAPI.On("PluginHTTP", mock.AnythingOfType("*http.Request")).Return(nil)
+		defer pluginAPI.AssertExpectations(t)
+
+		client := NewPluginClient(pluginAPI)
+
+		config, err := client.GetConfiguration()
+		assert.Error(t, err)
+		assert.Nil(t, config)
+	})
+}
+
+func TestGetToken(t *testing.T) {
+	t.Run("Valid", func(t *testing.T) {
+		token := &oauth2.Token{
+			AccessToken: "abcdef",
+		}
+
+		b := new(bytes.Buffer)
+		err := json.NewEncoder(b).Encode(token)
+		require.NoError(t, err)
+
+		pluginAPI := &plugintest.API{}
+		pluginAPI.On("PluginHTTP", mock.AnythingOfType("*http.Request")).Return(&http.Response{StatusCode: http.StatusOK, Body: ioutil.NopCloser(b)})
+		defer pluginAPI.AssertExpectations(t)
+
+		client := NewPluginClient(pluginAPI)
+
+		rToken, err := client.GetToken("someUserID")
+		assert.NoError(t, err)
+		assert.Equal(t, token, rToken)
+	})
+
+	t.Run("Error", func(t *testing.T) {
+		pluginAPI := &plugintest.API{}
+		pluginAPI.On("PluginHTTP", mock.AnythingOfType("*http.Request")).Return(nil)
+		defer pluginAPI.AssertExpectations(t)
+
+		client := NewPluginClient(pluginAPI)
+
+		token, err := client.GetToken("someUserID")
+		assert.Error(t, err)
+		assert.Nil(t, token)
+	})
+}
diff --git a/server/plugin/api_test.go b/server/plugin/api_test.go
new file mode 100644
index 0000000..b15a1a9
--- /dev/null
+++ b/server/plugin/api_test.go
@@ -0,0 +1,201 @@
+package plugin
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/mattermost/mattermost-server/v5/plugin"
+	"github.com/mattermost/mattermost-server/v5/plugin/plugintest"
+
+	"github.com/mattermost/mattermost-plugin-github/server/testutils"
+)
+
+func TestPlugin_ServeHTTP(t *testing.T) {
+	httpTestJSON := testutils.HTTPTest{
+		T:       t,
+		Encoder: testutils.EncodeJSON,
+	}
+
+	httpTestString := testutils.HTTPTest{
+		T:       t,
+		Encoder: testutils.EncodeString,
+	}
+
+	for name, test := range map[string]struct {
+		httpTest         testutils.HTTPTest
+		request          testutils.Request
+		expectedResponse testutils.ExpectedResponse
+		userID           string
+	}{
+		"unauthorized test json": {
+			httpTest: httpTestJSON,
+			request: testutils.Request{
+				Method: http.MethodPost,
+				URL:    "/api/v1/todo",
+				Body:   nil,
+			},
+			expectedResponse: testutils.ExpectedResponse{
+				StatusCode:   http.StatusUnauthorized,
+				ResponseType: testutils.ContentTypeJSON,
+				Body:         APIErrorResponse{ID: "", Message: "Not authorized.", StatusCode: http.StatusUnauthorized},
+			},
+			userID: "",
+		}, "unauthorized test http": {
+			httpTest: httpTestString,
+			request: testutils.Request{
+				Method: http.MethodGet,
+				URL:    "/api/v1/reviews",
+				Body:   nil,
+			},
+			expectedResponse: testutils.ExpectedResponse{
+				StatusCode:   http.StatusUnauthorized,
+				ResponseType: testutils.ContentTypePlain,
+				Body:         "Not authorized\n",
+			},
+			userID: "",
+		},
+	} {
+		t.Run(name, func(t *testing.T) {
+			p := NewPlugin()
+			p.setConfiguration(
+				&Configuration{
+					GitHubOrg:               "mockOrg",
+					GitHubOAuthClientID:     "mockID",
+					GitHubOAuthClientSecret: "mockSecret",
+					WebhookSecret:           "",
+					EnablePrivateRepo:       false,
+					EncryptionKey:           "mockKey",
+					EnterpriseBaseURL:       "",
+					EnterpriseUploadURL:     "",
+					EnableCodePreview:       false,
+				})
+			p.initializeAPI()
+			p.SetAPI(&plugintest.API{})
+
+			req := test.httpTest.CreateHTTPRequest(test.request)
+			req.Header.Add("Mattermost-User-ID", test.userID)
+			rr := httptest.NewRecorder()
+			p.ServeHTTP(&plugin.Context{}, rr, req)
+			test.httpTest.CompareHTTPResponse(rr, test.expectedResponse)
+		})
+	}
+}
+
+func TestGetToken(t *testing.T) {
+	httpTestString := testutils.HTTPTest{
+		T:       t,
+		Encoder: testutils.EncodeString,
+	}
+
+	for name, test := range map[string]struct {
+		httpTest         testutils.HTTPTest
+		request          testutils.Request
+		context          *plugin.Context
+		expectedResponse testutils.ExpectedResponse
+	}{
+		"not authorized": {
+			httpTest: httpTestString,
+			request: testutils.Request{
+				Method: http.MethodGet,
+				URL:    "/api/v1/token",
+				Body:   nil,
+			},
+			context: &plugin.Context{},
+			expectedResponse: testutils.ExpectedResponse{
+				StatusCode:   http.StatusUnauthorized,
+				ResponseType: testutils.ContentTypePlain,
+				Body:         "Not authorized\n",
+			},
+		},
+	} {
+		t.Run(name, func(t *testing.T) {
+			p := NewPlugin()
+			p.setConfiguration(
+				&Configuration{
+					GitHubOrg:               "mockOrg",
+					GitHubOAuthClientID:     "mockID",
+					GitHubOAuthClientSecret: "mockSecret",
+					EncryptionKey:           "mockKey",
+				})
+			p.initializeAPI()
+
+			p.SetAPI(&plugintest.API{})
+
+			req := test.httpTest.CreateHTTPRequest(test.request)
+			rr := httptest.NewRecorder()
+
+			p.ServeHTTP(test.context, rr, req)
+
+			test.httpTest.CompareHTTPResponse(rr, test.expectedResponse)
+		})
+	}
+}
+func TestGetConfig(t *testing.T) {
+	httpTestJSON := testutils.HTTPTest{
+		T:       t,
+		Encoder: testutils.EncodeJSON,
+	}
+
+	httpTestString := testutils.HTTPTest{
+		T:       t,
+		Encoder: testutils.EncodeString,
+	}
+
+	config := &Configuration{
+		GitHubOrg:               "mockOrg",
+		GitHubOAuthClientID:     "mockID",
+		GitHubOAuthClientSecret: "mockSecret",
+		EncryptionKey:           "mockKey",
+	}
+
+	for name, test := range map[string]struct {
+		httpTest         testutils.HTTPTest
+		request          testutils.Request
+		context          *plugin.Context
+		expectedResponse testutils.ExpectedResponse
+	}{
+		"not authorized": {
+			httpTest: httpTestString,
+			request: testutils.Request{
+				Method: http.MethodGet,
+				URL:    "/api/v1/config",
+				Body:   nil,
+			},
+			context: &plugin.Context{},
+			expectedResponse: testutils.ExpectedResponse{
+				StatusCode:   http.StatusUnauthorized,
+				ResponseType: testutils.ContentTypePlain,
+				Body:         "Not authorized\n",
+			},
+		}, "authorized": {
+			httpTest: httpTestJSON,
+			request: testutils.Request{
+				Method: http.MethodGet,
+				URL:    "/api/v1/config",
+				Body:   nil,
+			},
+			context: &plugin.Context{SourcePluginId: "somePluginId"},
+			expectedResponse: testutils.ExpectedResponse{
+				StatusCode:   http.StatusOK,
+				ResponseType: testutils.ContentTypeJSON,
+				Body:         config,
+			},
+		},
+	} {
+		t.Run(name, func(t *testing.T) {
+			p := NewPlugin()
+			p.setConfiguration(config)
+			p.initializeAPI()
+
+			p.SetAPI(&plugintest.API{})
+
+			req := test.httpTest.CreateHTTPRequest(test.request)
+			rr := httptest.NewRecorder()
+
+			p.ServeHTTP(test.context, rr, req)
+
+			test.httpTest.CompareHTTPResponse(rr, test.expectedResponse)
+		})
+	}
+}
diff --git a/server/command_test.go b/server/plugin/command_test.go
similarity index 99%
rename from server/command_test.go
rename to server/plugin/command_test.go
index 148da4a..d4791e7 100644
--- a/server/command_test.go
+++ b/server/plugin/command_test.go
@@ -1,4 +1,4 @@
-package main
+package plugin
 
 import (
 	"fmt"
diff --git a/server/permalinks_test.go b/server/plugin/permalinks_test.go
similarity index 99%
rename from server/permalinks_test.go
rename to server/plugin/permalinks_test.go
index 3d4f325..0ba7c3e 100644
--- a/server/permalinks_test.go
+++ b/server/plugin/permalinks_test.go
@@ -1,4 +1,4 @@
-package main
+package plugin
 
 import (
 	"fmt"
diff --git a/server/subscriptions_test.go b/server/plugin/subscriptions_test.go
similarity index 99%
rename from server/subscriptions_test.go
rename to server/plugin/subscriptions_test.go
index 094d99d..4b7ff3c 100644
--- a/server/subscriptions_test.go
+++ b/server/plugin/subscriptions_test.go
@@ -1,4 +1,4 @@
-package main
+package plugin
 
 import (
 	"encoding/json"
diff --git a/server/template_test.go b/server/plugin/template_test.go
similarity index 99%
rename from server/template_test.go
rename to server/plugin/template_test.go
index dad5a75..62da274 100644
--- a/server/template_test.go
+++ b/server/plugin/template_test.go
@@ -1,4 +1,4 @@
-package main
+package plugin
 
 import (
 	"strings"
diff --git a/server/utils_test.go b/server/plugin/utils_test.go
similarity index 99%
rename from server/utils_test.go
rename to server/plugin/utils_test.go
index be3421c..b6bd976 100644
--- a/server/utils_test.go
+++ b/server/plugin/utils_test.go
@@ -1,4 +1,4 @@
-package main
+package plugin
 
 import (
 	"fmt"
diff --git a/server/testutils/http.go b/server/testutils/http.go
index 4e5141f..5e1f141 100644
--- a/server/testutils/http.go
+++ b/server/testutils/http.go
@@ -82,6 +82,7 @@ func (test *HTTPTest) CreateHTTPRequest(request Request) *http.Request {
 
 	req, err := http.NewRequest(request.Method, request.URL, body)
 	tassert.NoError(err, "Error while creating Request")
+
 	return req
 }