Skip to content

Commit 2b52f7e

Browse files
committed
Fix API compatibility issues and add comprehensive tests
- Fix auth header to use Api-Token instead of Authorization Bearer - Fix request/response structures to match Mailtrap API docs: tokens (permissions), contacts (wrapped response, fields), contact-lists/fields (flat body), domains (sending_domain wrapper), suppressions (string ID), account-access (nested specifier), email-logs (cursor pagination), stats (sending_domain_ids), organizations (account wrapper), messages (headers JSON, source endpoint) - Add domains send-setup-instructions command - Add comprehensive tests for all commands (197 total tests) covering read/write operations, request validation, and error cases
1 parent 484c64c commit 2b52f7e

53 files changed

Lines changed: 4905 additions & 147 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ mailtrap messages spam-score --sandbox-id 12345 --id 67890
103103
# Domains
104104
mailtrap domains list
105105
mailtrap domains create --name "yourdomain.com"
106+
mailtrap domains send-setup-instructions --id 123 --email "admin@yourdomain.com"
106107

107108
# Templates
108109
mailtrap templates list
@@ -111,6 +112,7 @@ mailtrap templates create --name "Welcome" --subject "Hello {{name}}" --body-htm
111112
# Contacts
112113
mailtrap contacts create --email "user@example.com" --first-name "John"
113114
mailtrap contact-lists list
115+
mailtrap contact-fields create --name "Company" --data-type text
114116

115117
# Sandboxes & projects
116118
mailtrap projects list
@@ -136,7 +138,7 @@ mailtrap domains list --output text
136138
|-------|----------|
137139
| **Sending** | `send transactional`, `send bulk`, `send batch-transactional`, `send batch-bulk` |
138140
| **Sandbox Send** | `sandbox-send single`, `sandbox-send batch` |
139-
| **Domains** | `domains list`, `domains get`, `domains create`, `domains delete` |
141+
| **Domains** | `domains list`, `domains get`, `domains create`, `domains delete`, `domains send-setup-instructions` |
140142
| **Templates** | `templates list`, `templates get`, `templates create`, `templates update`, `templates delete` |
141143
| **Suppressions** | `suppressions list`, `suppressions delete` |
142144
| **Stats** | `stats get`, `stats by-domain`, `stats by-category`, `stats by-esp`, `stats by-date` |

internal/client/client.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ func (c *Client) do(ctx context.Context, base BaseURL, method, path string, quer
102102
}
103103

104104
func (c *Client) setAuthHeader(req *http.Request, base BaseURL) {
105-
req.Header.Set("Authorization", "Bearer "+c.apiToken)
105+
req.Header.Set("Api-Token", c.apiToken)
106106
}
107107

108108
func (c *Client) Get(ctx context.Context, base BaseURL, path string, query url.Values, result interface{}) error {

internal/client/client_test.go

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -413,16 +413,15 @@ func TestGetRaw_Error(t *testing.T) {
413413
}
414414
}
415415

416-
// --- 12. Auth header is set correctly (Authorization: Bearer) ---
416+
// --- 12. Auth header is set correctly (Api-Token) ---
417417

418418
func TestAuthHeader(t *testing.T) {
419419
const token = "secret-token-12345"
420420

421421
_, c, base := setupTestServer(t, func(w http.ResponseWriter, r *http.Request) {
422-
expected := "Bearer " + token
423-
got := r.Header.Get("Authorization")
424-
if got != expected {
425-
t.Errorf("expected Authorization header %q, got %q", expected, got)
422+
got := r.Header.Get("Api-Token")
423+
if got != token {
424+
t.Errorf("expected Api-Token header %q, got %q", token, got)
426425
}
427426
w.WriteHeader(http.StatusOK)
428427
})
@@ -438,10 +437,9 @@ func TestAuthHeader_GetRaw(t *testing.T) {
438437
const token = "raw-token-xyz"
439438

440439
_, c, base := setupTestServer(t, func(w http.ResponseWriter, r *http.Request) {
441-
expected := "Bearer " + token
442-
got := r.Header.Get("Authorization")
443-
if got != expected {
444-
t.Errorf("expected Authorization header %q, got %q", expected, got)
440+
got := r.Header.Get("Api-Token")
441+
if got != token {
442+
t.Errorf("expected Api-Token header %q, got %q", token, got)
445443
}
446444
w.WriteHeader(http.StatusOK)
447445
w.Write([]byte("ok"))
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
package account_access_test
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"net/http"
7+
"net/http/httptest"
8+
"strings"
9+
"testing"
10+
11+
"github.com/mailtrap/mailtrap-cli/internal/client"
12+
"github.com/mailtrap/mailtrap-cli/internal/cmdutil"
13+
"github.com/mailtrap/mailtrap-cli/internal/commands/account_access"
14+
"github.com/mailtrap/mailtrap-cli/internal/config"
15+
"github.com/spf13/viper"
16+
)
17+
18+
func setupTest(handler http.HandlerFunc) (*cmdutil.Factory, *bytes.Buffer, func()) {
19+
server := httptest.NewServer(handler)
20+
c := client.New("test-token")
21+
c.SetBaseURL(client.BaseGeneral, server.URL)
22+
buf := &bytes.Buffer{}
23+
f := &cmdutil.Factory{
24+
Config: func() *config.Config {
25+
return &config.Config{APIToken: "test-token", AccountID: "123"}
26+
},
27+
IOStreams: &cmdutil.IOStreams{
28+
Out: buf,
29+
ErrOut: &bytes.Buffer{},
30+
},
31+
ClientOverride: c,
32+
}
33+
viper.Set("api-token", "test-token")
34+
viper.Set("account-id", "123")
35+
viper.Set("output", "table")
36+
return f, buf, func() {
37+
server.Close()
38+
viper.Reset()
39+
}
40+
}
41+
42+
func TestAccountAccessList(t *testing.T) {
43+
f, buf, cleanup := setupTest(func(w http.ResponseWriter, r *http.Request) {
44+
if r.Method != http.MethodGet {
45+
t.Errorf("expected GET, got %s", r.Method)
46+
}
47+
if !strings.Contains(r.URL.Path, "/api/accounts/123/account_accesses") {
48+
t.Errorf("unexpected path: %s", r.URL.Path)
49+
}
50+
w.Header().Set("Content-Type", "application/json")
51+
json.NewEncoder(w).Encode([]map[string]interface{}{
52+
{
53+
"id": 1,
54+
"specifier_type": "User",
55+
"specifier": map[string]interface{}{
56+
"id": 10,
57+
"email": "user@test.com",
58+
"name": "Test User",
59+
},
60+
"resources": []map[string]interface{}{
61+
{
62+
"resource_id": 123,
63+
"resource_type": "account",
64+
"access_level": 100,
65+
},
66+
},
67+
},
68+
})
69+
})
70+
defer cleanup()
71+
72+
cmd := account_access.NewCmdAccountAccess(f)
73+
cmd.SetArgs([]string{"list"})
74+
cmd.SetOut(buf)
75+
76+
if err := cmd.Execute(); err != nil {
77+
t.Fatalf("unexpected error: %v", err)
78+
}
79+
80+
output := buf.String()
81+
if !strings.Contains(output, "user@test.com") {
82+
t.Errorf("expected output to contain 'user@test.com', got:\n%s", output)
83+
}
84+
if !strings.Contains(output, "Test User") {
85+
t.Errorf("expected output to contain 'Test User', got:\n%s", output)
86+
}
87+
if !strings.Contains(output, "account") {
88+
t.Errorf("expected output to contain 'account', got:\n%s", output)
89+
}
90+
}
91+
92+
func TestAccountAccessListJSON(t *testing.T) {
93+
f, buf, cleanup := setupTest(func(w http.ResponseWriter, r *http.Request) {
94+
w.Header().Set("Content-Type", "application/json")
95+
json.NewEncoder(w).Encode([]map[string]interface{}{
96+
{
97+
"id": 1,
98+
"specifier_type": "User",
99+
"specifier": map[string]interface{}{
100+
"id": 10,
101+
"email": "user@test.com",
102+
"name": "Test User",
103+
},
104+
"resources": []map[string]interface{}{
105+
{
106+
"resource_id": 123,
107+
"resource_type": "account",
108+
"access_level": 100,
109+
},
110+
},
111+
},
112+
})
113+
})
114+
defer cleanup()
115+
116+
viper.Set("output", "json")
117+
118+
cmd := account_access.NewCmdAccountAccess(f)
119+
cmd.SetArgs([]string{"list"})
120+
cmd.SetOut(buf)
121+
122+
if err := cmd.Execute(); err != nil {
123+
t.Fatalf("unexpected error: %v", err)
124+
}
125+
126+
output := buf.String()
127+
var result []map[string]interface{}
128+
if err := json.Unmarshal([]byte(output), &result); err != nil {
129+
t.Fatalf("output is not valid JSON: %v\noutput:\n%s", err, output)
130+
}
131+
if len(result) != 1 {
132+
t.Fatalf("expected 1 access, got %d", len(result))
133+
}
134+
// Verify nested structure is preserved in JSON mode
135+
specifier, ok := result[0]["specifier"].(map[string]interface{})
136+
if !ok {
137+
t.Fatal("expected 'specifier' to be a nested object")
138+
}
139+
if specifier["email"] != "user@test.com" {
140+
t.Errorf("expected specifier email 'user@test.com', got %v", specifier["email"])
141+
}
142+
}
143+
144+
func TestAccountAccessRemove(t *testing.T) {
145+
f, buf, cleanup := setupTest(func(w http.ResponseWriter, r *http.Request) {
146+
if r.Method != http.MethodDelete {
147+
t.Errorf("expected DELETE, got %s", r.Method)
148+
}
149+
if !strings.Contains(r.URL.Path, "/api/accounts/123/account_accesses/1") {
150+
t.Errorf("unexpected path: %s", r.URL.Path)
151+
}
152+
w.WriteHeader(http.StatusOK)
153+
})
154+
defer cleanup()
155+
156+
cmd := account_access.NewCmdAccountAccess(f)
157+
cmd.SetArgs([]string{"remove", "--id", "1"})
158+
cmd.SetOut(buf)
159+
160+
if err := cmd.Execute(); err != nil {
161+
t.Fatalf("unexpected error: %v", err)
162+
}
163+
164+
output := buf.String()
165+
if !strings.Contains(output, "removed successfully") {
166+
t.Errorf("expected 'removed successfully' in output, got:\n%s", output)
167+
}
168+
}
169+
170+
func TestAccountAccessRemoveMissingID(t *testing.T) {
171+
f, _, cleanup := setupTest(func(w http.ResponseWriter, r *http.Request) {})
172+
defer cleanup()
173+
174+
buf := &bytes.Buffer{}
175+
f.IOStreams.Out = buf
176+
177+
cmd := account_access.NewCmdAccountAccess(f)
178+
cmd.SetArgs([]string{"remove"})
179+
cmd.SetOut(buf)
180+
181+
err := cmd.Execute()
182+
if err == nil {
183+
t.Fatal("expected error when --id is missing")
184+
}
185+
if !strings.Contains(err.Error(), "--id is required") {
186+
t.Errorf("expected '--id is required' error, got: %v", err)
187+
}
188+
}

internal/commands/account_access/list.go

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,41 @@ import (
1010
"github.com/spf13/cobra"
1111
)
1212

13+
type Specifier struct {
14+
ID int `json:"id"`
15+
Email string `json:"email"`
16+
Name string `json:"name"`
17+
}
18+
19+
type Resource struct {
20+
ResourceID int `json:"resource_id"`
21+
ResourceType string `json:"resource_type"`
22+
AccessLevel int `json:"access_level"`
23+
}
24+
1325
type AccountAccess struct {
14-
ID int `json:"id"`
15-
UserID int `json:"specifier_id"`
16-
UserEmail string `json:"specifier_email"`
17-
AccessLevel int `json:"access_level"`
18-
CreatedAt string `json:"created_at"`
26+
ID int `json:"id"`
27+
SpecifierType string `json:"specifier_type"`
28+
Specifier Specifier `json:"specifier"`
29+
Resources []Resource `json:"resources"`
30+
}
31+
32+
type accountAccessRow struct {
33+
ID int `json:"id"`
34+
SpecifierType string `json:"specifier_type"`
35+
Email string `json:"email"`
36+
Name string `json:"name"`
37+
AccessLevel int `json:"access_level"`
38+
ResourceType string `json:"resource_type"`
1939
}
2040

2141
var accountAccessColumns = []output.Column{
2242
{Header: "ID", Field: "id"},
23-
{Header: "USER_ID", Field: "specifier_id"},
24-
{Header: "USER_EMAIL", Field: "specifier_email"},
43+
{Header: "TYPE", Field: "specifier_type"},
44+
{Header: "EMAIL", Field: "email"},
45+
{Header: "NAME", Field: "name"},
2546
{Header: "ACCESS_LEVEL", Field: "access_level"},
26-
{Header: "CREATED_AT", Field: "created_at"},
47+
{Header: "RESOURCE_TYPE", Field: "resource_type"},
2748
}
2849

2950
func NewCmdList(f *cmdutil.Factory) *cobra.Command {
@@ -49,7 +70,29 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command {
4970
}
5071

5172
format := cmdutil.GetOutputFormat()
52-
return output.Print(f.IOStreams.Out, format, accesses, accountAccessColumns)
73+
74+
if format == output.FormatJSON {
75+
return output.Print(f.IOStreams.Out, format, accesses, nil)
76+
}
77+
78+
var rows []accountAccessRow
79+
for _, a := range accesses {
80+
topLevel := 0
81+
topResource := ""
82+
if len(a.Resources) > 0 {
83+
topLevel = a.Resources[0].AccessLevel
84+
topResource = a.Resources[0].ResourceType
85+
}
86+
rows = append(rows, accountAccessRow{
87+
ID: a.ID,
88+
SpecifierType: a.SpecifierType,
89+
Email: a.Specifier.Email,
90+
Name: a.Specifier.Name,
91+
AccessLevel: topLevel,
92+
ResourceType: topResource,
93+
})
94+
}
95+
return output.Print(f.IOStreams.Out, format, rows, accountAccessColumns)
5396
},
5497
}
5598

internal/commands/accounts/accounts_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ func TestAccountsList(t *testing.T) {
5151
if r.URL.Path != "/api/accounts" {
5252
t.Errorf("unexpected path: %s", r.URL.Path)
5353
}
54-
if r.Header.Get("Authorization") != "Bearer test-token" {
55-
t.Errorf("expected Authorization header 'Bearer test-token', got %q", r.Header.Get("Authorization"))
54+
if r.Header.Get("Api-Token") != "test-token" {
55+
t.Errorf("expected Api-Token header 'test-token', got %q", r.Header.Get("Api-Token"))
5656
}
5757

5858
w.Header().Set("Content-Type", "application/json")

0 commit comments

Comments
 (0)