Repository: slack-go/slack (go)

Issue:
Create and configure apps with manifests
### Description
Use manifest's system to create, configure and validate your Slack apps.

### Slack's documentation
- https://api.slack.com/reference/manifests
- https://api.slack.com/methods/apps.manifest.create
- https://api.slack.com/methods/apps.manifest.update
- https://api.slack.com/methods/apps.manifest.delete
- https://api.slack.com/methods/apps.manifest.export
- https://api.slack.com/methods/apps.manifest.validate

Pull request:
Add support for manifests and config tokens
- Add support for `tooling.tokens.rotate` endpoint, and store `configToken` and `configRefreshToken` in the `Client` alongside the other token types
- Add support for manifest endpoints - fixes #1084 

Considering that Slack allows uploading manifests as both JSON and YAML, but the API docs specify that the manifest submitted to this endpoint should be JSON, I added `yaml:` tags to the `Manifest` struct to allow users to marshal their existing YAML manifests.

Patch:
diff --git a/examples/manifests/README.md b/examples/manifests/README.md
new file mode 100644
index 0000000..ba6db23
--- /dev/null
+++ b/examples/manifests/README.md
@@ -0,0 +1,38 @@
+# Manifest examples
+
+This example shows how to interact with the
+new [manifest endpoints](https://api.slack.com/reference/manifests#manifest_apis). These endpoints require a special set
+of tokens called `configuration tokens`. Refer to
+the [relevant documentation](https://api.slack.com/authentication/config-tokens) for how to create these tokens.
+
+For examples on how to use configuration tokens, see the [tokens example](../tokens).
+
+## Usage info
+
+The manifest endpoints allow you to configure your application programmatically instead of manually creating
+a `manifest.yaml` file and uploading it on your Slack application's dashboard.
+
+A manifest should follow a specific structure and has a handful of required fields. These are describe in
+the [manifest documentation](https://api.slack.com/reference/manifests#fields), but Slack additionally returns very
+informative error messages for malformed templates to help you pin down what the issue is. The library itself does not
+attempt to perform any form of validation on your manifest.
+
+**Note that each configuration token may only be used once before being invalidated. Again refer to the tokens example
+for more information.**
+
+## Available methods
+
+- ``Slack.CreateManifest()``
+- ``Slack.DeleteManifest()``
+- ``Slack.ExportManifest()``
+- ``Slack.UpdateManifest()``
+
+## Example details
+
+The example code here only shows how to _update_ an application using a manifest. The other available methods are either
+identical in usage or trivial to use, so no full example is provided for them.
+
+The example doesn't rotate the configuration tokens after updating the manifest. **You should almost always do this**.
+Your access token is invalidated after sending a request, and rotating your tokens will allow you to make another
+request in the future. This example does not do this explicitly as it would just repeat the tokens example. For sake of
+simplicity, it only focuses on the manifest part.
diff --git a/examples/manifests/manifest.go b/examples/manifests/manifest.go
new file mode 100644
index 0000000..bfcfa9c
--- /dev/null
+++ b/examples/manifests/manifest.go
@@ -0,0 +1,45 @@
+package manifests
+
+import (
+	"fmt"
+	"github.com/slack-go/slack"
+)
+
+// createManifest programmatically creates a Slack app manifest
+func createManifest() *slack.Manifest {
+	return &slack.Manifest{
+		Display: slack.Display{
+			Name: "Your Application",
+		},
+		// ... other configuration here
+	}
+}
+
+func main() {
+	api := slack.New(
+		"YOUR_TOKEN_HERE",
+		// You may choose to provide your access token when creating your Slack client
+		// or when invoking the method calls
+		slack.OptionConfigToken("YOUR_CONFIG_ACCESS_TOKEN_HERE"),
+	)
+
+	// Create a new Manifest object
+	manifest := createManifest()
+
+	// Update your application using the new manifest
+	// You may pass your token as a parameter here as well, if you didn't do it above
+	response, err := api.UpdateManifest(manifest, "", "YOUR_APP_ID_HERE")
+	if err != nil {
+		fmt.Printf("error updating Slack application: %v\n", err)
+		return
+	}
+
+	if !response.Ok {
+		fmt.Printf("unable to update Slack application: %v\n", response.Errors)
+	}
+
+	fmt.Println("successfully updated Slack application")
+
+	// The access token is now invalid, so it should be rotated for future use
+	// Refer to the examples about tokens for more details
+}
diff --git a/examples/tokens/README.md b/examples/tokens/README.md
new file mode 100644
index 0000000..7e8e163
--- /dev/null
+++ b/examples/tokens/README.md
@@ -0,0 +1,10 @@
+# Tokens examples
+
+The refresh token endpoint can be used to update
+your [configuration tokenset](https://api.slack.com/authentication/config-tokens). These tokens may only be used **once
+** before being invalidated, and are only valid for up to **12 hours**.
+
+Once a token has been used, or before it expires, you can use the `RotateTokens()` method to obtain a fresh set to use
+for the next request. Depending on your use-case you may want to store these somewhere for a future run, so they are
+only returned by the method call. If you wish to update the tokens inside the active Slack client, this can be done
+using `UpdateConfigTokens()`.
diff --git a/examples/tokens/tokens.go b/examples/tokens/tokens.go
new file mode 100644
index 0000000..63d46d2
--- /dev/null
+++ b/examples/tokens/tokens.go
@@ -0,0 +1,33 @@
+package tokens
+
+import (
+	"fmt"
+	"github.com/slack-go/slack"
+)
+
+func main() {
+	api := slack.New(
+		"YOUR_TOKEN_HERE",
+		// You may choose to provide your config tokens when creating your Slack client
+		// or when invoking the method calls
+		slack.OptionConfigToken("YOUR_CONFIG_ACCESS_TOKEN_HERE"),
+		slack.OptionConfigRefreshToken("YOUR_REFRESH_TOKEN_HERE"),
+	)
+
+	// Obtain a fresh set of tokens
+	// You may pass your tokens as a parameter here as well, if you didn't do it above
+	freshTokens, err := api.RotateTokens("", "")
+	if err != nil {
+		fmt.Printf("error rotating tokens: %v\n", err)
+		return
+	}
+
+	fmt.Printf("new access token: %s\n", freshTokens.Token)
+	fmt.Printf("new refresh token: %s\n", freshTokens.RefreshToken)
+	fmt.Printf("new tokenset expires at: %d\n", freshTokens.ExpiresAt)
+
+	// Optionally: update the tokens inside the running Slack client
+	// This isn't necessary if you restart the application after storing the tokens elsewhere,
+	// or pass them as parameters to RotateTokens() explicitly
+	api.UpdateConfigTokens(freshTokens)
+}
diff --git a/manifests.go b/manifests.go
new file mode 100644
index 0000000..0041bf2
--- /dev/null
+++ b/manifests.go
@@ -0,0 +1,287 @@
+package slack
+
+import (
+	"context"
+	"encoding/json"
+	"net/url"
+)
+
+// Manifest is an application manifest schema
+type Manifest struct {
+	Metadata    ManifestMetadata `json:"_metadata,omitempty" yaml:"_metadata,omitempty"`
+	Display     Display          `json:"display_information" yaml:"display_information"`
+	Settings    Settings         `json:"settings,omitempty" yaml:"settings,omitempty"`
+	Features    Features         `json:"features,omitempty" yaml:"features,omitempty"`
+	OAuthConfig OAuthConfig      `json:"oauth_config,omitempty" yaml:"oauth_config,omitempty"`
+}
+
+// CreateManifest creates an app from an app manifest
+func (api *Client) CreateManifest(manifest *Manifest, token string) (*ManifestResponse, error) {
+	return api.CreateManifestContext(context.Background(), manifest, token)
+}
+
+// CreateManifestContext creates an app from an app manifest with a custom context
+func (api *Client) CreateManifestContext(ctx context.Context, manifest *Manifest, token string) (*ManifestResponse, error) {
+	if token == "" {
+		token = api.configToken
+	}
+
+	jsonBytes, err := json.Marshal(manifest)
+	if err != nil {
+		return nil, err
+	}
+
+	values := url.Values{
+		"token":    {token},
+		"manifest": {string(jsonBytes)},
+	}
+
+	response := &ManifestResponse{}
+	err = api.postMethod(ctx, "apps.manifest.create", values, response)
+	if err != nil {
+		return nil, err
+	}
+
+	return response, response.Err()
+}
+
+// DeleteManifest permanently deletes an app created through app manifests
+func (api *Client) DeleteManifest(token string, appId string) (*SlackResponse, error) {
+	return api.DeleteManifestContext(context.Background(), token, appId)
+}
+
+// DeleteManifestContext permanently deletes an app created through app manifests with a custom context
+func (api *Client) DeleteManifestContext(ctx context.Context, token string, appId string) (*SlackResponse, error) {
+	if token == "" {
+		token = api.configToken
+	}
+
+	values := url.Values{
+		"token":  {token},
+		"app_id": {appId},
+	}
+
+	response := &SlackResponse{}
+	err := api.postMethod(ctx, "apps.manifest.delete", values, response)
+	if err != nil {
+		return nil, err
+	}
+
+	return response, response.Err()
+}
+
+// ExportManifest exports an app manifest from an existing app
+func (api *Client) ExportManifest(token string, appId string) (*Manifest, error) {
+	return api.ExportManifestContext(context.Background(), token, appId)
+}
+
+// ExportManifestContext exports an app manifest from an existing app with a custom context
+func (api *Client) ExportManifestContext(ctx context.Context, token string, appId string) (*Manifest, error) {
+	if token == "" {
+		token = api.configToken
+	}
+
+	values := url.Values{
+		"token":  {token},
+		"app_id": {appId},
+	}
+
+	response := &ExportManifestResponse{}
+	err := api.postMethod(ctx, "apps.manifest.export", values, response)
+	if err != nil {
+		return nil, err
+	}
+
+	return &response.Manifest, response.Err()
+}
+
+// UpdateManifest updates an app from an app manifest
+func (api *Client) UpdateManifest(manifest *Manifest, token string, appId string) (*UpdateManifestResponse, error) {
+	return api.UpdateManifestContext(context.Background(), manifest, token, appId)
+}
+
+// UpdateManifestContext updates an app from an app manifest with a custom context
+func (api *Client) UpdateManifestContext(ctx context.Context, manifest *Manifest, token string, appId string) (*UpdateManifestResponse, error) {
+	if token == "" {
+		token = api.configToken
+	}
+
+	jsonBytes, err := json.Marshal(manifest)
+	if err != nil {
+		return nil, err
+	}
+
+	values := url.Values{
+		"token":    {token},
+		"app_id":   {appId},
+		"manifest": {string(jsonBytes)},
+	}
+
+	response := &UpdateManifestResponse{}
+	err = api.postMethod(ctx, "apps.manifest.update", values, response)
+	if err != nil {
+		return nil, err
+	}
+
+	return response, response.Err()
+}
+
+// ValidateManifest sends a request to apps.manifest.validate to validate your app manifest
+func (api *Client) ValidateManifest(manifest *Manifest, token string, appId string) (*ManifestResponse, error) {
+	return api.ValidateManifestContext(context.Background(), manifest, token, appId)
+}
+
+// ValidateManifestContext sends a request to apps.manifest.validate to validate your app manifest with a custom context
+func (api *Client) ValidateManifestContext(ctx context.Context, manifest *Manifest, token string, appId string) (*ManifestResponse, error) {
+	if token == "" {
+		token = api.configToken
+	}
+
+	// Marshal manifest into string
+	jsonBytes, err := json.Marshal(manifest)
+	if err != nil {
+		return nil, err
+	}
+
+	values := url.Values{
+		"token":    {token},
+		"manifest": {string(jsonBytes)},
+	}
+
+	if appId != "" {
+		values.Add("app_id", appId)
+	}
+
+	response := &ManifestResponse{}
+	err = api.postMethod(ctx, "apps.manifest.validate", values, response)
+	if err != nil {
+		return nil, err
+	}
+
+	return response, response.Err()
+}
+
+// ManifestMetadata is a group of settings that describe the manifest
+type ManifestMetadata struct {
+	MajorVersion int `json:"major_version,omitempty" yaml:"major_version,omitempty"`
+	MinorVersion int `json:"minor_version,omitempty" yaml:"minor_version,omitempty"`
+}
+
+// Display is a group of settings that describe parts of an app's appearance within Slack
+type Display struct {
+	Name            string `json:"name" yaml:"name"`
+	Description     string `json:"description,omitempty" yaml:"description,omitempty"`
+	LongDescription string `json:"long_description,omitempty" yaml:"long_description,omitempty"`
+	BackgroundColor string `json:"background_color,omitempty" yaml:"background_color,omitempty"`
+}
+
+// Settings is a group of settings corresponding to the Settings section of the app config pages.
+type Settings struct {
+	AllowedIPAddressRanges []string           `json:"allowed_ip_address_ranges,omitempty" yaml:"allowed_ip_address_ranges,omitempty"`
+	EventSubscriptions     EventSubscriptions `json:"event_subscriptions,omitempty" yaml:"event_subscriptions,omitempty"`
+	Interactivity          Interactivity      `json:"interactivity,omitempty" yaml:"interactivity,omitempty"`
+	OrgDeployEnabled       bool               `json:"org_deploy_enabled,omitempty" yaml:"org_deploy_enabled,omitempty"`
+	SocketModeEnabled      bool               `json:"socket_mode_enabled,omitempty" yaml:"socket_mode_enabled,omitempty"`
+}
+
+// EventSubscriptions is a group of settings that describe the Events API configuration
+type EventSubscriptions struct {
+	RequestUrl string   `json:"request_url,omitempty" yaml:"request_url,omitempty"`
+	BotEvents  []string `json:"bot_events,omitempty" yaml:"bot_events,omitempty"`
+	UserEvents []string `json:"user_events,omitempty" yaml:"user_events,omitempty"`
+}
+
+// Interactivity is a group of settings that describe the interactivity configuration
+type Interactivity struct {
+	IsEnabled             bool   `json:"is_enabled" yaml:"is_enabled"`
+	RequestUrl            string `json:"request_url,omitempty" yaml:"request_url,omitempty"`
+	MessageMenuOptionsUrl string `json:"message_menu_options_url,omitempty" yaml:"message_menu_options_url,omitempty"`
+}
+
+// Features is a group of settings corresponding to the Features section of the app config pages
+type Features struct {
+	AppHome       AppHome                `json:"app_home,omitempty" yaml:"app_home,omitempty"`
+	BotUser       BotUser                `json:"bot_user,omitempty" yaml:"bot_user,omitempty"`
+	Shortcuts     []Shortcut             `json:"shortcuts,omitempty" yaml:"shortcuts,omitempty"`
+	SlashCommands []ManifestSlashCommand `json:"slash_commands,omitempty" yaml:"slash_commands,omitempty"`
+	WorkflowSteps []WorkflowStep         `json:"workflow_steps,omitempty" yaml:"workflow_steps,omitempty"`
+}
+
+// AppHome is a group of settings that describe the App Home configuration
+type AppHome struct {
+	HomeTabEnabled             bool `json:"home_tab_enabled,omitempty" yaml:"home_tab_enabled,omitempty"`
+	MessagesTabEnabled         bool `json:"messages_tab_enabled,omitempty" yaml:"messages_tab_enabled,omitempty"`
+	MessagesTabReadOnlyEnabled bool `json:"messages_tab_read_only_enabled,omitempty" yaml:"messages_tab_read_only_enabled,omitempty"`
+}
+
+// BotUser is a group of settings that describe bot user configuration
+type BotUser struct {
+	DisplayName  string `json:"display_name" yaml:"display_name"`
+	AlwaysOnline bool   `json:"always_online,omitempty" yaml:"always_online,omitempty"`
+}
+
+// Shortcut is a group of settings that describes shortcut configuration
+type Shortcut struct {
+	Name        string       `json:"name" yaml:"name"`
+	CallbackID  string       `json:"callback_id" yaml:"callback_id"`
+	Description string       `json:"description" yaml:"description"`
+	Type        ShortcutType `json:"type" yaml:"type"`
+}
+
+// ShortcutType is a new string type for the available types of shortcuts
+type ShortcutType string
+
+const (
+	MessageShortcut ShortcutType = "message"
+	GlobalShortcut  ShortcutType = "global"
+)
+
+// ManifestSlashCommand is a group of settings that describes slash command configuration
+type ManifestSlashCommand struct {
+	Command      string `json:"command" yaml:"command"`
+	Description  string `json:"description" yaml:"description"`
+	ShouldEscape bool   `json:"should_escape,omitempty" yaml:"should_escape,omitempty"`
+	Url          string `json:"url,omitempty" yaml:"url,omitempty"`
+	UsageHint    string `json:"usage_hint,omitempty" yaml:"usage_hint,omitempty"`
+}
+
+// WorkflowStep is a group of settings that describes workflow steps configuration
+type WorkflowStep struct {
+	Name       string `json:"name" yaml:"name"`
+	CallbackID string `json:"callback_id" yaml:"callback_id"`
+}
+
+// OAuthConfig is a group of settings that describe OAuth configuration for the app
+type OAuthConfig struct {
+	RedirectUrls []string    `json:"redirect_urls,omitempty" yaml:"redirect_urls,omitempty"`
+	Scopes       OAuthScopes `json:"scopes,omitempty" yaml:"scopes,omitempty"`
+}
+
+// OAuthScopes is a group of settings that describe permission scopes configuration
+type OAuthScopes struct {
+	Bot  []string `json:"bot,omitempty" yaml:"bot,omitempty"`
+	User []string `json:"user,omitempty" yaml:"user,omitempty"`
+}
+
+// ManifestResponse is the response returned by the API for apps.manifest.x endpoints
+type ManifestResponse struct {
+	Errors []ManifestValidationError `json:"errors,omitempty"`
+	SlackResponse
+}
+
+// ManifestValidationError is an error message returned for invalid manifests
+type ManifestValidationError struct {
+	Message string `json:"message"`
+	Pointer string `json:"pointer"`
+}
+
+type ExportManifestResponse struct {
+	Manifest Manifest `json:"manifest,omitempty"`
+	SlackResponse
+}
+
+type UpdateManifestResponse struct {
+	AppId              string `json:"app_id,omitempty"`
+	PermissionsUpdated bool   `json:"permissions_updated,omitempty"`
+	ManifestResponse
+}
diff --git a/slack.go b/slack.go
index ea3aab6..756106f 100644
--- a/slack.go
+++ b/slack.go
@@ -57,12 +57,14 @@ type authTestResponseFull struct {
 type ParamOption func(*url.Values)
 
 type Client struct {
-	token         string
-	appLevelToken string
-	endpoint      string
-	debug         bool
-	log           ilogger
-	httpclient    httpClient
+	token              string
+	appLevelToken      string
+	configToken        string
+	configRefreshToken string
+	endpoint           string
+	debug              bool
+	log                ilogger
+	httpclient         httpClient
 }
 
 // Option defines an option for a Client
@@ -99,6 +101,16 @@ func OptionAppLevelToken(token string) func(*Client) {
 	return func(c *Client) { c.appLevelToken = token }
 }
 
+// OptionConfigToken sets a configuration token for the client.
+func OptionConfigToken(token string) func(*Client) {
+	return func(c *Client) { c.configToken = token }
+}
+
+// OptionConfigRefreshToken sets a configuration refresh token for the client.
+func OptionConfigRefreshToken(token string) func(*Client) {
+	return func(c *Client) { c.configRefreshToken = token }
+}
+
 // New builds a slack client from the provided token and options.
 func New(token string, options ...Option) *Client {
 	s := &Client{
diff --git a/tokens.go b/tokens.go
new file mode 100644
index 0000000..4b83bee
--- /dev/null
+++ b/tokens.go
@@ -0,0 +1,50 @@
+package slack
+
+import (
+	"context"
+	"net/url"
+)
+
+// RotateTokens exchanges a refresh token for a new app configuration token
+func (api *Client) RotateTokens(configToken string, refreshToken string) (*TokenResponse, error) {
+	return api.RotateTokensContext(context.Background(), configToken, refreshToken)
+}
+
+// RotateTokensContext exchanges a refresh token for a new app configuration token with a custom context
+func (api *Client) RotateTokensContext(ctx context.Context, configToken string, refreshToken string) (*TokenResponse, error) {
+	if configToken == "" {
+		configToken = api.configToken
+	}
+
+	if refreshToken == "" {
+		refreshToken = api.configRefreshToken
+	}
+
+	values := url.Values{
+		"refresh_token": {refreshToken},
+	}
+
+	response := &TokenResponse{}
+	err := api.getMethod(ctx, "tooling.tokens.rotate", configToken, values, response)
+	if err != nil {
+		return nil, err
+	}
+
+	return response, response.Err()
+}
+
+// UpdateConfigTokens replaces the configuration tokens in the client with those returned by the API
+func (api *Client) UpdateConfigTokens(response *TokenResponse) {
+	api.configToken = response.Token
+	api.configRefreshToken = response.RefreshToken
+}
+
+type TokenResponse struct {
+	Token        string `json:"token,omitempty"`
+	RefreshToken string `json:"refresh_token,omitempty"`
+	TeamId       string `json:"team_id,omitempty"`
+	UserId       string `json:"user_id,omitempty"`
+	IssuedAt     uint64 `json:"iat,omitempty"`
+	ExpiresAt    uint64 `json:"exp,omitempty"`
+	SlackResponse
+}


Test patch:
diff --git a/manifests_test.go b/manifests_test.go
new file mode 100644
index 0000000..9133e68
--- /dev/null
+++ b/manifests_test.go
@@ -0,0 +1,149 @@
+package slack
+
+import (
+	"encoding/json"
+	"net/http"
+	"reflect"
+	"testing"
+)
+
+func TestCreateManifest(t *testing.T) {
+	http.HandleFunc("/apps.manifest.create", handleCreateManifest)
+	once.Do(startServer)
+
+	api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
+
+	manif := getTestManifest()
+	resp, err := api.CreateManifest(&manif, "token")
+	if err != nil {
+		t.Errorf("Unexpected error: %v", err)
+		return
+	}
+
+	if !reflect.DeepEqual(resp, getTestManifestResponse()) {
+		t.Fatal(ErrIncorrectResponse)
+	}
+}
+
+func handleCreateManifest(rw http.ResponseWriter, r *http.Request) {
+	rw.Header().Set("Content-Type", "application/json")
+
+	response, _ := json.Marshal(getTestManifestResponse())
+	rw.Write(response)
+}
+
+func TestDeleteManifest(t *testing.T) {
+	http.HandleFunc("/apps.manifest.delete", handleDeleteManifest)
+	expectedResponse := SlackResponse{Ok: true}
+
+	once.Do(startServer)
+	api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
+
+	resp, err := api.DeleteManifest("token", "app id")
+	if err != nil {
+		t.Errorf("Unexpected error: %v", err)
+		return
+	}
+
+	if !reflect.DeepEqual(expectedResponse, *resp) {
+		t.Fatal(ErrIncorrectResponse)
+	}
+}
+
+func handleDeleteManifest(rw http.ResponseWriter, r *http.Request) {
+	rw.Header().Set("Content-Type", "application/json")
+
+	response, _ := json.Marshal(SlackResponse{Ok: true})
+	rw.Write(response)
+}
+
+func TestExportManifest(t *testing.T) {
+	http.HandleFunc("/apps.manifest.export", handleExportManifest)
+	expectedResponse := getTestManifest()
+
+	api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
+
+	resp, err := api.ExportManifest("token", "app id")
+	if err != nil {
+		t.Errorf("Unexpected error: %v", err)
+		return
+	}
+
+	if !reflect.DeepEqual(expectedResponse, *resp) {
+		t.Fatal(ErrIncorrectResponse)
+	}
+}
+
+func handleExportManifest(rw http.ResponseWriter, r *http.Request) {
+	rw.Header().Set("Content-Type", "application/json")
+
+	response, _ := json.Marshal(ExportManifestResponse{Manifest: getTestManifest()})
+	rw.Write(response)
+}
+
+func TestUpdateManifest(t *testing.T) {
+	http.HandleFunc("/apps.manifest.update", handleUpdateManifest)
+	expectedResponse := UpdateManifestResponse{AppId: "app id"}
+
+	api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
+
+	manif := getTestManifest()
+	resp, err := api.UpdateManifest(&manif, "token", "app id")
+	if err != nil {
+		t.Errorf("Unexpected error: %v", err)
+		return
+	}
+
+	if !reflect.DeepEqual(expectedResponse, *resp) {
+		t.Fatal(ErrIncorrectResponse)
+	}
+}
+
+func handleUpdateManifest(rw http.ResponseWriter, r *http.Request) {
+	rw.Header().Set("Content-Type", "application/json")
+
+	response, _ := json.Marshal(UpdateManifestResponse{AppId: "app id"})
+	rw.Write(response)
+}
+
+func TestValidateManifest(t *testing.T) {
+	http.HandleFunc("/apps.manifest.validate", handleValidateManifest)
+	expectedResponse := ManifestResponse{SlackResponse: SlackResponse{Ok: true}}
+
+	api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
+
+	manif := getTestManifest()
+	resp, err := api.ValidateManifest(&manif, "token", "app id")
+	if err != nil {
+		t.Errorf("Unexpected error: %v", err)
+		return
+	}
+
+	if !reflect.DeepEqual(expectedResponse, *resp) {
+		t.Fatal(ErrIncorrectResponse)
+	}
+}
+
+func handleValidateManifest(rw http.ResponseWriter, r *http.Request) {
+	rw.Header().Set("Content-Type", "application/json")
+
+	response, _ := json.Marshal(ManifestResponse{SlackResponse: SlackResponse{Ok: true}})
+	rw.Write(response)
+}
+
+func getTestManifest() Manifest {
+	return Manifest{
+		Display: Display{
+			Name:        "test",
+			Description: "this is a test",
+		},
+	}
+}
+
+func getTestManifestResponse() *ManifestResponse {
+	return &ManifestResponse{
+		SlackResponse: SlackResponse{
+			Ok: true,
+		},
+	}
+}
diff --git a/tokens_test.go b/tokens_test.go
new file mode 100644
index 0000000..6211745
--- /dev/null
+++ b/tokens_test.go
@@ -0,0 +1,45 @@
+package slack
+
+import (
+	"encoding/json"
+	"net/http"
+	"reflect"
+	"testing"
+)
+
+func TestRotateTokens(t *testing.T) {
+	http.HandleFunc("/tooling.tokens.rotate", handleRotateToken)
+	expected := getTestTokenResponse()
+
+	once.Do(startServer)
+	api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
+
+	tok, err := api.RotateTokens("expired-config", "old-refresh")
+	if err != nil {
+		t.Errorf("Unexpected error: %v", err)
+		return
+	}
+
+	if !reflect.DeepEqual(expected, *tok) {
+		t.Fatal(ErrIncorrectResponse)
+	}
+}
+
+func getTestTokenResponse() TokenResponse {
+	return TokenResponse{
+		Token:         "token",
+		RefreshToken:  "refresh",
+		UserId:        "uid",
+		TeamId:        "tid",
+		IssuedAt:      1,
+		ExpiresAt:     1,
+		SlackResponse: SlackResponse{Ok: true},
+	}
+}
+
+func handleRotateToken(rw http.ResponseWriter, r *http.Request) {
+	rw.Header().Set("Content-Type", "application/json")
+
+	response, _ := json.Marshal(getTestTokenResponse())
+	rw.Write(response)
+}