diff --git a/README.md b/README.md index 9dd6fb5..50f28ab 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ mailtrap templates create --name "Welcome" --subject "Hello {{name}}" --body-htm # Contacts mailtrap contacts create --email "user@example.com" --first-name "John" mailtrap contact-lists list -mailtrap contact-fields create --name "Company" --data-type text +mailtrap contact-fields create --name "Company" --data-type text --merge-tag "{{company}}" # Sandboxes & projects mailtrap projects list diff --git a/docs/TEST_PLAN.md b/docs/TEST_PLAN.md index 827a40e..403910f 100644 --- a/docs/TEST_PLAN.md +++ b/docs/TEST_PLAN.md @@ -180,7 +180,7 @@ Tests are organized by endpoint group. Each test specifies: |---|------|---------|----------| | 9.1 | List contact fields | `mailtrap contact-fields list` | Table with field entries | | 9.2 | Get contact field | `mailtrap contact-fields get --id ` | Single field details | -| 9.3 | Create contact field | `mailtrap contact-fields create --name "test-field" --type "string"` | New field in output | +| 9.3 | Create contact field | `mailtrap contact-fields create --name "test-field" --data-type text --merge-tag "{{test_field}}"` | New field in output | | 9.4 | Update contact field | `mailtrap contact-fields update --id --name "test-field-updated"` | Updated field | | 9.5 | Delete contact field | `mailtrap contact-fields delete --id ` | Success message | | 9.6 | Get missing ID | `mailtrap contact-fields get` | Error: `--id is required` | @@ -304,7 +304,7 @@ Prerequisite: Send an email with an attachment to the sandbox. |---|------|---------|----------| | 18.1 | List tokens | `mailtrap tokens list` | Table with tokens (may require admin token) | | 18.2 | Get token | `mailtrap tokens get --id ` | Token details | -| 18.3 | Create token | `mailtrap tokens create --name "test-token"` | New token | +| 18.3 | Create token | `mailtrap tokens create --name "test-token" --permissions '[{"resource_type":"account","resource_id":,"access_level":100}]'` | New token | | 18.4 | Reset token | `mailtrap tokens reset --id ` | New token value | | 18.5 | Delete token | `mailtrap tokens delete --id ` | Success message | | 18.6 | Get missing ID | `mailtrap tokens get` | Error: `--id is required` | @@ -346,6 +346,8 @@ Prerequisite: Send an email with an attachment to the sandbox. | B3 | **Medium** | NOT A BUG | `contacts list` returns 404 — the Mailtrap API has no `GET /contacts` endpoint. Contacts can only be managed individually (get/create/update/delete). No `list` subcommand exists in the CLI. | | B4 | **Low** | NOT A BUG | `tokens list` returns "Access forbidden" — this is an API permission issue requiring an admin-level token, not a CLI bug. The error message is surfaced correctly. | | B5 | **Low** | NOT A BUG | `stats get` requires `--start-date` — already enforced via `cobra.MarkFlagRequired("start-date")`. The CLI shows a clear error when omitted. | +| B6 | **High** | FIXED | `tokens create` — request body was wrapped in an `api_token` key (`{"api_token": {"name": ..., "resources": [...]}}`), but the API expects a flat body (`{"name": ..., "resources": [...]}`). Removed the wrapper in `tokens/create.go`. | +| B7 | **High** | FIXED | `contact-fields create` — the `--merge-tag` flag was missing entirely. The Mailtrap API requires `merge_tag` when creating a contact field, but the CLI only accepted `--name` and `--data-type`. Added `--merge-tag` flag, validation, and included `merge_tag` in the POST body. | --- diff --git a/internal/commands/contact_fields/contact_fields_test.go b/internal/commands/contact_fields/contact_fields_test.go index 3759c27..bafcc92 100644 --- a/internal/commands/contact_fields/contact_fields_test.go +++ b/internal/commands/contact_fields/contact_fields_test.go @@ -181,6 +181,9 @@ func TestContactFieldsCreate(t *testing.T) { if payload["data_type"] != "text" { t.Errorf("expected data_type 'text', got %v", payload["data_type"]) } + if payload["merge_tag"] != "{{company}}" { + t.Errorf("expected merge_tag '{{company}}', got %v", payload["merge_tag"]) + } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ @@ -190,7 +193,7 @@ func TestContactFieldsCreate(t *testing.T) { defer cleanup() cmd := contact_fields.NewCmdContactFields(f) - cmd.SetArgs([]string{"create", "--name", "Company", "--data-type", "text"}) + cmd.SetArgs([]string{"create", "--name", "Company", "--data-type", "text", "--merge-tag", "{{company}}"}) cmd.SetOut(buf) err := cmd.Execute() @@ -236,6 +239,22 @@ func TestContactFieldsCreateMissingDataType(t *testing.T) { } } +func TestContactFieldsCreateMissingMergeTag(t *testing.T) { + f, _, cleanup := setupTest(func(w http.ResponseWriter, r *http.Request) {}) + defer cleanup() + + cmd := contact_fields.NewCmdContactFields(f) + cmd.SetArgs([]string{"create", "--name", "Company", "--data-type", "text"}) + + err := cmd.Execute() + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "--merge-tag is required") { + t.Errorf("expected error to contain '--merge-tag is required', got: %v", err) + } +} + func TestContactFieldsUpdate(t *testing.T) { f, buf, cleanup := setupTest(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPatch { diff --git a/internal/commands/contact_fields/create.go b/internal/commands/contact_fields/create.go index a5d1b49..5c4e250 100644 --- a/internal/commands/contact_fields/create.go +++ b/internal/commands/contact_fields/create.go @@ -13,6 +13,7 @@ import ( func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { var name string var dataType string + var mergeTag string cmd := &cobra.Command{ Use: "create", @@ -24,6 +25,9 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { if err := cmdutil.RequireFlag("data-type", dataType); err != nil { return err } + if err := cmdutil.RequireFlag("merge-tag", mergeTag); err != nil { + return err + } c, err := f.NewClient() if err != nil { @@ -40,6 +44,7 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { body := map[string]interface{}{ "name": name, "data_type": dataType, + "merge_tag": mergeTag, } var field ContactField @@ -54,6 +59,7 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { cmd.Flags().StringVar(&name, "name", "", "Contact field name (required)") cmd.Flags().StringVar(&dataType, "data-type", "", "Data type: text, integer, float, boolean, date (required)") + cmd.Flags().StringVar(&mergeTag, "merge-tag", "", "Merge tag for the field (required)") return cmd } diff --git a/internal/commands/stats/by_category.go b/internal/commands/stats/by_category.go index 9b405b9..f4ee724 100644 --- a/internal/commands/stats/by_category.go +++ b/internal/commands/stats/by_category.go @@ -45,7 +45,7 @@ func NewCmdByCategory(f *cmdutil.Factory) *cobra.Command { params.Add("sending_domain_ids[]", d) } for _, s := range opts.Streams { - params.Add("streams[]", s) + params.Add("sending_streams[]", s) } for _, cat := range opts.Categories { params.Add("categories[]", cat) @@ -53,13 +53,13 @@ func NewCmdByCategory(f *cmdutil.Factory) *cobra.Command { fullPath := fmt.Sprintf("%s?%s", path, params.Encode()) - var result []Stats + var result []CategoryStats if err := c.Get(context.Background(), client.BaseGeneral, fullPath, nil, &result); err != nil { return err } format := cmdutil.GetOutputFormat() - output.Print(f.IOStreams.Out, format, result, statsColumns) + output.Print(f.IOStreams.Out, format, result, categoryStatsColumns) return nil }, diff --git a/internal/commands/stats/by_date.go b/internal/commands/stats/by_date.go index 5b10270..195368c 100644 --- a/internal/commands/stats/by_date.go +++ b/internal/commands/stats/by_date.go @@ -45,7 +45,7 @@ func NewCmdByDate(f *cmdutil.Factory) *cobra.Command { params.Add("sending_domain_ids[]", d) } for _, s := range opts.Streams { - params.Add("streams[]", s) + params.Add("sending_streams[]", s) } for _, cat := range opts.Categories { params.Add("categories[]", cat) @@ -53,13 +53,13 @@ func NewCmdByDate(f *cmdutil.Factory) *cobra.Command { fullPath := fmt.Sprintf("%s?%s", path, params.Encode()) - var result []Stats + var result []DateStats if err := c.Get(context.Background(), client.BaseGeneral, fullPath, nil, &result); err != nil { return err } format := cmdutil.GetOutputFormat() - output.Print(f.IOStreams.Out, format, result, statsColumns) + output.Print(f.IOStreams.Out, format, result, dateStatsColumns) return nil }, diff --git a/internal/commands/stats/by_domain.go b/internal/commands/stats/by_domain.go index 18ee2ba..d62cfa3 100644 --- a/internal/commands/stats/by_domain.go +++ b/internal/commands/stats/by_domain.go @@ -45,7 +45,7 @@ func NewCmdByDomain(f *cmdutil.Factory) *cobra.Command { params.Add("sending_domain_ids[]", d) } for _, s := range opts.Streams { - params.Add("streams[]", s) + params.Add("sending_streams[]", s) } for _, cat := range opts.Categories { params.Add("categories[]", cat) @@ -53,13 +53,13 @@ func NewCmdByDomain(f *cmdutil.Factory) *cobra.Command { fullPath := fmt.Sprintf("%s?%s", path, params.Encode()) - var result []Stats + var result []DomainStats if err := c.Get(context.Background(), client.BaseGeneral, fullPath, nil, &result); err != nil { return err } format := cmdutil.GetOutputFormat() - output.Print(f.IOStreams.Out, format, result, statsColumns) + output.Print(f.IOStreams.Out, format, result, domainStatsColumns) return nil }, diff --git a/internal/commands/stats/by_esp.go b/internal/commands/stats/by_esp.go index e9e8526..da34955 100644 --- a/internal/commands/stats/by_esp.go +++ b/internal/commands/stats/by_esp.go @@ -45,7 +45,7 @@ func NewCmdByESP(f *cmdutil.Factory) *cobra.Command { params.Add("sending_domain_ids[]", d) } for _, s := range opts.Streams { - params.Add("streams[]", s) + params.Add("sending_streams[]", s) } for _, cat := range opts.Categories { params.Add("categories[]", cat) @@ -53,13 +53,13 @@ func NewCmdByESP(f *cmdutil.Factory) *cobra.Command { fullPath := fmt.Sprintf("%s?%s", path, params.Encode()) - var result []Stats + var result []ESPStats if err := c.Get(context.Background(), client.BaseGeneral, fullPath, nil, &result); err != nil { return err } format := cmdutil.GetOutputFormat() - output.Print(f.IOStreams.Out, format, result, statsColumns) + output.Print(f.IOStreams.Out, format, result, espStatsColumns) return nil }, diff --git a/internal/commands/stats/get.go b/internal/commands/stats/get.go index 3ee7845..57c4cec 100644 --- a/internal/commands/stats/get.go +++ b/internal/commands/stats/get.go @@ -2,6 +2,7 @@ package stats import ( "context" + "encoding/json" "fmt" "net/url" @@ -38,6 +39,153 @@ var statsColumns = []output.Column{ {Header: "SPAM RATE", Field: "spam_rate"}, } +// Grouped response types — the API wraps stats under a "stats" key alongside the grouping field. +// MarshalJSON on each type flattens the nested stats for table/text output. + +type DomainStats struct { + SendingDomainID int `json:"sending_domain_id"` + Stats Stats `json:"stats"` +} + +func (d DomainStats) MarshalJSON() ([]byte, error) { + type flat struct { + SendingDomainID int `json:"sending_domain_id"` + DeliveryCount int `json:"delivery_count"` + DeliveryRate float64 `json:"delivery_rate"` + BounceCount int `json:"bounce_count"` + BounceRate float64 `json:"bounce_rate"` + OpenCount int `json:"open_count"` + OpenRate float64 `json:"open_rate"` + ClickCount int `json:"click_count"` + ClickRate float64 `json:"click_rate"` + SpamCount int `json:"spam_count"` + SpamRate float64 `json:"spam_rate"` + } + return json.Marshal(flat{d.SendingDomainID, d.Stats.DeliveryCount, d.Stats.DeliveryRate, d.Stats.BounceCount, d.Stats.BounceRate, d.Stats.OpenCount, d.Stats.OpenRate, d.Stats.ClickCount, d.Stats.ClickRate, d.Stats.SpamCount, d.Stats.SpamRate}) +} + +var domainStatsColumns = []output.Column{ + {Header: "DOMAIN ID", Field: "sending_domain_id"}, + {Header: "DELIVERY COUNT", Field: "delivery_count"}, + {Header: "DELIVERY RATE", Field: "delivery_rate"}, + {Header: "BOUNCE COUNT", Field: "bounce_count"}, + {Header: "BOUNCE RATE", Field: "bounce_rate"}, + {Header: "OPEN COUNT", Field: "open_count"}, + {Header: "OPEN RATE", Field: "open_rate"}, + {Header: "CLICK COUNT", Field: "click_count"}, + {Header: "CLICK RATE", Field: "click_rate"}, + {Header: "SPAM COUNT", Field: "spam_count"}, + {Header: "SPAM RATE", Field: "spam_rate"}, +} + +type CategoryStats struct { + Category string `json:"category"` + Stats Stats `json:"stats"` +} + +func (c CategoryStats) MarshalJSON() ([]byte, error) { + type flat struct { + Category string `json:"category"` + DeliveryCount int `json:"delivery_count"` + DeliveryRate float64 `json:"delivery_rate"` + BounceCount int `json:"bounce_count"` + BounceRate float64 `json:"bounce_rate"` + OpenCount int `json:"open_count"` + OpenRate float64 `json:"open_rate"` + ClickCount int `json:"click_count"` + ClickRate float64 `json:"click_rate"` + SpamCount int `json:"spam_count"` + SpamRate float64 `json:"spam_rate"` + } + return json.Marshal(flat{c.Category, c.Stats.DeliveryCount, c.Stats.DeliveryRate, c.Stats.BounceCount, c.Stats.BounceRate, c.Stats.OpenCount, c.Stats.OpenRate, c.Stats.ClickCount, c.Stats.ClickRate, c.Stats.SpamCount, c.Stats.SpamRate}) +} + +var categoryStatsColumns = []output.Column{ + {Header: "CATEGORY", Field: "category"}, + {Header: "DELIVERY COUNT", Field: "delivery_count"}, + {Header: "DELIVERY RATE", Field: "delivery_rate"}, + {Header: "BOUNCE COUNT", Field: "bounce_count"}, + {Header: "BOUNCE RATE", Field: "bounce_rate"}, + {Header: "OPEN COUNT", Field: "open_count"}, + {Header: "OPEN RATE", Field: "open_rate"}, + {Header: "CLICK COUNT", Field: "click_count"}, + {Header: "CLICK RATE", Field: "click_rate"}, + {Header: "SPAM COUNT", Field: "spam_count"}, + {Header: "SPAM RATE", Field: "spam_rate"}, +} + +type ESPStats struct { + EmailServiceProvider string `json:"email_service_provider"` + Stats Stats `json:"stats"` +} + +func (e ESPStats) MarshalJSON() ([]byte, error) { + type flat struct { + EmailServiceProvider string `json:"email_service_provider"` + DeliveryCount int `json:"delivery_count"` + DeliveryRate float64 `json:"delivery_rate"` + BounceCount int `json:"bounce_count"` + BounceRate float64 `json:"bounce_rate"` + OpenCount int `json:"open_count"` + OpenRate float64 `json:"open_rate"` + ClickCount int `json:"click_count"` + ClickRate float64 `json:"click_rate"` + SpamCount int `json:"spam_count"` + SpamRate float64 `json:"spam_rate"` + } + return json.Marshal(flat{e.EmailServiceProvider, e.Stats.DeliveryCount, e.Stats.DeliveryRate, e.Stats.BounceCount, e.Stats.BounceRate, e.Stats.OpenCount, e.Stats.OpenRate, e.Stats.ClickCount, e.Stats.ClickRate, e.Stats.SpamCount, e.Stats.SpamRate}) +} + +var espStatsColumns = []output.Column{ + {Header: "ESP", Field: "email_service_provider"}, + {Header: "DELIVERY COUNT", Field: "delivery_count"}, + {Header: "DELIVERY RATE", Field: "delivery_rate"}, + {Header: "BOUNCE COUNT", Field: "bounce_count"}, + {Header: "BOUNCE RATE", Field: "bounce_rate"}, + {Header: "OPEN COUNT", Field: "open_count"}, + {Header: "OPEN RATE", Field: "open_rate"}, + {Header: "CLICK COUNT", Field: "click_count"}, + {Header: "CLICK RATE", Field: "click_rate"}, + {Header: "SPAM COUNT", Field: "spam_count"}, + {Header: "SPAM RATE", Field: "spam_rate"}, +} + +type DateStats struct { + Date string `json:"date"` + Stats Stats `json:"stats"` +} + +func (d DateStats) MarshalJSON() ([]byte, error) { + type flat struct { + Date string `json:"date"` + DeliveryCount int `json:"delivery_count"` + DeliveryRate float64 `json:"delivery_rate"` + BounceCount int `json:"bounce_count"` + BounceRate float64 `json:"bounce_rate"` + OpenCount int `json:"open_count"` + OpenRate float64 `json:"open_rate"` + ClickCount int `json:"click_count"` + ClickRate float64 `json:"click_rate"` + SpamCount int `json:"spam_count"` + SpamRate float64 `json:"spam_rate"` + } + return json.Marshal(flat{d.Date, d.Stats.DeliveryCount, d.Stats.DeliveryRate, d.Stats.BounceCount, d.Stats.BounceRate, d.Stats.OpenCount, d.Stats.OpenRate, d.Stats.ClickCount, d.Stats.ClickRate, d.Stats.SpamCount, d.Stats.SpamRate}) +} + +var dateStatsColumns = []output.Column{ + {Header: "DATE", Field: "date"}, + {Header: "DELIVERY COUNT", Field: "delivery_count"}, + {Header: "DELIVERY RATE", Field: "delivery_rate"}, + {Header: "BOUNCE COUNT", Field: "bounce_count"}, + {Header: "BOUNCE RATE", Field: "bounce_rate"}, + {Header: "OPEN COUNT", Field: "open_count"}, + {Header: "OPEN RATE", Field: "open_rate"}, + {Header: "CLICK COUNT", Field: "click_count"}, + {Header: "CLICK RATE", Field: "click_rate"}, + {Header: "SPAM COUNT", Field: "spam_count"}, + {Header: "SPAM RATE", Field: "spam_rate"}, +} + type GetOptions struct { StartDate string EndDate string diff --git a/internal/commands/stats/stats_test.go b/internal/commands/stats/stats_test.go index 9b5354b..498c592 100644 --- a/internal/commands/stats/stats_test.go +++ b/internal/commands/stats/stats_test.go @@ -124,7 +124,16 @@ func TestStatsByDomain(t *testing.T) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode([]map[string]interface{}{ - {"delivery_count": 50, "delivery_rate": 0.9, "bounce_count": 2, "bounce_rate": 0.04, "open_count": 25, "open_rate": 0.5, "click_count": 10, "click_rate": 0.2, "spam_count": 0, "spam_rate": 0.0}, + { + "sending_domain_id": 42, + "stats": map[string]interface{}{ + "delivery_count": 50, "delivery_rate": 0.9, + "bounce_count": 2, "bounce_rate": 0.04, + "open_count": 25, "open_rate": 0.5, + "click_count": 10, "click_rate": 0.2, + "spam_count": 0, "spam_rate": 0.0, + }, + }, }) }) defer cleanup() @@ -139,6 +148,9 @@ func TestStatsByDomain(t *testing.T) { } output := buf.String() + if !strings.Contains(output, "42") { + t.Errorf("expected output to contain domain id '42', got:\n%s", output) + } if !strings.Contains(output, "50") { t.Errorf("expected output to contain '50', got:\n%s", output) } @@ -155,7 +167,16 @@ func TestStatsByCategory(t *testing.T) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode([]map[string]interface{}{ - {"delivery_count": 30, "delivery_rate": 0.85, "bounce_count": 3, "bounce_rate": 0.06, "open_count": 15, "open_rate": 0.5, "click_count": 8, "click_rate": 0.27, "spam_count": 0, "spam_rate": 0.0}, + { + "category": "newsletter", + "stats": map[string]interface{}{ + "delivery_count": 30, "delivery_rate": 0.85, + "bounce_count": 3, "bounce_rate": 0.06, + "open_count": 15, "open_rate": 0.5, + "click_count": 8, "click_rate": 0.27, + "spam_count": 0, "spam_rate": 0.0, + }, + }, }) }) defer cleanup() @@ -170,6 +191,9 @@ func TestStatsByCategory(t *testing.T) { } output := buf.String() + if !strings.Contains(output, "newsletter") { + t.Errorf("expected output to contain 'newsletter', got:\n%s", output) + } if !strings.Contains(output, "30") { t.Errorf("expected output to contain '30', got:\n%s", output) } @@ -186,7 +210,16 @@ func TestStatsByEsp(t *testing.T) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode([]map[string]interface{}{ - {"delivery_count": 40, "delivery_rate": 0.88, "bounce_count": 4, "bounce_rate": 0.08, "open_count": 20, "open_rate": 0.5, "click_count": 12, "click_rate": 0.3, "spam_count": 1, "spam_rate": 0.02}, + { + "email_service_provider": "Gmail", + "stats": map[string]interface{}{ + "delivery_count": 40, "delivery_rate": 0.88, + "bounce_count": 4, "bounce_rate": 0.08, + "open_count": 20, "open_rate": 0.5, + "click_count": 12, "click_rate": 0.3, + "spam_count": 1, "spam_rate": 0.02, + }, + }, }) }) defer cleanup() @@ -201,6 +234,9 @@ func TestStatsByEsp(t *testing.T) { } output := buf.String() + if !strings.Contains(output, "Gmail") { + t.Errorf("expected output to contain 'Gmail', got:\n%s", output) + } if !strings.Contains(output, "40") { t.Errorf("expected output to contain '40', got:\n%s", output) } @@ -217,7 +253,16 @@ func TestStatsByDate(t *testing.T) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode([]map[string]interface{}{ - {"delivery_count": 60, "delivery_rate": 0.92, "bounce_count": 6, "bounce_rate": 0.06, "open_count": 30, "open_rate": 0.5, "click_count": 15, "click_rate": 0.25, "spam_count": 2, "spam_rate": 0.03}, + { + "date": "2024-01-15", + "stats": map[string]interface{}{ + "delivery_count": 60, "delivery_rate": 0.92, + "bounce_count": 6, "bounce_rate": 0.06, + "open_count": 30, "open_rate": 0.5, + "click_count": 15, "click_rate": 0.25, + "spam_count": 2, "spam_rate": 0.03, + }, + }, }) }) defer cleanup() @@ -232,6 +277,9 @@ func TestStatsByDate(t *testing.T) { } output := buf.String() + if !strings.Contains(output, "2024-01-15") { + t.Errorf("expected output to contain '2024-01-15', got:\n%s", output) + } if !strings.Contains(output, "60") { t.Errorf("expected output to contain '60', got:\n%s", output) } diff --git a/internal/commands/tokens/create.go b/internal/commands/tokens/create.go index 1ae2637..d306088 100644 --- a/internal/commands/tokens/create.go +++ b/internal/commands/tokens/create.go @@ -54,10 +54,8 @@ Example: path := cmdutil.AccountPath("api_tokens") body := map[string]interface{}{ - "api_token": map[string]interface{}{ - "name": name, - "resources": resources, - }, + "name": name, + "resources": resources, } var token APIToken diff --git a/internal/commands/tokens/tokens_test.go b/internal/commands/tokens/tokens_test.go index 15d6a5f..6654887 100644 --- a/internal/commands/tokens/tokens_test.go +++ b/internal/commands/tokens/tokens_test.go @@ -176,14 +176,10 @@ func TestTokensCreate(t *testing.T) { t.Fatalf("failed to parse request body: %v", err) } - apiToken, ok := payload["api_token"].(map[string]interface{}) - if !ok { - t.Fatal("expected api_token wrapper in body") - } - if apiToken["name"] != "new-token" { - t.Errorf("expected name 'new-token', got %v", apiToken["name"]) + if payload["name"] != "new-token" { + t.Errorf("expected name 'new-token', got %v", payload["name"]) } - resources, ok := apiToken["resources"].([]interface{}) + resources, ok := payload["resources"].([]interface{}) if !ok { t.Fatal("expected resources to be an array") } diff --git a/skills/mailtrap-cli/references/accounts.md b/skills/mailtrap-cli/references/accounts.md index 580fc7f..87fbe46 100644 --- a/skills/mailtrap-cli/references/accounts.md +++ b/skills/mailtrap-cli/references/accounts.md @@ -57,6 +57,7 @@ Create a new API token. | Flag | Type | Required | Description | |------|------|----------|-------------| | `--name` | string | Yes | Token name | +| `--permissions` | string | Yes | Permissions JSON array, e.g. `'[{"resource_type":"account","resource_id":123,"access_level":100}]'` | **Note:** The token value is shown only once in the response. Store it securely. diff --git a/skills/mailtrap-cli/references/contacts.md b/skills/mailtrap-cli/references/contacts.md index 280ea3a..1c4d7fe 100644 --- a/skills/mailtrap-cli/references/contacts.md +++ b/skills/mailtrap-cli/references/contacts.md @@ -177,7 +177,8 @@ Create a custom contact field. | Flag | Type | Required | Description | |------|------|----------|-------------| | `--name` | string | Yes | Field name | -| `--field-type` | string | Yes | Field type (e.g. `string`, `number`, `date`) | +| `--data-type` | string | Yes | Data type: `text`, `integer`, `float`, `boolean`, `date` | +| `--merge-tag` | string | Yes | Merge tag for the field (e.g. `{{company}}`) | --- @@ -189,7 +190,8 @@ Update a custom contact field. |------|------|----------|-------------| | `--id` | string | Yes | Contact field ID | | `--name` | string | No | New field name | -| `--field-type` | string | No | New field type | +| `--data-type` | string | No | New data type | +| `--merge-tag` | string | No | New merge tag | ---