From 54df4caef91c8a07f7080badefead4131628e1b5 Mon Sep 17 00:00:00 2001 From: Patrick Arlt Date: Thu, 10 Mar 2022 15:12:51 -0800 Subject: [PATCH] feat(arcgis-rest-request): refresh session and retry with new token for invalid token errors * feat(arcgis-rest-request): refresh session and retry with new token for invalid token errors * chore(arcgis-rest-request): rename to refreshCrednetials and cleanup * chore(arcgis-rest-js): code review suggestions Co-authored-by: Noah Mulfinger Co-authored-by: Noah Mulfinger --- .../groups/update-user-membership.test.ts | 2 +- .../test/sharing/access.test.ts | 2 +- .../sharing/share-item-with-group.test.ts | 2 +- .../sharing/unshare-item-with-group.test.ts | 2 +- .../src/ApplicationCredentialsManager.ts | 2 +- .../src/ArcGISIdentityManager.ts | 63 +++++----- .../arcgis-rest-request/src/fetch-token.ts | 1 + packages/arcgis-rest-request/src/request.ts | 87 ++++++++++---- .../src/utils/IAuthenticationManager.ts | 25 ++++ .../src/utils/IRequestOptions.ts | 1 - .../test/ApplicationSession.test.ts | 2 +- .../test/ArcGISIdentityManager.test.ts | 10 +- .../arcgis-rest-request/test/request.test.ts | 113 +++++++++++++++++- .../test/utils/ArcGISAuthError.test.ts | 28 ++--- .../test/utils/check-for-errors.test.ts | 4 +- typedoc.json | 1 + 16 files changed, 261 insertions(+), 84 deletions(-) diff --git a/packages/arcgis-rest-portal/test/groups/update-user-membership.test.ts b/packages/arcgis-rest-portal/test/groups/update-user-membership.test.ts index b44cfc5c48..392b875856 100644 --- a/packages/arcgis-rest-portal/test/groups/update-user-membership.test.ts +++ b/packages/arcgis-rest-portal/test/groups/update-user-membership.test.ts @@ -16,7 +16,7 @@ describe("udpate-user-membership", () => { }); // make sure session doesnt cache metadata - MOCK_USER_SESSION.refreshSession() + MOCK_USER_SESSION.refreshCredentials() .then(() => done()) .catch(); }); diff --git a/packages/arcgis-rest-portal/test/sharing/access.test.ts b/packages/arcgis-rest-portal/test/sharing/access.test.ts index 7722391552..f53a0f28ee 100644 --- a/packages/arcgis-rest-portal/test/sharing/access.test.ts +++ b/packages/arcgis-rest-portal/test/sharing/access.test.ts @@ -23,7 +23,7 @@ describe("setItemAccess()", () => { }); // make sure session doesnt cache metadata - MOCK_USER_SESSION.refreshSession() + MOCK_USER_SESSION.refreshCredentials() .then(() => done()) .catch(); }); diff --git a/packages/arcgis-rest-portal/test/sharing/share-item-with-group.test.ts b/packages/arcgis-rest-portal/test/sharing/share-item-with-group.test.ts index 33bce512bd..65c2485717 100644 --- a/packages/arcgis-rest-portal/test/sharing/share-item-with-group.test.ts +++ b/packages/arcgis-rest-portal/test/sharing/share-item-with-group.test.ts @@ -97,7 +97,7 @@ describe("shareItemWithGroup() ::", () => { }); // make sure session doesnt cache metadata - MOCK_USER_SESSION.refreshSession() + MOCK_USER_SESSION.refreshCredentials() .then(() => done()) .catch(); }); diff --git a/packages/arcgis-rest-portal/test/sharing/unshare-item-with-group.test.ts b/packages/arcgis-rest-portal/test/sharing/unshare-item-with-group.test.ts index 3cbcaf0803..d720e22ece 100644 --- a/packages/arcgis-rest-portal/test/sharing/unshare-item-with-group.test.ts +++ b/packages/arcgis-rest-portal/test/sharing/unshare-item-with-group.test.ts @@ -53,7 +53,7 @@ describe("unshareItemWithGroup() ::", () => { }); // make sure session doesnt cache metadata - MOCK_USER_SESSION.refreshSession() + MOCK_USER_SESSION.refreshCredentials() .then(() => done()) .catch(); }); diff --git a/packages/arcgis-rest-request/src/ApplicationCredentialsManager.ts b/packages/arcgis-rest-request/src/ApplicationCredentialsManager.ts index afce62d649..e6754411af 100644 --- a/packages/arcgis-rest-request/src/ApplicationCredentialsManager.ts +++ b/packages/arcgis-rest-request/src/ApplicationCredentialsManager.ts @@ -122,7 +122,7 @@ export class ApplicationCredentialsManager implements IAuthenticationManager { ); } - public refreshSession() { + public refreshCredentials() { return this.refreshToken().then(() => this); } } diff --git a/packages/arcgis-rest-request/src/ArcGISIdentityManager.ts b/packages/arcgis-rest-request/src/ArcGISIdentityManager.ts index e4141622d6..2a8e3480bc 100644 --- a/packages/arcgis-rest-request/src/ArcGISIdentityManager.ts +++ b/packages/arcgis-rest-request/src/ArcGISIdentityManager.ts @@ -157,7 +157,7 @@ export interface IArcGISIdentityManagerOptions { redirectUri?: string; /** - * OAuth 2.0 refresh token from a previous user session. + * OAuth 2.0 refresh token. */ refreshToken?: string; @@ -177,7 +177,7 @@ export interface IArcGISIdentityManagerOptions { password?: string; /** - * OAuth 2.0 access token from a previous user session. + * OAuth 2.0 access token. */ token?: string; @@ -239,7 +239,7 @@ export interface IArcGISIdentityManagerOptions { * * {@linkcode ArcGISIdentityManager.fromCredential} - For creating an `ArcGISIdentityManager` instance from a `Credentials` object in the ArcGIS JS API `IdentityManager` * * {@linkcode ArcGISIdentityManager.signIn} - Authenticate directly with a users username and password. * - * Once a session has been created there are additional utilities: + * Once a manager is created there are additional utilities: * * * {@linkcode ArcGISIdentityManager.serialize} can be used to create a JSON object representing an instance of `ArcGISIdentityManager` * * {@linkcode ArcGISIdentityManager.deserialize} will create a new `ArcGISIdentityManager` from a JSON object created with {@linkcode ArcGISIdentityManager.serialize} @@ -288,7 +288,7 @@ export class ArcGISIdentityManager implements IAuthenticationManager { } /** - * Returns `true` if this session can be refreshed and `false` if it cannot. + * Returns `true` if these credentials can be refreshed and `false` if it cannot. */ get canRefresh() { if (this.username && this.password) { @@ -542,8 +542,8 @@ export class ArcGISIdentityManager implements IAuthenticationManager { return Promise.reject(new ArcGISAuthError(errorMessage, error)); } - // create a function to create the final UserSession from the token info. - function createSession( + // create a function to create the final ArcGISIdentityManager from the token info. + function createManager( oauthInfo: IFetchTokenResponse, originalUrl: string ) { @@ -619,7 +619,7 @@ export class ArcGISIdentityManager implements IAuthenticationManager { } }) .then((tokenResponse) => { - return createSession( + return createManager( { ...tokenResponse, ...state }, state.originalUrl ); @@ -631,7 +631,7 @@ export class ArcGISIdentityManager implements IAuthenticationManager { if (!pkce && params.access_token) { return Promise.resolve( - createSession( + createManager( { token: params.access_token, expires: new Date( @@ -650,7 +650,7 @@ export class ArcGISIdentityManager implements IAuthenticationManager { } /** - * Request session information from the parent application + * Request credentials information from the parent application * * When an application is embedded into another application via an IFrame, the embedded app can * use `window.postMessage` to request credentials from the host application. This function wraps @@ -693,9 +693,9 @@ export class ArcGISIdentityManager implements IAuthenticationManager { { type: "arcgis:auth:requestCredential" }, parentOrigin ); - }).then((session) => { + }).then((manager) => { win.removeEventListener("message", handler, false); - return session; + return manager; }); } @@ -834,11 +834,11 @@ export class ArcGISIdentityManager implements IAuthenticationManager { /** * Revokes all active tokens for a provided {@linkcode ArcGISIdentityManager}. The can be considered the equivalent to signing the user out of your application. */ - public static destroy(session: ArcGISIdentityManager) { + public static destroy(manager: ArcGISIdentityManager) { return revokeToken({ - clientId: session.clientId, - portal: session.portal, - token: session.refreshToken || session.token + clientId: manager.clientId, + portal: manager.portal, + token: manager.refreshToken || manager.token }); } @@ -848,10 +848,10 @@ export class ArcGISIdentityManager implements IAuthenticationManager { public static fromToken( options: IFromTokenOptions ): Promise { - const session = new ArcGISIdentityManager(options); + const manager = new ArcGISIdentityManager(options); - return session.getUser().then(() => { - return session; + return manager.getUser().then(() => { + return manager; }); } @@ -861,10 +861,10 @@ export class ArcGISIdentityManager implements IAuthenticationManager { * If possible you should use {@linkcode ArcGISIdentityManager.beginOAuth2} to authenticate users in a browser or {@linkcode ArcGISIdentityManager.authorize} for authenticating users with a web server. */ public static signIn(options: ISignInOptions) { - const session = new ArcGISIdentityManager(options); + const manager = new ArcGISIdentityManager(options); - return session.getUser().then(() => { - return session; + return manager.getUser().then(() => { + return manager; }); } @@ -1004,7 +1004,7 @@ export class ArcGISIdentityManager implements IAuthenticationManager { * Returns authentication in a format useable in the [ArcGIS API for JavaScript](https://developers.arcgis.com/javascript/). * * ```js - * esriId.registerToken(session.toCredential()); + * esriId.registerToken(manager.toCredential()); * ``` * * @returns ICredential @@ -1023,7 +1023,7 @@ export class ArcGISIdentityManager implements IAuthenticationManager { * Returns information about the currently logged in [user](https://developers.arcgis.com/rest/users-groups-and-items/user.htm). Subsequent calls will *not* result in additional web traffic. * * ```js - * session.getUser() + * manager.getUser() * .then(response => { * console.log(response.role); // "org_admin" * }) @@ -1061,7 +1061,7 @@ export class ArcGISIdentityManager implements IAuthenticationManager { * Returns information about the currently logged in user's [portal](https://developers.arcgis.com/rest/users-groups-and-items/portal-self.htm). Subsequent calls will *not* result in additional web traffic. * * ```js - * session.getPortal() + * manager.getPortal() * .then(response => { * console.log(portal.name); // "City of ..." * }) @@ -1099,7 +1099,7 @@ export class ArcGISIdentityManager implements IAuthenticationManager { * Returns the username for the currently logged in [user](https://developers.arcgis.com/rest/users-groups-and-items/user.htm). Subsequent calls will *not* result in additional web traffic. This is also used internally when a username is required for some requests but is not present in the options. * * * ```js - * session.getUsername() + * manager.getUsername() * .then(response => { * console.log(response); // "casey_jones" * }) @@ -1199,9 +1199,7 @@ export class ArcGISIdentityManager implements IAuthenticationManager { /** * Manually refreshes the current `token` and `tokenExpires`. */ - public refreshSession( - requestOptions?: ITokenRequestOptions - ): Promise { + public refreshCredentials(requestOptions?: ITokenRequestOptions) { // make sure subsequent calls to getUser() don't returned cached metadata this._user = null; @@ -1304,7 +1302,7 @@ export class ArcGISIdentityManager implements IAuthenticationManager { error: { name: "tokenExpiredError", message: - "Session token was expired, and not returned to the child application" + "Token was expired, and not returned to the child application" } }; } @@ -1444,11 +1442,11 @@ export class ArcGISIdentityManager implements IAuthenticationManager { } if (!this._pendingTokenRequests[this.portal]) { - this._pendingTokenRequests[this.portal] = this.refreshSession( + this._pendingTokenRequests[this.portal] = this.refreshCredentials( requestOptions - ).then((session) => { + ).then((manager) => { this._pendingTokenRequests[this.portal] = null; - return session.token; + return manager.token; }); } @@ -1499,6 +1497,7 @@ export class ArcGISIdentityManager implements IAuthenticationManager { }, ...requestOptions }; + return fetchToken(`${this.portal}/oauth2/token`, options).then( (response) => { this._token = response.token; diff --git a/packages/arcgis-rest-request/src/fetch-token.ts b/packages/arcgis-rest-request/src/fetch-token.ts index 28a7cd4de4..b509c429f9 100644 --- a/packages/arcgis-rest-request/src/fetch-token.ts +++ b/packages/arcgis-rest-request/src/fetch-token.ts @@ -30,6 +30,7 @@ export function fetchToken( requestOptions: ITokenRequestOptions ): Promise { const options: IRequestOptions = requestOptions; + // we generate a response, so we can't return the raw response options.rawResponse = false; diff --git a/packages/arcgis-rest-request/src/request.ts b/packages/arcgis-rest-request/src/request.ts index 4a9013530e..b76758eb5e 100644 --- a/packages/arcgis-rest-request/src/request.ts +++ b/packages/arcgis-rest-request/src/request.ts @@ -75,10 +75,12 @@ export class ArcGISAuthError extends ArcGISRequestError { code === "AUTHENTICATION_ERROR_CODE" ? message : `${code}: ${message}`; } - public retry(getSession: IRetryAuthError, retryLimit = 3) { + public retry(getSession: IRetryAuthError, retryLimit = 1) { let tries = 0; const retryRequest = (resolve: any, reject: any) => { + tries = tries + 1; + getSession(this.url, this.options) .then((session) => { const newOptions = { @@ -86,8 +88,7 @@ export class ArcGISAuthError extends ArcGISRequestError { ...{ authentication: session } }; - tries = tries + 1; - return request(this.url, newOptions); + return internalRequest(this.url, newOptions); }) .then((response) => { resolve(response); @@ -95,7 +96,11 @@ export class ArcGISAuthError extends ArcGISRequestError { .catch((e) => { if (e.name === "ArcGISAuthError" && tries < retryLimit) { retryRequest(resolve, reject); - } else if (e.name === "ArcGISAuthError" && tries >= retryLimit) { + } else if ( + e.name === this.name && + e.message === this.message && + tries >= retryLimit + ) { reject(this); } else { reject(e); @@ -171,30 +176,17 @@ export function checkForErrors( } /** - * ```js - * import { request } from '@esri/arcgis-rest-request'; - * // - * request('https://www.arcgis.com/sharing/rest') - * .then(response) // response.currentVersion === 5.2 - * // - * request('https://www.arcgis.com/sharing/rest', { - * httpMethod: "GET" - * }) - * // - * request('https://www.arcgis.com/sharing/rest/search', { - * params: { q: 'parks' } - * }) - * .then(response) // response.total => 78379 - * ``` - * Generic method for making HTTP requests to ArcGIS REST API endpoints. + * This is the internal implementation of `request` without the automatic retry behavior to prevent + * infinite loops when a server continues to return invalid token errors. * * @param url - The URL of the ArcGIS REST API endpoint. * @param requestOptions - Options for the request, including parameters relevant to the endpoint. * @returns A Promise that will resolve with the data from the response. + * @internal */ -export function request( +export function internalRequest( url: string, - requestOptions: IRequestOptions = { params: { f: "json" } } + requestOptions: IRequestOptions ): Promise { const defaults = getDefaultRequestOptions(); const options: IRequestOptions = { @@ -270,6 +262,10 @@ export function request( authentication = options.authentication; } + // for errors in GET requests we want the URL passed to the error to be the URL before + // query params are applied. + const originalUrl = url; + return ( authentication ? authentication.getToken(url).catch((err) => { @@ -416,7 +412,7 @@ export function request( if ((params.f === "json" || params.f === "geojson") && !rawResponse) { const response = checkForErrors( data, - url, + originalUrl, params, options, originalAuthError @@ -444,3 +440,48 @@ export function request( } }); } + +/** + * ```js + * import { request } from '@esri/arcgis-rest-request'; + * // + * request('https://www.arcgis.com/sharing/rest') + * .then(response) // response.currentVersion === 5.2 + * // + * request('https://www.arcgis.com/sharing/rest', { + * httpMethod: "GET" + * }) + * // + * request('https://www.arcgis.com/sharing/rest/search', { + * params: { q: 'parks' } + * }) + * .then(response) // response.total => 78379 + * ``` + * Generic method for making HTTP requests to ArcGIS REST API endpoints. + * + * @param url - The URL of the ArcGIS REST API endpoint. + * @param requestOptions - Options for the request, including parameters relevant to the endpoint. + * @returns A Promise that will resolve with the data from the response. + */ +export function request( + url: string, + requestOptions: IRequestOptions = { params: { f: "json" } } +): Promise { + return internalRequest(url, requestOptions).catch((e) => { + if ( + e instanceof ArcGISAuthError && + e.code === 498 && + e.message === "498: Invalid token." && + requestOptions.authentication && + typeof requestOptions.authentication !== "string" && + requestOptions.authentication.canRefresh && + requestOptions.authentication.refreshCredentials + ) { + return e.retry(() => { + return (requestOptions.authentication as any).refreshCredentials(); + }, 1); + } else { + return Promise.reject(e); + } + }); +} diff --git a/packages/arcgis-rest-request/src/utils/IAuthenticationManager.ts b/packages/arcgis-rest-request/src/utils/IAuthenticationManager.ts index 9a1cb2dee7..888d042c0f 100644 --- a/packages/arcgis-rest-request/src/utils/IAuthenticationManager.ts +++ b/packages/arcgis-rest-request/src/utils/IAuthenticationManager.ts @@ -17,6 +17,31 @@ export interface IAuthenticationManager { * Defaults to 'https://www.arcgis.com/sharing/rest'. */ portal: string; + + /** + * Returns the proper token for a given URL and request options. + * @param url The requested URL. + * @param requestOptions the requests options. + */ getToken(url: string, requestOptions?: ITokenRequestOptions): Promise; + + /** + * Returns the proper [`credentials`] option for `fetch` for a given domain. + * See [trusted server](https://enterprise.arcgis.com/en/portal/latest/administer/windows/configure-security.htm#ESRI_SECTION1_70CC159B3540440AB325BE5D89DBE94A). + * Used internally by underlying request methods to add support for specific security considerations. + * + * @param url The url of the request + * @returns "include" or "same-origin" + */ getDomainCredentials?(url: string): RequestCredentials; + + /** + * Should return `true` if these credentials can be refreshed and `false` if it cannot. The + */ + canRefresh?: boolean; + + /** + * Refresh the stored credentials. + */ + refreshCredentials?(requestOptions?: ITokenRequestOptions): Promise; } diff --git a/packages/arcgis-rest-request/src/utils/IRequestOptions.ts b/packages/arcgis-rest-request/src/utils/IRequestOptions.ts index 425fadc9e5..0cf968c84f 100644 --- a/packages/arcgis-rest-request/src/utils/IRequestOptions.ts +++ b/packages/arcgis-rest-request/src/utils/IRequestOptions.ts @@ -51,7 +51,6 @@ export interface IRequestOptions { headers?: { [key: string]: any; }; - /** * Suppress any ArcGIS REST JS related warnings for this request. */ diff --git a/packages/arcgis-rest-request/test/ApplicationSession.test.ts b/packages/arcgis-rest-request/test/ApplicationSession.test.ts index dedd7dd69c..f8bf22295d 100644 --- a/packages/arcgis-rest-request/test/ApplicationSession.test.ts +++ b/packages/arcgis-rest-request/test/ApplicationSession.test.ts @@ -136,7 +136,7 @@ describe("ApplicationCredentialsManager", () => { }); session - .refreshSession() + .refreshCredentials() .then((s) => { expect(s).toBe(session); done(); diff --git a/packages/arcgis-rest-request/test/ArcGISIdentityManager.test.ts b/packages/arcgis-rest-request/test/ArcGISIdentityManager.test.ts index 5ca390a20a..bf06ca6248 100644 --- a/packages/arcgis-rest-request/test/ArcGISIdentityManager.test.ts +++ b/packages/arcgis-rest-request/test/ArcGISIdentityManager.test.ts @@ -714,7 +714,7 @@ describe("ArcGISIdentityManager", () => { }); }); - describe(".refreshSession()", () => { + describe(".refreshCredentials()", () => { it("should refresh with a username and password if expired", (done) => { const session = new ArcGISIdentityManager({ username: "c@sey", @@ -730,7 +730,7 @@ describe("ArcGISIdentityManager", () => { }); session - .refreshSession() + .refreshCredentials() .then((s) => { expect(s.token).toBe("token"); expect(s.tokenExpires).toEqual(TOMORROW); @@ -759,7 +759,7 @@ describe("ArcGISIdentityManager", () => { }); session - .refreshSession() + .refreshCredentials() .then((s) => { expect(s.token).toBe("newToken"); expect(s.tokenExpires.getTime()).toBeGreaterThan( @@ -790,7 +790,7 @@ describe("ArcGISIdentityManager", () => { }); session - .refreshSession() + .refreshCredentials() .then((s) => { expect(s.token).toBe("newToken"); expect(s.tokenExpires.getTime()).toBeGreaterThan( @@ -816,7 +816,7 @@ describe("ArcGISIdentityManager", () => { expect(session.canRefresh).toBe(false); - session.refreshSession().catch((e) => { + session.refreshCredentials().catch((e) => { expect(e instanceof ArcGISAuthError).toBeTruthy(); expect(e.name).toBe("ArcGISAuthError"); expect(e.message).toBe("Unable to refresh token."); diff --git a/packages/arcgis-rest-request/test/request.test.ts b/packages/arcgis-rest-request/test/request.test.ts index 39a20720a4..b78daf887f 100644 --- a/packages/arcgis-rest-request/test/request.test.ts +++ b/packages/arcgis-rest-request/test/request.test.ts @@ -5,7 +5,8 @@ import { request, ErrorTypes, setDefaultRequestOptions, - IRequestOptions + IRequestOptions, + ArcGISIdentityManager } from "../src/index.js"; import fetchMock from "fetch-mock"; import { @@ -16,6 +17,7 @@ import { MockParamBuilder } from "./mocks/param-builder.js"; import { ArcGISOnlineError } from "./mocks/errors.js"; import { WebMapAsText, WebMapAsJSON } from "./mocks/webmap.js"; import { GeoJSONFeatureCollection } from "./mocks/geojson-feature-collection.js"; +import { TOMORROW } from "../../../scripts/test-helpers.js"; describe("request()", () => { afterEach(() => { @@ -560,4 +562,113 @@ describe("request()", () => { console.warn = oldWarn; }); }); + + describe("automatic retry", () => { + it("should retry requests that fail with an invalid token error", () => { + fetchMock.getOnce( + "https://www.arcgis.com/sharing/rest/portals/self?f=json&token=INVALID_TOKEN", + { error: { code: 498, message: "Invalid token.", details: [] } } + ); + + fetchMock.getOnce( + "https://www.arcgis.com/sharing/rest/portals/self?f=json&token=VALID_TOKEN", + { + user: { + username: "c@sey" + } + } + ); + + const session = new ArcGISIdentityManager({ + clientId: "clientId", + token: "INVALID_TOKEN", + username: "c@sey", + refreshToken: "refreshToken", + refreshTokenExpires: TOMORROW + }); + + expect(session.canRefresh).toBe(true); + + fetchMock.post("https://www.arcgis.com/sharing/rest/oauth2/token", { + access_token: "VALID_TOKEN", + expires_in: 60, + username: " c@sey" + }); + + return request("https://www.arcgis.com/sharing/rest/portals/self", { + httpMethod: "GET", + authentication: session + }) + .then((response) => { + expect(response.user.username).toBe("c@sey"); + }) + .catch((e) => { + fail(e); + }); + }); + + it("should throw the error from the refresh call if refreshing fails", () => { + fetchMock.get( + "https://www.arcgis.com/sharing/rest/portals/self?f=json&token=INVALID_TOKEN", + { error: { code: 498, message: "Invalid token.", details: [] } } + ); + + const session = new ArcGISIdentityManager({ + clientId: "clientId", + token: "INVALID_TOKEN", + username: "c@sey", + refreshToken: "refreshToken", + refreshTokenExpires: TOMORROW + }); + + fetchMock.post("https://www.arcgis.com/sharing/rest/oauth2/token", { + error: { + code: 498, + error: "invalid_request", + error_description: "Invalid refresh_token", + message: "Invalid refresh_token", + details: [] + } + }); + + return request("https://www.arcgis.com/sharing/rest/portals/self", { + httpMethod: "GET", + authentication: session + }).catch((e) => { + expect(e.code).toBe(498); + expect(e.message).toBe("498: Invalid refresh_token"); + return; + }); + }); + + it("should throw an error if it also fails with the new token", () => { + fetchMock.get( + "https://www.arcgis.com/sharing/rest/portals/self?f=json&token=TOKEN", + { error: { code: 498, message: "Invalid token.", details: [] } } + ); + + const session = new ArcGISIdentityManager({ + clientId: "clientId", + token: "TOKEN", + username: "c@sey", + refreshToken: "refreshToken", + refreshTokenExpires: TOMORROW + }); + + fetchMock.post("https://www.arcgis.com/sharing/rest/oauth2/token", { + access_token: "TOKEN", + expires_in: 60, + username: " c@sey" + }); + + return request("https://www.arcgis.com/sharing/rest/portals/self", { + httpMethod: "GET", + authentication: session + }).catch((e) => { + expect(e.code).toBe(498); + expect(e.message).toBe("498: Invalid token."); + return; + }); + }); + }); }); diff --git a/packages/arcgis-rest-request/test/utils/ArcGISAuthError.test.ts b/packages/arcgis-rest-request/test/utils/ArcGISAuthError.test.ts index 28c705cac9..7310e7b2ec 100644 --- a/packages/arcgis-rest-request/test/utils/ArcGISAuthError.test.ts +++ b/packages/arcgis-rest-request/test/utils/ArcGISAuthError.test.ts @@ -4,12 +4,12 @@ import { ArcGISAuthError, IRetryAuthError, - ErrorTypes, + ErrorTypes } from "../../src/index.js"; import { ArcGISOnlineAuthError, ArcGISOnlineError, - GenerateTokenError, + GenerateTokenError } from "./../mocks/errors.js"; import { request } from "../../src/request.js"; import fetchMock from "fetch-mock"; @@ -56,7 +56,7 @@ describe("ArcGISRequestError", () => { }, retryHandler(url, options) { return Promise.resolve(MockAuth); - }, + } }; it("should allow retrying a request with a new or updated session", (done) => { @@ -71,21 +71,21 @@ describe("ArcGISRequestError", () => { title: "Test Map", tags: "foo", type: "Web Map", - f: "json", - }, + f: "json" + } } ); fetchMock.once("*", { success: true, id: "abc", - folder: null, + folder: null }); const retryHandlerSpy = spyOn(MockAuth, "retryHandler").and.callThrough(); error - .retry(MockAuth.retryHandler, 1) + .retry(MockAuth.retryHandler, 3) .then((response: any) => { const [url, options]: [string, RequestInit] = fetchMock.lastCall("*"); expect(url).toEqual( @@ -118,8 +118,8 @@ describe("ArcGISRequestError", () => { title: "Test Map", tags: "foo", type: "Web Map", - f: "json", - }, + f: "json" + } } ); @@ -127,7 +127,7 @@ describe("ArcGISRequestError", () => { const retryHandlerSpy = spyOn(MockAuth, "retryHandler").and.callThrough(); - error.retry(MockAuth.retryHandler).catch((e: any) => { + error.retry(MockAuth.retryHandler, 3).catch((e: any) => { const [url, options]: [string, RequestInit] = fetchMock.lastCall("*"); expect(url).toEqual( "http://www.arcgis.com/sharing/rest/content/users/caseyjones/addItem" @@ -155,8 +155,8 @@ describe("ArcGISRequestError", () => { httpMethod: "POST", params: { type: "Web Map", - f: "json", - }, + f: "json" + } } ); @@ -185,8 +185,8 @@ describe("ArcGISRequestError", () => { username: "correct", password: "incorrect", expiration: 10260, - referer: "localhost", - }, + referer: "localhost" + } }).catch((err) => { expect(err.name).toBe(ErrorTypes.ArcGISAuthError); done(); diff --git a/packages/arcgis-rest-request/test/utils/check-for-errors.test.ts b/packages/arcgis-rest-request/test/utils/check-for-errors.test.ts index ce97505cef..6a744e7fba 100644 --- a/packages/arcgis-rest-request/test/utils/check-for-errors.test.ts +++ b/packages/arcgis-rest-request/test/utils/check-for-errors.test.ts @@ -5,7 +5,7 @@ import { checkForErrors, warn, ArcGISRequestError, - ArcGISAuthError, + ArcGISAuthError } from "../../src/index.js"; import { SharingRestInfo } from "./../mocks/sharing-rest-info.js"; import { @@ -17,7 +17,7 @@ import { ArcGISOnlineErrorNoCode, ArcGISServerTokenRequired, ArcGISOnlineAuthError, - BillingErrorWithCode200, + BillingErrorWithCode200 } from "./../mocks/errors.js"; describe("checkForErrors", () => { diff --git a/typedoc.json b/typedoc.json index f01b57abf1..e4d70ce945 100644 --- a/typedoc.json +++ b/typedoc.json @@ -2,6 +2,7 @@ "out": "typedoc", "json": "typedoc/typedoc.json", "logLevel": "Verbose", + "excludeInternal": true, "packages": [ "./packages/arcgis-rest-auth/", "./packages/arcgis-rest-demographics/",