From 3b51f73af8dbb59dd82bf376895b3ee892c5b654 Mon Sep 17 00:00:00 2001 From: Alex Shchyhol Date: Mon, 3 Aug 2026 14:28:08 +0200 Subject: [PATCH 01/10] MT-23076: add token expiration type and create request field --- api_tokens.go | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/api_tokens.go b/api_tokens.go index 60cadab..25d1f05 100644 --- a/api_tokens.go +++ b/api_tokens.go @@ -2,6 +2,7 @@ package mailtrap import ( "context" + "encoding/json" "fmt" "net/http" ) @@ -37,10 +38,42 @@ type APITokenPermission struct { AccessLevel int `json:"access_level"` } +// TokenExpiration is an optional token expiration as an RFC 3339 date-time. +// Leave the request field nil for the server default (a 1-year default is +// being rolled out). Use NeverExpires for a token that never expires. Past or +// more-than-5-years-ahead values are rejected with 422. +type TokenExpiration struct { + value string + never bool +} + +// ExpiresAt returns a token expiration at the given RFC 3339 date-time, e.g. +// "2027-06-01T00:00:00Z". +func ExpiresAt(rfc3339 string) *TokenExpiration { + return &TokenExpiration{value: rfc3339} +} + +// NeverExpires returns a token expiration for a token that never expires. It +// serializes as an explicit "expires_at": null. +func NeverExpires() *TokenExpiration { + return &TokenExpiration{never: true} +} + +// MarshalJSON encodes the RFC 3339 date-time, or null for NeverExpires. +func (e TokenExpiration) MarshalJSON() ([]byte, error) { + if e.never { + return []byte("null"), nil + } + return json.Marshal(e.value) +} + // CreateAPITokenRequest is the payload for creating an API token. Name is // required. type CreateAPITokenRequest struct { - Name string `json:"name"` + Name string `json:"name"` + // ExpiresAt is the optional token expiration. Nil omits the field and + // applies the server default; see TokenExpiration. + ExpiresAt *TokenExpiration `json:"expires_at,omitempty"` Resources []*APITokenPermission `json:"resources,omitempty"` } From 45f525f49bad98862711a69246fe42ef755c9ef6 Mon Sep 17 00:00:00 2001 From: Alex Shchyhol Date: Mon, 3 Aug 2026 14:28:50 +0200 Subject: [PATCH 02/10] MT-23076: accept optional expiration in api token reset --- api_tokens.go | 19 +++++++++++++++++-- api_tokens_test.go | 2 +- examples/api-tokens/main.go | 2 +- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/api_tokens.go b/api_tokens.go index 25d1f05..1016f66 100644 --- a/api_tokens.go +++ b/api_tokens.go @@ -77,6 +77,13 @@ type CreateAPITokenRequest struct { Resources []*APITokenPermission `json:"resources,omitempty"` } +// ResetAPITokenRequest is the optional payload for resetting an API token. +type ResetAPITokenRequest struct { + // ExpiresAt is the optional expiration of the replacement token. Nil omits + // the field and applies the server default; see TokenExpiration. + ExpiresAt *TokenExpiration `json:"expires_at,omitempty"` +} + // List returns all API tokens visible to the current token. func (s *APITokensService) List(ctx context.Context) ([]*APIToken, *Response, error) { var tokens []*APIToken @@ -103,10 +110,18 @@ func (s *APITokensService) Create(ctx context.Context, req *CreateAPITokenReques // Reset expires the token and issues a replacement with the same permissions. // The returned token's Token field holds the new value; store it securely. -func (s *APITokensService) Reset(ctx context.Context, tokenID int64) (*APIToken, *Response, error) { +// req is optional: pass nil to send no request body and apply the server +// default expiration. +func (s *APITokensService) Reset(ctx context.Context, tokenID int64, req *ResetAPITokenRequest) (*APIToken, *Response, error) { path := fmt.Sprintf("/api/api_tokens/%d/reset", tokenID) + // Assign req to any only when non-nil: a typed nil pointer would encode as + // a literal null body instead of sending no body at all. + var body any + if req != nil { + body = req + } token := new(APIToken) - resp, err := s.client.do(ctx, HostGeneral, http.MethodPost, path, nil, nil, token) + resp, err := s.client.do(ctx, HostGeneral, http.MethodPost, path, nil, body, token) return token, resp, err } diff --git a/api_tokens_test.go b/api_tokens_test.go index 1838864..9901e02 100644 --- a/api_tokens_test.go +++ b/api_tokens_test.go @@ -68,7 +68,7 @@ func TestAPITokens_Reset(t *testing.T) { _, _ = w.Write([]byte(`{"id":12345,"name":"My API Token","token":"newtoken123"}`)) }) - token, _, err := client.APITokens.Reset(context.Background(), 12345) + token, _, err := client.APITokens.Reset(context.Background(), 12345, nil) if err != nil { t.Fatalf("Reset: %v", err) } diff --git a/examples/api-tokens/main.go b/examples/api-tokens/main.go index 7fdb2e3..891e364 100644 --- a/examples/api-tokens/main.go +++ b/examples/api-tokens/main.go @@ -44,7 +44,7 @@ func main() { } // Reset expires the token and issues a replacement with the same permissions. - token, _, err = client.APITokens.Reset(ctx, token.ID) + token, _, err = client.APITokens.Reset(ctx, token.ID, nil) if err != nil { log.Fatal(err) } From ca8d3c4768650f68052f62cdc570859a39a40ba2 Mon Sep 17 00:00:00 2001 From: Alex Shchyhol Date: Mon, 3 Aug 2026 14:30:10 +0200 Subject: [PATCH 03/10] MT-23076: cover token expiration in api token tests --- api_tokens_test.go | 154 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 152 insertions(+), 2 deletions(-) diff --git a/api_tokens_test.go b/api_tokens_test.go index 9901e02..40b9076 100644 --- a/api_tokens_test.go +++ b/api_tokens_test.go @@ -2,12 +2,29 @@ package mailtrap_test import ( "context" + "encoding/json" + "errors" + "io" "net/http" + "strings" "testing" "github.com/mailtrap/mailtrap-go" ) +// wantRawBody fails the test unless r's body is exactly want, proving whether +// the expires_at key is absent, null, or a string on the wire. +func wantRawBody(t *testing.T, r *http.Request, want string) { + t.Helper() + b, err := io.ReadAll(r.Body) + if err != nil { + t.Fatalf("read request body: %v", err) + } + if got := strings.TrimSpace(string(b)); got != want { + t.Errorf("request body = %q, want %q", got, want) + } +} + func TestAPITokens_List(t *testing.T) { mux, client := setup(t) mux.HandleFunc("GET /api/api_tokens", func(w http.ResponseWriter, _ *http.Request) { @@ -44,7 +61,7 @@ func TestAPITokens_Get(t *testing.T) { func TestAPITokens_Create(t *testing.T) { mux, client := setup(t) mux.HandleFunc("POST /api/api_tokens", func(w http.ResponseWriter, r *http.Request) { - wantJSONBody(t, r, `{"name":"My API Token","resources":[{"resource_type":"account","resource_id":3229,"access_level":100}]}`) + wantRawBody(t, r, `{"name":"My API Token","resources":[{"resource_type":"account","resource_id":3229,"access_level":100}]}`) _, _ = w.Write([]byte(`{"id":12345,"name":"My API Token","token":"a1b2c3d4e5f6"}`)) }) @@ -62,9 +79,106 @@ func TestAPITokens_Create(t *testing.T) { } } +func TestAPITokens_Create_expiresAt(t *testing.T) { + mux, client := setup(t) + mux.HandleFunc("POST /api/api_tokens", func(w http.ResponseWriter, r *http.Request) { + wantRawBody(t, r, `{"name":"My API Token","expires_at":"2027-06-01T00:00:00Z"}`) + _, _ = w.Write([]byte(`{"id":12345,"name":"My API Token","expires_at":"2027-06-01T00:00:00Z","token":"a1b2c3d4e5f6"}`)) + }) + + token, _, err := client.APITokens.Create(context.Background(), &mailtrap.CreateAPITokenRequest{ + Name: "My API Token", + ExpiresAt: mailtrap.ExpiresAt("2027-06-01T00:00:00Z"), + }) + if err != nil { + t.Fatalf("Create: %v", err) + } + if token.ExpiresAt != "2027-06-01T00:00:00Z" { + t.Errorf("token = %+v", token) + } +} + +func TestAPITokens_Create_neverExpires(t *testing.T) { + mux, client := setup(t) + mux.HandleFunc("POST /api/api_tokens", func(w http.ResponseWriter, r *http.Request) { + wantRawBody(t, r, `{"name":"My API Token","expires_at":null}`) + _, _ = w.Write([]byte(`{"id":12345,"name":"My API Token","expires_at":null,"token":"a1b2c3d4e5f6"}`)) + }) + + token, _, err := client.APITokens.Create(context.Background(), &mailtrap.CreateAPITokenRequest{ + Name: "My API Token", + ExpiresAt: mailtrap.NeverExpires(), + }) + if err != nil { + t.Fatalf("Create: %v", err) + } + if token.ExpiresAt != "" { + t.Errorf("token = %+v", token) + } +} + +func TestAPITokens_Create_expirationRejected(t *testing.T) { + mux, client := setup(t) + mux.HandleFunc("POST /api/api_tokens", func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusUnprocessableEntity) + _, _ = w.Write([]byte(`{"errors":{"expires_at":["must be in the future"]}}`)) + }) + + _, _, err := client.APITokens.Create(context.Background(), &mailtrap.CreateAPITokenRequest{ + Name: "My API Token", + ExpiresAt: mailtrap.ExpiresAt("2020-01-01T00:00:00Z"), + }) + var ve *mailtrap.ValidationError + if !errors.As(err, &ve) { + t.Fatalf("errors.As(*ValidationError) = false for %T", err) + } + if got := ve.Fields["expires_at"]; len(got) != 1 || got[0] != "must be in the future" { + t.Errorf("Fields[expires_at] = %v", got) + } +} + +func TestCreateAPITokenRequest_marshalExpiresAt(t *testing.T) { + tests := []struct { + name string + req *mailtrap.CreateAPITokenRequest + want string + }{ + { + name: "nil omits the key", + req: &mailtrap.CreateAPITokenRequest{Name: "t"}, + want: `{"name":"t"}`, + }, + { + name: "NeverExpires writes explicit null", + req: &mailtrap.CreateAPITokenRequest{Name: "t", ExpiresAt: mailtrap.NeverExpires()}, + want: `{"name":"t","expires_at":null}`, + }, + { + name: "ExpiresAt writes the date-time", + req: &mailtrap.CreateAPITokenRequest{Name: "t", ExpiresAt: mailtrap.ExpiresAt("2027-06-01T00:00:00Z")}, + want: `{"name":"t","expires_at":"2027-06-01T00:00:00Z"}`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := json.Marshal(tt.req) + if err != nil { + t.Fatalf("Marshal: %v", err) + } + if string(got) != tt.want { + t.Errorf("Marshal = %s, want %s", got, tt.want) + } + }) + } +} + func TestAPITokens_Reset(t *testing.T) { mux, client := setup(t) - mux.HandleFunc("POST /api/api_tokens/12345/reset", func(w http.ResponseWriter, _ *http.Request) { + mux.HandleFunc("POST /api/api_tokens/12345/reset", func(w http.ResponseWriter, r *http.Request) { + wantRawBody(t, r, "") + if ct := r.Header.Get("Content-Type"); ct != "" { + t.Errorf("Content-Type = %q, want empty", ct) + } _, _ = w.Write([]byte(`{"id":12345,"name":"My API Token","token":"newtoken123"}`)) }) @@ -77,6 +191,42 @@ func TestAPITokens_Reset(t *testing.T) { } } +func TestAPITokens_Reset_expiresAt(t *testing.T) { + mux, client := setup(t) + mux.HandleFunc("POST /api/api_tokens/12345/reset", func(w http.ResponseWriter, r *http.Request) { + wantRawBody(t, r, `{"expires_at":"2027-06-01T00:00:00Z"}`) + _, _ = w.Write([]byte(`{"id":12345,"name":"My API Token","expires_at":"2027-06-01T00:00:00Z","token":"newtoken123"}`)) + }) + + token, _, err := client.APITokens.Reset(context.Background(), 12345, &mailtrap.ResetAPITokenRequest{ + ExpiresAt: mailtrap.ExpiresAt("2027-06-01T00:00:00Z"), + }) + if err != nil { + t.Fatalf("Reset: %v", err) + } + if token.ExpiresAt != "2027-06-01T00:00:00Z" { + t.Errorf("token = %+v", token) + } +} + +func TestAPITokens_Reset_neverExpires(t *testing.T) { + mux, client := setup(t) + mux.HandleFunc("POST /api/api_tokens/12345/reset", func(w http.ResponseWriter, r *http.Request) { + wantRawBody(t, r, `{"expires_at":null}`) + _, _ = w.Write([]byte(`{"id":12345,"name":"My API Token","expires_at":null,"token":"newtoken123"}`)) + }) + + token, _, err := client.APITokens.Reset(context.Background(), 12345, &mailtrap.ResetAPITokenRequest{ + ExpiresAt: mailtrap.NeverExpires(), + }) + if err != nil { + t.Fatalf("Reset: %v", err) + } + if token.ExpiresAt != "" { + t.Errorf("token = %+v", token) + } +} + func TestAPITokens_Delete(t *testing.T) { mux, client := setup(t) mux.HandleFunc("DELETE /api/api_tokens/12345", func(w http.ResponseWriter, _ *http.Request) { From 068360d85d599d4dbd31f6d917c85d4b3a86f925 Mon Sep 17 00:00:00 2001 From: Alex Shchyhol Date: Mon, 3 Aug 2026 14:30:39 +0200 Subject: [PATCH 04/10] MT-23076: show token expiration in api tokens example --- examples/api-tokens/main.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/examples/api-tokens/main.go b/examples/api-tokens/main.go index 891e364..3fb99a4 100644 --- a/examples/api-tokens/main.go +++ b/examples/api-tokens/main.go @@ -29,6 +29,9 @@ func main() { token, _, err := client.APITokens.Create(ctx, &mailtrap.CreateAPITokenRequest{ Name: "CI token", + // Omit ExpiresAt for the server default expiration, or pass + // mailtrap.NeverExpires() for a token that never expires. + ExpiresAt: mailtrap.ExpiresAt("2027-06-01T00:00:00Z"), Resources: []*mailtrap.APITokenPermission{ {ResourceType: mailtrap.ResourceTypeAccount, ResourceID: accountID, AccessLevel: mailtrap.AccessLevelViewer}, }, @@ -37,14 +40,17 @@ func main() { log.Fatal(err) } // The full token value is only returned by Create and Reset — store it securely. - fmt.Printf("created token %d: %s\n", token.ID, token.Token) + fmt.Printf("created token %d (expires %s): %s\n", token.ID, token.ExpiresAt, token.Token) if _, _, err = client.APITokens.Get(ctx, token.ID); err != nil { log.Fatal(err) } // Reset expires the token and issues a replacement with the same permissions. - token, _, err = client.APITokens.Reset(ctx, token.ID, nil) + // Pass nil instead of a request to apply the server default expiration. + token, _, err = client.APITokens.Reset(ctx, token.ID, &mailtrap.ResetAPITokenRequest{ + ExpiresAt: mailtrap.NeverExpires(), + }) if err != nil { log.Fatal(err) } From 8a0960d93f62de8c87cf4999a3eb341c254fd685 Mon Sep 17 00:00:00 2001 From: Alex Shchyhol Date: Mon, 3 Aug 2026 14:30:56 +0200 Subject: [PATCH 05/10] MT-23076: mention token expiration in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6e3b6ef..67ca363 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ Account & organization management: - Accounts — list the accounts a token can access — [`examples/accounts`](examples/accounts) - Account accesses — list & remove user/invite/token access — [`examples/account-accesses`](examples/account-accesses) - Permissions — list resources & bulk-update access permissions — [`examples/permissions`](examples/permissions) -- API token management — list, create, get, reset & delete — [`examples/api-tokens`](examples/api-tokens) +- API token management — list, create, get, reset & delete, with optional token expiration — [`examples/api-tokens`](examples/api-tokens) - Billing — current billing-cycle usage across Sandbox, Sending & Marketing — [`examples/billing`](examples/billing) - Organization sub-accounts — list & create — [`examples/sub-accounts`](examples/sub-accounts) From 38452ff39f0790d0682a9a56152f740818894c01 Mon Sep 17 00:00:00 2001 From: Alex Shchyhol Date: Mon, 3 Aug 2026 14:31:20 +0200 Subject: [PATCH 06/10] MT-23076: add masked token to account access specifier --- account_accesses.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/account_accesses.go b/account_accesses.go index f5c0186..a8fed16 100644 --- a/account_accesses.go +++ b/account_accesses.go @@ -44,7 +44,7 @@ type AccountAccess struct { // AccountAccessSpecifier describes the entity that holds the access. Which // fields are set depends on the specifier type: users and invites carry Email, -// while API tokens carry AuthorName, Token, and ExpiresAt. +// while API tokens carry AuthorName, Token, MaskedToken, and ExpiresAt. type AccountAccessSpecifier struct { ID int64 `json:"id"` Email string `json:"email,omitempty"` @@ -52,6 +52,7 @@ type AccountAccessSpecifier struct { TwoFactorAuthenticationEnabled *bool `json:"two_factor_authentication_enabled,omitempty"` AuthorName string `json:"author_name,omitempty"` Token string `json:"token,omitempty"` + MaskedToken string `json:"masked_token,omitempty"` ExpiresAt string `json:"expires_at,omitempty"` } From c2b0d09e8c8d00424651c8d6bbf7cd137997d044 Mon Sep 17 00:00:00 2001 From: Alex Shchyhol Date: Wed, 26 Aug 2026 18:26:55 +0200 Subject: [PATCH 07/10] MT-23076: use the real 422 body for the rejected expiration test --- api_tokens_test.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/api_tokens_test.go b/api_tokens_test.go index 40b9076..3e22eb2 100644 --- a/api_tokens_test.go +++ b/api_tokens_test.go @@ -121,7 +121,8 @@ func TestAPITokens_Create_expirationRejected(t *testing.T) { mux, client := setup(t) mux.HandleFunc("POST /api/api_tokens", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusUnprocessableEntity) - _, _ = w.Write([]byte(`{"errors":{"expires_at":["must be in the future"]}}`)) + // Expiration failures are record-level, so the API reports them under "base". + _, _ = w.Write([]byte(`{"errors":{"base":["Expiration date must be in the future"]}}`)) }) _, _, err := client.APITokens.Create(context.Background(), &mailtrap.CreateAPITokenRequest{ @@ -132,8 +133,8 @@ func TestAPITokens_Create_expirationRejected(t *testing.T) { if !errors.As(err, &ve) { t.Fatalf("errors.As(*ValidationError) = false for %T", err) } - if got := ve.Fields["expires_at"]; len(got) != 1 || got[0] != "must be in the future" { - t.Errorf("Fields[expires_at] = %v", got) + if got := ve.Fields["base"]; len(got) != 1 || got[0] != "Expiration date must be in the future" { + t.Errorf("Fields[base] = %v", got) } } From 6e6ca62ed4c31a3fc163007534abbd59b17c4276 Mon Sep 17 00:00:00 2001 From: Alex Shchyhol Date: Wed, 26 Aug 2026 18:26:55 +0200 Subject: [PATCH 08/10] MT-23076: derive the example expiration from the current date --- examples/api-tokens/main.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/api-tokens/main.go b/examples/api-tokens/main.go index 3fb99a4..0257c7b 100644 --- a/examples/api-tokens/main.go +++ b/examples/api-tokens/main.go @@ -6,6 +6,7 @@ import ( "log" "os" "strconv" + "time" "github.com/mailtrap/mailtrap-go" ) @@ -31,7 +32,7 @@ func main() { Name: "CI token", // Omit ExpiresAt for the server default expiration, or pass // mailtrap.NeverExpires() for a token that never expires. - ExpiresAt: mailtrap.ExpiresAt("2027-06-01T00:00:00Z"), + ExpiresAt: mailtrap.ExpiresAt(time.Now().AddDate(0, 0, 30).Format(time.RFC3339)), Resources: []*mailtrap.APITokenPermission{ {ResourceType: mailtrap.ResourceTypeAccount, ResourceID: accountID, AccessLevel: mailtrap.AccessLevelViewer}, }, From 9edcd83c6a6d55fc0799bb7a04c5784cd923be26 Mon Sep 17 00:00:00 2001 From: Alex Shchyhol Date: Wed, 26 Aug 2026 18:26:56 +0200 Subject: [PATCH 09/10] MT-23076: clarify expiration and reset behavior in api token docs --- api_tokens.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/api_tokens.go b/api_tokens.go index 1016f66..33a0927 100644 --- a/api_tokens.go +++ b/api_tokens.go @@ -41,14 +41,16 @@ type APITokenPermission struct { // TokenExpiration is an optional token expiration as an RFC 3339 date-time. // Leave the request field nil for the server default (a 1-year default is // being rolled out). Use NeverExpires for a token that never expires. Past or -// more-than-5-years-ahead values are rejected with 422. +// more-than-5-years-ahead values are rejected with 422. It is a request-only +// type: responses report the expiry as the plain APIToken.ExpiresAt string. type TokenExpiration struct { value string never bool } // ExpiresAt returns a token expiration at the given RFC 3339 date-time, e.g. -// "2027-06-01T00:00:00Z". +// "2027-06-01T00:00:00Z". An empty string is sent as "" and rejected by the +// server; leave the request field nil to omit the expiration instead. func ExpiresAt(rfc3339 string) *TokenExpiration { return &TokenExpiration{value: rfc3339} } @@ -111,7 +113,8 @@ func (s *APITokensService) Create(ctx context.Context, req *CreateAPITokenReques // Reset expires the token and issues a replacement with the same permissions. // The returned token's Token field holds the new value; store it securely. // req is optional: pass nil to send no request body and apply the server -// default expiration. +// default expiration. Resetting a token that has already expired is rejected +// with 422. func (s *APITokensService) Reset(ctx context.Context, tokenID int64, req *ResetAPITokenRequest) (*APIToken, *Response, error) { path := fmt.Sprintf("/api/api_tokens/%d/reset", tokenID) // Assign req to any only when non-nil: a typed nil pointer would encode as From 860c86050271d9f90f15ce6676cd6fb2f21e2fb4 Mon Sep 17 00:00:00 2001 From: Alex Shchyhol Date: Fri, 28 Aug 2026 12:46:47 +0200 Subject: [PATCH 10/10] MT-23076: cover masked token and clarify reset example --- account_accesses.go | 2 +- account_accesses_test.go | 8 ++++++-- examples/api-tokens/main.go | 5 ++++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/account_accesses.go b/account_accesses.go index a8fed16..dc9a1bd 100644 --- a/account_accesses.go +++ b/account_accesses.go @@ -44,7 +44,7 @@ type AccountAccess struct { // AccountAccessSpecifier describes the entity that holds the access. Which // fields are set depends on the specifier type: users and invites carry Email, -// while API tokens carry AuthorName, Token, MaskedToken, and ExpiresAt. +// while API tokens carry AuthorName, MaskedToken, and ExpiresAt. type AccountAccessSpecifier struct { ID int64 `json:"id"` Email string `json:"email,omitempty"` diff --git a/account_accesses_test.go b/account_accesses_test.go index 1222c44..2608c5f 100644 --- a/account_accesses_test.go +++ b/account_accesses_test.go @@ -12,14 +12,14 @@ import ( func TestAccountAccesses_List(t *testing.T) { mux, client := setup(t) mux.HandleFunc("GET /api/account_accesses", func(w http.ResponseWriter, _ *http.Request) { - _, _ = w.Write([]byte(`[{"id":42,"specifier_type":"User","specifier":{"id":1,"email":"a@b.co"},"resources":[{"resource_id":10,"resource_type":"account","access_level":100}],"permissions":{"can_read":true,"can_destroy":true}}]`)) + _, _ = w.Write([]byte(`[{"id":42,"specifier_type":"User","specifier":{"id":1,"email":"a@b.co"},"resources":[{"resource_id":10,"resource_type":"account","access_level":100}],"permissions":{"can_read":true,"can_destroy":true}},{"id":43,"specifier_type":"ApiToken","specifier":{"id":7,"name":"CI token","author_name":"System","masked_token":"********e5f6","expires_at":"2027-06-01T00:00:00Z"},"resources":[],"permissions":{"can_read":true,"can_destroy":false}}]`)) }) accesses, _, err := client.AccountAccesses.List(context.Background(), nil) if err != nil { t.Fatalf("List: %v", err) } - if len(accesses) != 1 { + if len(accesses) != 2 { t.Fatalf("accesses = %+v", accesses) } a := accesses[0] @@ -29,6 +29,10 @@ func TestAccountAccesses_List(t *testing.T) { if a.Resources[0].AccessLevel != mailtrap.AccessLevelAdmin || !a.Permissions.CanDestroy { t.Errorf("resources/permissions = %+v / %+v", a.Resources[0], a.Permissions) } + tok := accesses[1] + if tok.SpecifierType != mailtrap.SpecifierTypeAPIToken || tok.Specifier.MaskedToken != "********e5f6" || tok.Specifier.ExpiresAt != "2027-06-01T00:00:00Z" { + t.Errorf("api token specifier = %+v", tok.Specifier) + } } func TestAccountAccesses_List_filters(t *testing.T) { diff --git a/examples/api-tokens/main.go b/examples/api-tokens/main.go index 0257c7b..481e7f0 100644 --- a/examples/api-tokens/main.go +++ b/examples/api-tokens/main.go @@ -48,13 +48,16 @@ func main() { } // Reset expires the token and issues a replacement with the same permissions. - // Pass nil instead of a request to apply the server default expiration. + // Here the replacement never expires; pass nil instead of a request to apply + // the server default expiration. token, _, err = client.APITokens.Reset(ctx, token.ID, &mailtrap.ResetAPITokenRequest{ ExpiresAt: mailtrap.NeverExpires(), }) if err != nil { log.Fatal(err) } + // ExpiresAt is empty for a token that never expires. + fmt.Printf("reset token %d (expires %q): %s\n", token.ID, token.ExpiresAt, token.Token) if _, err = client.APITokens.Delete(ctx, token.ID); err != nil { log.Fatal(err)