Repository: mailgun/mailgun-go
Pull request #318 (2024-01-08 11:27:02)

DE-1203 subaccounts & X-Mailgun-On-Behalf-Of http header
DE-1201 - added subaccounts CRUD

DE-1201 - added support of X-Mailgun-On-Behalf-Of http header

Purpose: 

Implement operations with subaccounts API. Subaccounts gives an account admin the ability to create a new account (Subaccount) that’s linked to the primary account, but separates users and assets from the primary account.

Primary accounts can make API calls on behalf of their subaccounts, e.g. sending messages, managing mailing lists, etc. This is accomplished by using the X-Mailgun-On-Behalf-Of header, which must contain the subaccount’s account ID.


Patch:
diff --git a/httphelpers.go b/httphelpers.go
index 88121a5..b88f820 100644
--- a/httphelpers.go
+++ b/httphelpers.go
@@ -18,7 +18,7 @@ import (
 	"github.com/pkg/errors"
 )
 
-var validURL = regexp.MustCompile(`/v[2-4].*`)
+var validURL = regexp.MustCompile(`/v[2-5].*`)
 
 type httpRequest struct {
 	URL                string
@@ -364,7 +364,7 @@ func (r *httpRequest) curlString(req *http.Request, p payload) string {
 		parts = append(parts, fmt.Sprintf("-H \"Host: %s\"", req.Host))
 	}
 
-	//parts = append(parts, fmt.Sprintf(" --user '%s:%s'", r.BasicAuthUser, r.BasicAuthPassword))
+	// parts = append(parts, fmt.Sprintf(" --user '%s:%s'", r.BasicAuthUser, r.BasicAuthPassword))
 
 	if p != nil {
 		if p.getContentType() == "application/json" {
diff --git a/mailgun.go b/mailgun.go
index 9ade586..0324f8d 100644
--- a/mailgun.go
+++ b/mailgun.go
@@ -116,6 +116,9 @@ const (
 	listsEndpoint        = "lists"
 	basicAuthUser        = "api"
 	templatesEndpoint    = "templates"
+	accountsEndpoint     = "accounts"
+	subaccountsEndpoint  = "subaccounts"
+	OnBehalfOfHeader     = "X-Mailgun-On-Behalf-Of"
 )
 
 // Mailgun defines the supported subset of the Mailgun API.
@@ -246,6 +249,15 @@ type Mailgun interface {
 	UpdateTemplateVersion(ctx context.Context, templateName string, version *TemplateVersion) error
 	DeleteTemplateVersion(ctx context.Context, templateName, tag string) error
 	ListTemplateVersions(templateName string, opts *ListOptions) *TemplateVersionsIterator
+
+	ListSubaccounts(opts *ListSubaccountsOptions) *SubaccountsIterator
+	CreateSubaccount(ctx context.Context, subaccountName string) (SubaccountResponse, error)
+	SubaccountDetails(ctx context.Context, subaccountId string) (SubaccountResponse, error)
+	EnableSubaccount(ctx context.Context, subaccountId string) (SubaccountResponse, error)
+	DisableSubaccount(ctx context.Context, subaccountId string) (SubaccountResponse, error)
+
+	SetOnBehalfOfSubaccount(subaccountId string)
+	RemoveOnBehalfOfSubaccount()
 }
 
 // MailgunImpl bundles data needed by a large number of methods in order to interact with the Mailgun API.
@@ -317,6 +329,17 @@ func (mg *MailgunImpl) SetClient(c *http.Client) {
 	mg.client = c
 }
 
+// SetOnBehalfOfSubaccount sets X-Mailgun-On-Behalf-Of header to SUBACCOUNT_ACCOUNT_ID in order to perform API request
+// on behalf of subaccount.
+func (mg *MailgunImpl) SetOnBehalfOfSubaccount(subaccountId string) {
+	mg.AddOverrideHeader(OnBehalfOfHeader, subaccountId)
+}
+
+// RemoveOnBehalfOfSubaccount remove X-Mailgun-On-Behalf-Of header for primary usage.
+func (mg *MailgunImpl) RemoveOnBehalfOfSubaccount() {
+	delete(mg.overrideHeaders, OnBehalfOfHeader)
+}
+
 // SetAPIBase updates the API Base URL for this client.
 //  // For EU Customers
 //  mg.SetAPIBase(mailgun.APIBaseEU)
@@ -402,6 +425,10 @@ func generatePublicApiUrl(m Mailgun, endpoint string) string {
 	return fmt.Sprintf("%s/%s", m.APIBase(), endpoint)
 }
 
+func generateSubaccountsApiUrl(m Mailgun) string {
+	return fmt.Sprintf("%s/%s/%s", m.APIBase(), accountsEndpoint, subaccountsEndpoint)
+}
+
 // generateParameterizedUrl works as generateApiUrl, but supports query parameters.
 func generateParameterizedUrl(m Mailgun, endpoint string, payload payload) (string, error) {
 	paramBuffer, err := payload.getPayloadBuffer()
diff --git a/mock.go b/mock.go
index 86e368e..23f0896 100644
--- a/mock.go
+++ b/mock.go
@@ -27,6 +27,7 @@ type MockServer interface {
 	Events() []Event
 	Webhooks() WebHooksListResponse
 	Templates() []Template
+	SubaccountList() []Subaccount
 }
 
 // A mailgun api mock suitable for testing
@@ -47,6 +48,7 @@ type mockServer struct {
 	credentials      []Credential
 	stats            []Stats
 	tags             []Tag
+	subaccountList   []Subaccount
 	webhooks         WebHooksListResponse
 	mutex            sync.Mutex
 }
@@ -105,6 +107,12 @@ func (ms *mockServer) Unsubscribes() []Unsubscribe {
 	return ms.unsubscribes
 }
 
+func (ms *mockServer) SubaccountList() []Subaccount {
+	defer ms.mutex.Unlock()
+	ms.mutex.Lock()
+	return ms.subaccountList
+}
+
 // Create a new instance of the mailgun API mock server
 func NewMockServer() MockServer {
 	ms := mockServer{}
@@ -129,6 +137,7 @@ func NewMockServer() MockServer {
 		ms.addCredentialsRoutes(r)
 		ms.addStatsRoutes(r)
 		ms.addTagsRoutes(r)
+		ms.addSubaccountRoutes(r)
 	})
 	ms.addValidationRoutes(r)
 
diff --git a/mock_subaccounts.go b/mock_subaccounts.go
new file mode 100644
index 0000000..c6141b2
--- /dev/null
+++ b/mock_subaccounts.go
@@ -0,0 +1,127 @@
+package mailgun
+
+import (
+	"net/http"
+
+	"github.com/go-chi/chi/v5"
+)
+
+func (ms *mockServer) addSubaccountRoutes(r chi.Router) {
+	ms.subaccountList = append(ms.subaccountList, Subaccount{
+		Id:     "enabled.subaccount",
+		Name:   "mailgun.test",
+		Status: "enabled",
+	}, Subaccount{
+		Id:     "disabled.subaccount",
+		Name:   "mailgun.test",
+		Status: "disabled",
+	})
+
+	r.Get("/accounts/subaccounts", ms.listSubaccounts)
+	r.Post("/accounts/subaccounts", ms.createSubaccount)
+
+	r.Get("/accounts/subaccounts/{subaccountID}", ms.getSubaccount)
+	r.Post("/accounts/subaccounts/{subaccountID}/enable", ms.enableSubaccount)
+	r.Post("/accounts/subaccounts/{subaccountID}/disable", ms.disableSubaccount)
+}
+
+func (ms *mockServer) listSubaccounts(w http.ResponseWriter, r *http.Request) {
+	defer ms.mutex.Unlock()
+	ms.mutex.Lock()
+
+	var list subaccountsListResponse
+	for _, subaccount := range ms.subaccountList {
+		list.Items = append(list.Items, subaccount)
+	}
+
+	skip := stringToInt(r.FormValue("skip"))
+	limit := stringToInt(r.FormValue("limit"))
+	if limit == 0 {
+		limit = 100
+	}
+
+	if skip > len(list.Items) {
+		skip = len(list.Items)
+	}
+
+	end := limit + skip
+	if end > len(list.Items) {
+		end = len(list.Items)
+	}
+
+	// If we are at the end of the list
+	if skip == end {
+		toJSON(w, subaccountsListResponse{
+			Total: len(list.Items),
+			Items: []Subaccount{},
+		})
+		return
+	}
+
+	toJSON(w, subaccountsListResponse{
+		Total: len(list.Items),
+		Items: list.Items[skip:end],
+	})
+}
+
+func (ms *mockServer) getSubaccount(w http.ResponseWriter, r *http.Request) {
+	defer ms.mutex.Unlock()
+	ms.mutex.Lock()
+
+	for _, s := range ms.subaccountList {
+		if s.Id == chi.URLParam(r, "subaccountID") {
+			toJSON(w, SubaccountResponse{Item: s})
+			return
+		}
+	}
+	w.WriteHeader(http.StatusNotFound)
+	toJSON(w, okResp{Message: "Not Found"})
+}
+
+func (ms *mockServer) createSubaccount(w http.ResponseWriter, r *http.Request) {
+	defer ms.mutex.Unlock()
+	ms.mutex.Lock()
+
+	ms.subaccountList = append(ms.subaccountList, Subaccount{
+		Id:     "test",
+		Name:   r.FormValue("name"),
+		Status: "active",
+	})
+	toJSON(w, okResp{Message: "Subaccount has been created"})
+}
+
+func (ms *mockServer) enableSubaccount(w http.ResponseWriter, r *http.Request) {
+	defer ms.mutex.Unlock()
+	ms.mutex.Lock()
+
+	for _, subaccount := range ms.subaccountList {
+		if subaccount.Id == chi.URLParam(r, "subaccountID") && subaccount.Status == "disabled" {
+			subaccount.Status = "enabled"
+			toJSON(w, SubaccountResponse{Item: subaccount})
+			return
+		}
+		if subaccount.Id == chi.URLParam(r, "subaccountID") && subaccount.Status == "enabled" {
+			toJSON(w, okResp{Message: "subaccount is already enabled"})
+			return
+		}
+	}
+	toJSON(w, okResp{Message: "Not Found"})
+}
+
+func (ms *mockServer) disableSubaccount(w http.ResponseWriter, r *http.Request) {
+	defer ms.mutex.Unlock()
+	ms.mutex.Lock()
+
+	for _, subaccount := range ms.subaccountList {
+		if subaccount.Id == chi.URLParam(r, "subaccountID") && subaccount.Status == "enabled" {
+			subaccount.Status = "disabled"
+			toJSON(w, SubaccountResponse{Item: subaccount})
+			return
+		}
+		if subaccount.Id == chi.URLParam(r, "subaccountID") && subaccount.Status == "disabled" {
+			toJSON(w, okResp{Message: "subaccount is already disabled"})
+			return
+		}
+	}
+	toJSON(w, okResp{Message: "Not Found"})
+}
diff --git a/subaccounts.go b/subaccounts.go
new file mode 100644
index 0000000..b5aac2c
--- /dev/null
+++ b/subaccounts.go
@@ -0,0 +1,246 @@
+package mailgun
+
+import (
+	"context"
+	"strconv"
+)
+
+type ListSubaccountsOptions struct {
+	Limit     int
+	Skip      int
+	SortArray string
+	Enabled   bool
+}
+
+type SubaccountsIterator struct {
+	subaccountsListResponse
+
+	mg        Mailgun
+	limit     int
+	offset    int
+	skip      int
+	sortArray string
+	enabled   bool
+	url       string
+	err       error
+}
+
+// A Subaccount structure holds information about a subaccount.
+type Subaccount struct {
+	Id     string `json:"id"`
+	Name   string `json:"name"`
+	Status string `json:"status"`
+}
+
+type SubaccountResponse struct {
+	Item Subaccount `json:"subaccount"`
+}
+
+type subaccountsListResponse struct {
+	Items []Subaccount `json:"subaccounts"`
+	Total int          `json:"total"`
+}
+
+// ListSubaccounts retrieves a set of subaccount linked to the primary Mailgun account.
+func (mg *MailgunImpl) ListSubaccounts(opts *ListSubaccountsOptions) *SubaccountsIterator {
+	r := newHTTPRequest(generateSubaccountsApiUrl(mg))
+	r.setClient(mg.client)
+	r.setBasicAuth(basicAuthUser, mg.APIKey())
+
+	var limit, skip int
+	var sortArray string
+	var enabled bool
+	if opts != nil {
+		limit = opts.Limit
+		skip = opts.Skip
+		sortArray = opts.SortArray
+		enabled = opts.Enabled
+	}
+	if limit == 0 {
+		limit = 10
+	}
+
+	return &SubaccountsIterator{
+		mg:                      mg,
+		url:                     generateSubaccountsApiUrl(mg),
+		subaccountsListResponse: subaccountsListResponse{Total: -1},
+		limit:                   limit,
+		skip:                    skip,
+		sortArray:               sortArray,
+		enabled:                 enabled,
+	}
+}
+
+// If an error occurred during iteration `Err()` will return non nil
+func (ri *SubaccountsIterator) Err() error {
+	return ri.err
+}
+
+// Offset returns the current offset of the iterator
+func (ri *SubaccountsIterator) Offset() int {
+	return ri.offset
+}
+
+// Next retrieves the next page of items from the api. Returns false when there
+// no more pages to retrieve or if there was an error. Use `.Err()` to retrieve
+// the error
+func (ri *SubaccountsIterator) Next(ctx context.Context, items *[]Subaccount) bool {
+	if ri.err != nil {
+		return false
+	}
+
+	ri.err = ri.fetch(ctx, ri.offset, ri.limit)
+	if ri.err != nil {
+		return false
+	}
+
+	cpy := make([]Subaccount, len(ri.Items))
+	copy(cpy, ri.Items)
+	*items = cpy
+	if len(ri.Items) == 0 {
+		return false
+	}
+	ri.offset = ri.offset + len(ri.Items)
+	return true
+}
+
+// First retrieves the first page of items from the api. Returns false if there
+// was an error. It also sets the iterator object to the first page.
+// Use `.Err()` to retrieve the error.
+func (ri *SubaccountsIterator) First(ctx context.Context, items *[]Subaccount) bool {
+	if ri.err != nil {
+		return false
+	}
+	ri.err = ri.fetch(ctx, 0, ri.limit)
+	if ri.err != nil {
+		return false
+	}
+	cpy := make([]Subaccount, len(ri.Items))
+	copy(cpy, ri.Items)
+	*items = cpy
+	ri.offset = len(ri.Items)
+	return true
+}
+
+// Last retrieves the last page of items from the api.
+// Calling Last() is invalid unless you first call First() or Next()
+// Returns false if there was an error. It also sets the iterator object
+// to the last page. Use `.Err()` to retrieve the error.
+func (ri *SubaccountsIterator) Last(ctx context.Context, items *[]Subaccount) bool {
+	if ri.err != nil {
+		return false
+	}
+
+	if ri.Total == -1 {
+		return false
+	}
+
+	ri.offset = ri.Total - ri.limit
+	if ri.offset < 0 {
+		ri.offset = 0
+	}
+
+	ri.err = ri.fetch(ctx, ri.offset, ri.limit)
+	if ri.err != nil {
+		return false
+	}
+	cpy := make([]Subaccount, len(ri.Items))
+	copy(cpy, ri.Items)
+	*items = cpy
+	return true
+}
+
+// Previous retrieves the previous page of items from the api. Returns false when there
+// no more pages to retrieve or if there was an error. Use `.Err()` to retrieve
+// the error if any
+func (ri *SubaccountsIterator) Previous(ctx context.Context, items *[]Subaccount) bool {
+	if ri.err != nil {
+		return false
+	}
+
+	if ri.Total == -1 {
+		return false
+	}
+
+	ri.offset = ri.offset - (ri.limit * 2)
+	if ri.offset < 0 {
+		ri.offset = 0
+	}
+
+	ri.err = ri.fetch(ctx, ri.offset, ri.limit)
+	if ri.err != nil {
+		return false
+	}
+	cpy := make([]Subaccount, len(ri.Items))
+	copy(cpy, ri.Items)
+	*items = cpy
+	if len(ri.Items) == 0 {
+		return false
+	}
+	return true
+}
+
+func (ri *SubaccountsIterator) fetch(ctx context.Context, skip, limit int) error {
+	ri.Items = nil
+	r := newHTTPRequest(ri.url)
+	r.setBasicAuth(basicAuthUser, ri.mg.APIKey())
+	r.setClient(ri.mg.Client())
+
+	if skip != 0 {
+		r.addParameter("skip", strconv.Itoa(skip))
+	}
+	if limit != 0 {
+		r.addParameter("limit", strconv.Itoa(limit))
+	}
+
+	return getResponseFromJSON(ctx, r, &ri.subaccountsListResponse)
+}
+
+// CreateSubaccount instructs Mailgun to create a new account (Subaccount) that is linked to the primary account.
+// Subaccounts are child accounts that share the same plan and usage allocations as the primary, but have their own
+// assets (sending domains, unique users, API key, SMTP credentials, settings, statistics and site login).
+// All you need is the name of the subaccount.
+func (mg *MailgunImpl) CreateSubaccount(ctx context.Context, subaccountName string) (SubaccountResponse, error) {
+	r := newHTTPRequest(generateSubaccountsApiUrl(mg))
+	r.setClient(mg.client)
+	r.setBasicAuth(basicAuthUser, mg.APIKey())
+
+	payload := newUrlEncodedPayload()
+	payload.addValue("name", subaccountName)
+	resp := SubaccountResponse{}
+	err := postResponseFromJSON(ctx, r, payload, &resp)
+	return resp, err
+}
+
+// SubaccountDetails retrieves detailed information about subaccount using subaccountId.
+func (mg *MailgunImpl) SubaccountDetails(ctx context.Context, subaccountId string) (SubaccountResponse, error) {
+	r := newHTTPRequest(generateSubaccountsApiUrl(mg) + "/" + subaccountId)
+	r.setClient(mg.client)
+	r.setBasicAuth(basicAuthUser, mg.APIKey())
+
+	var resp SubaccountResponse
+	err := getResponseFromJSON(ctx, r, &resp)
+	return resp, err
+}
+
+// EnableSubaccount instructs Mailgun to enable subaccount.
+func (mg *MailgunImpl) EnableSubaccount(ctx context.Context, subaccountId string) (SubaccountResponse, error) {
+	r := newHTTPRequest(generateSubaccountsApiUrl(mg) + "/" + subaccountId + "/" + "enable")
+	r.setClient(mg.client)
+	r.setBasicAuth(basicAuthUser, mg.APIKey())
+
+	resp := SubaccountResponse{}
+	err := postResponseFromJSON(ctx, r, nil, &resp)
+	return resp, err
+}
+
+// DisableSubaccount instructs Mailgun to disable subaccount.
+func (mg *MailgunImpl) DisableSubaccount(ctx context.Context, subaccountId string) (SubaccountResponse, error) {
+	r := newHTTPRequest(generateSubaccountsApiUrl(mg) + "/" + subaccountId + "/" + "disable")
+	r.setClient(mg.client)
+	r.setBasicAuth(basicAuthUser, mg.APIKey())
+
+	resp := SubaccountResponse{}
+	err := postResponseFromJSON(ctx, r, nil, &resp)
+	return resp, err
+}


Test patch:
diff --git a/messages_test.go b/messages_test.go
index 09989e5..cd9fed3 100644
--- a/messages_test.go
+++ b/messages_test.go
@@ -605,6 +605,45 @@ func TestAddOverrideHeader(t *testing.T) {
 	ensure.StringContains(t, mg.GetCurlOutput(), "Host:")
 }
 
+func TestOnBehalfOfSubaccount(t *testing.T) {
+	const (
+		exampleDomain  = "testDomain"
+		exampleAPIKey  = "testAPIKey"
+		toUser         = "test@test.com"
+		exampleMessage = "Queue. Thank you"
+		exampleID      = "<20111114174239.25659.5817@samples.mailgun.org>"
+	)
+	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+		ensure.DeepEqual(t, req.Method, http.MethodPost)
+		ensure.DeepEqual(t, req.URL.Path, fmt.Sprintf("/v3/%s/messages", exampleDomain))
+		ensure.DeepEqual(t, req.Header.Get("CustomHeader"), "custom-value")
+		ensure.DeepEqual(t, req.Host, "example.com")
+		ensure.DeepEqual(t, req.Header.Get(mailgun.OnBehalfOfHeader), "mailgun.subaccount")
+
+		rsp := fmt.Sprintf(`{"message":"%s", "id":"%s"}`, exampleMessage, exampleID)
+		fmt.Fprint(w, rsp)
+	}))
+	defer srv.Close()
+
+	mg := mailgun.NewMailgun(exampleDomain, exampleAPIKey)
+	mg.SetAPIBase(srv.URL + "/v3")
+	mg.AddOverrideHeader("Host", "example.com")
+	mg.AddOverrideHeader("CustomHeader", "custom-value")
+	mg.SetOnBehalfOfSubaccount("mailgun.subaccount")
+	ctx := context.Background()
+
+	m := mg.NewMessage(fromUser, exampleSubject, exampleText, toUser)
+	m.SetRequireTLS(true)
+	m.SetSkipVerification(true)
+
+	msg, id, err := mg.Send(ctx, m)
+	ensure.Nil(t, err)
+	ensure.DeepEqual(t, msg, exampleMessage)
+	ensure.DeepEqual(t, id, exampleID)
+
+	ensure.StringContains(t, mg.GetCurlOutput(), "Host:")
+}
+
 func TestCaptureCurlOutput(t *testing.T) {
 	const (
 		exampleDomain  = "testDomain"
diff --git a/subaccounts_test.go b/subaccounts_test.go
new file mode 100644
index 0000000..864b9fd
--- /dev/null
+++ b/subaccounts_test.go
@@ -0,0 +1,122 @@
+package mailgun_test
+
+import (
+	"context"
+	"net/http"
+	"testing"
+
+	"github.com/facebookgo/ensure"
+	"github.com/mailgun/mailgun-go/v4"
+)
+
+const (
+	testSubaccountName       = "mailgun.test"
+	testEnabledSubaccountId  = "enabled.subaccount"
+	testDisabledSubaccountId = "disabled.subaccount"
+)
+
+func TestListSubaccounts(t *testing.T) {
+	mg := mailgun.NewMailgun(testDomain, testKey)
+	mg.SetAPIBase(server.URL())
+
+	iterator := mg.ListSubaccounts(nil)
+	ensure.NotNil(t, iterator)
+
+	ctx := context.Background()
+
+	var page []mailgun.Subaccount
+	for iterator.Next(ctx, &page) {
+		for _, d := range page {
+			t.Logf("TestListSubaccounts: %#v\n", d)
+		}
+	}
+	t.Logf("TestListSubaccounts: %d subaccounts retrieved\n", iterator.Total)
+	ensure.Nil(t, iterator.Err())
+	ensure.True(t, iterator.Total != 0)
+}
+
+func TestSubaccountDetails(t *testing.T) {
+	mg := mailgun.NewMailgun(testDomain, testKey)
+	mg.SetAPIBase(server.URL())
+
+	ctx := context.Background()
+
+	iterator := mg.ListSubaccounts(nil)
+	ensure.NotNil(t, iterator)
+
+	page := []mailgun.Subaccount{}
+	ensure.True(t, iterator.Next(context.Background(), &page))
+	ensure.Nil(t, iterator.Err())
+
+	resp, err := mg.SubaccountDetails(ctx, page[0].Id)
+	ensure.Nil(t, err)
+	ensure.NotNil(t, resp)
+}
+
+func TestSubaccountDetailsStatusNotFound(t *testing.T) {
+	mg := mailgun.NewMailgun(testDomain, testKey)
+	mg.SetAPIBase(server.URL())
+
+	ctx := context.Background()
+
+	_, err := mg.SubaccountDetails(ctx, "unexisting.id")
+	if err == nil {
+		t.Fatal("Did not expect a subaccount to exist")
+	}
+	ure, ok := err.(*mailgun.UnexpectedResponseError)
+	ensure.True(t, ok)
+	ensure.DeepEqual(t, ure.Actual, http.StatusNotFound)
+}
+
+func TestCreateSubaccount(t *testing.T) {
+	mg := mailgun.NewMailgun(testDomain, testKey)
+	mg.SetAPIBase(server.URL())
+
+	ctx := context.Background()
+
+	resp, err := mg.CreateSubaccount(ctx, testSubaccountName)
+	ensure.Nil(t, err)
+	ensure.NotNil(t, resp)
+}
+
+func TestEnableSubaccountAlreadyEnabled(t *testing.T) {
+	mg := mailgun.NewMailgun(testDomain, testKey)
+	mg.SetAPIBase(server.URL())
+
+	ctx := context.Background()
+
+	_, err := mg.EnableSubaccount(ctx, testEnabledSubaccountId)
+	ensure.Nil(t, err)
+}
+
+func TestEnableSubaccount(t *testing.T) {
+	mg := mailgun.NewMailgun(testDomain, testKey)
+	mg.SetAPIBase(server.URL())
+
+	ctx := context.Background()
+
+	resp, err := mg.EnableSubaccount(ctx, testDisabledSubaccountId)
+	ensure.Nil(t, err)
+	ensure.DeepEqual(t, resp.Item.Status, "enabled")
+}
+
+func TestDisableSubaccount(t *testing.T) {
+	mg := mailgun.NewMailgun(testDomain, testKey)
+	mg.SetAPIBase(server.URL())
+
+	ctx := context.Background()
+
+	resp, err := mg.DisableSubaccount(ctx, testEnabledSubaccountId)
+	ensure.Nil(t, err)
+	ensure.DeepEqual(t, resp.Item.Status, "disabled")
+}
+
+func TestDisableSubaccountAlreadyDisabled(t *testing.T) {
+	mg := mailgun.NewMailgun(testDomain, testKey)
+	mg.SetAPIBase(server.URL())
+
+	ctx := context.Background()
+
+	_, err := mg.DisableSubaccount(ctx, testDisabledSubaccountId)
+	ensure.Nil(t, err)
+}