Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions examples/general/api-tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,12 @@ async function apiTokensFlow() {

// Create a new API token scoped to specific resources.
// The full `token` value is returned only in this response — store it securely.
// `expires_at` is optional: omit it for the server default (a 1-year default
// is being rolled out), pass an ISO 8601 date-time for a custom expiration,
// or pass `null` for a token that never expires.
const created = await apiTokensClient.create({
name: "My token",
expires_at: "2027-06-01T00:00:00Z",
resources: [
{ resource_type: "account", resource_id: Number(ACCOUNT_ID), access_level: 10 },
],
Expand All @@ -36,6 +40,8 @@ async function apiTokensFlow() {

// Reset the API token: expires the existing token and returns a new one
// with the same permissions. The new `token` value is only returned here.
// Like create, reset accepts an optional `expires_at` for the new token,
// e.g. `reset(tokenId, { expires_at: null })` for a token that never expires.
const reset = await apiTokensClient.reset(tokenId);
console.log("Reset API token:", JSON.stringify(reset, null, 2));
console.log("New token value (store securely):", reset.token);
Expand Down
131 changes: 131 additions & 0 deletions src/__tests__/lib/api/resources/ApiTokens.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,73 @@ describe("lib/api/resources/ApiTokens: ", () => {
expect(result).toEqual(responseData);
});

it("omits expires_at from the request body when not provided.", async () => {
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/api_tokens`;

expect.assertions(1);

mock.onPost(endpoint).reply(200, responseData);
await apiTokensAPI.create(params);

expect("expires_at" in JSON.parse(mock.history.post[0].data)).toEqual(
false
);
});

it("sends expires_at when provided.", async () => {
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/api_tokens`;
const expiresAt = "2027-06-01T00:00:00Z";

expect.assertions(1);

mock
.onPost(endpoint)
.reply(200, { ...responseData, expires_at: expiresAt });
await apiTokensAPI.create({ ...params, expires_at: expiresAt });

expect(JSON.parse(mock.history.post[0].data).expires_at).toEqual(
expiresAt
);
});

it("sends explicit null expires_at for a token that never expires.", async () => {
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/api_tokens`;

expect.assertions(2);

mock.onPost(endpoint).reply(200, responseData);
await apiTokensAPI.create({ ...params, expires_at: null });

const body = JSON.parse(mock.history.post[0].data);

expect("expires_at" in body).toEqual(true);
expect(body.expires_at).toBeNull();
});

it("fails with error when the server rejects expires_at.", async () => {
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/api_tokens`;
const expectedErrorMessage = "expires_at: must not be in the past";

expect.assertions(2);

mock
.onPost(endpoint)
.reply(422, { errors: { expires_at: ["must not be in the past"] } });

try {
await apiTokensAPI.create({
...params,
expires_at: "2020-01-01T00:00:00Z",
});
} catch (error) {
expect(error).toBeInstanceOf(MailtrapError);

if (error instanceof MailtrapError) {
expect(error.message).toEqual(expectedErrorMessage);
}
}
});

it("fails with error.", async () => {
const expectedErrorMessage = "Request failed with status code 404";

Expand Down Expand Up @@ -223,6 +290,70 @@ describe("lib/api/resources/ApiTokens: ", () => {
expect(result).toEqual(responseData);
});

it("sends no request body when params are omitted.", async () => {
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/api_tokens/${tokenId}/reset`;

expect.assertions(1);

mock.onPost(endpoint).reply(200, responseData);
await apiTokensAPI.reset(tokenId);

expect(mock.history.post[0].data).toBeUndefined();
});

it("sends expires_at in the request body when provided.", async () => {
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/api_tokens/${tokenId}/reset`;
const expiresAt = "2027-06-01T00:00:00Z";

expect.assertions(1);

mock
.onPost(endpoint)
.reply(200, { ...responseData, expires_at: expiresAt });
await apiTokensAPI.reset(tokenId, { expires_at: expiresAt });

expect(JSON.parse(mock.history.post[0].data)).toEqual({
expires_at: expiresAt,
});
});

it("sends explicit null expires_at for a token that never expires.", async () => {
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/api_tokens/${tokenId}/reset`;

expect.assertions(2);

mock.onPost(endpoint).reply(200, responseData);
await apiTokensAPI.reset(tokenId, { expires_at: null });

const body = JSON.parse(mock.history.post[0].data);

expect("expires_at" in body).toEqual(true);
expect(body.expires_at).toBeNull();
});

it("fails with error when the server rejects expires_at.", async () => {
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/api_tokens/${tokenId}/reset`;
const expectedErrorMessage = "expires_at: must not be in the past";

expect.assertions(2);

mock
.onPost(endpoint)
.reply(422, { errors: { expires_at: ["must not be in the past"] } });

try {
await apiTokensAPI.reset(tokenId, {
expires_at: "2020-01-01T00:00:00Z",
});
} catch (error) {
expect(error).toBeInstanceOf(MailtrapError);

if (error instanceof MailtrapError) {
expect(error.message).toEqual(expectedErrorMessage);
}
}
});

it("fails with error.", async () => {
const expectedErrorMessage = "Request failed with status code 404";

Expand Down
16 changes: 15 additions & 1 deletion src/lib/api/resources/ApiTokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ApiToken,
ApiTokenWithToken,
CreateApiTokenRequest,
ResetApiTokenRequest,
} from "../../../types/api/api-tokens";

const { CLIENT_SETTINGS } = CONFIG;
Expand Down Expand Up @@ -33,6 +34,9 @@ export default class ApiTokensApi {
/**
* Create a new API token for the account with the given name and resource permissions.
* The full token value is returned only in the response of this call — store it securely.
* Unless `expires_at` is provided, the token expiration falls back to the server
* default (a 1-year default is being rolled out); pass `expires_at: null` for a
* token that never expires.
*/
public async create(params: CreateApiTokenRequest) {
const url = this.apiTokensURL;
Expand All @@ -54,10 +58,20 @@ export default class ApiTokensApi {
* Reset an API token: expires the existing token and returns a new one with
* the same permissions. The new token value is returned only in this response —
* store it securely. Only tokens that have not already been reset can be reset.
* Unless `expires_at` is provided, the new token expiration falls back to the
* server default (a 1-year default is being rolled out); pass `expires_at: null`
* for a token that never expires.
*/
public async reset(id: number) {
public async reset(id: number, params?: ResetApiTokenRequest) {
const url = `${this.apiTokensURL}/${id}/reset`;

if (params && "expires_at" in params) {
return this.client.post<ApiTokenWithToken, ApiTokenWithToken>(
url,
params
);
}

return this.client.post<ApiTokenWithToken, ApiTokenWithToken>(url);
}

Expand Down
17 changes: 17 additions & 0 deletions src/types/api/api-tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ export type ResourcePermission = {

export type CreateApiTokenRequest = {
name: string;
/**
* Optional token expiration as an ISO 8601 date-time.
* Omit for the server default (a 1-year default is being rolled out).
* Pass explicit `null` for a token that never expires.
* Past or more-than-5-years-ahead values are rejected with 422.
*/
expires_at?: string | null;
resources?: ResourcePermissionInput[];
};

Expand All @@ -31,3 +38,13 @@ export type ApiToken = {
export type ApiTokenWithToken = ApiToken & {
token: string;
};

export type ResetApiTokenRequest = {
/**
* Optional expiration for the new token as an ISO 8601 date-time.
* Omit for the server default (a 1-year default is being rolled out).
* Pass explicit `null` for a token that never expires.
* Past or more-than-5-years-ahead values are rejected with 422.
*/
expires_at?: string | null;
};
Loading