From f0f369ca72b24f57db1d84d1555d0c815e1510a6 Mon Sep 17 00:00:00 2001 From: Lyle Schemmerling Date: Tue, 2 Jan 2024 14:58:02 -0700 Subject: [PATCH 1/2] fix the typescript client not properly handling get query params with an array value --- src/DefaultRESTClient.ts | 16 +++++++--- test/FusionAuthClientTest.ts | 60 ++++++++++++++++++++++++++---------- 2 files changed, 56 insertions(+), 20 deletions(-) diff --git a/src/DefaultRESTClient.ts b/src/DefaultRESTClient.ts index b45f26f..21bb9c1 100644 --- a/src/DefaultRESTClient.ts +++ b/src/DefaultRESTClient.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2020, FusionAuth, All Rights Reserved + * Copyright (c) 2019-2024, FusionAuth, All Rights Reserved * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -206,10 +206,18 @@ export default class DefaultRESTClient implements IRESTClient } private getQueryString() { - var queryString = ''; - for (let key in this.parameters) { + let queryString = ''; + const appendParam = (key: string, param: string) => { queryString += (queryString.length === 0) ? '?' : '&'; - queryString += key + '=' + encodeURIComponent(this.parameters[key]); + queryString += key + '=' + encodeURIComponent(param); + } + for (let key in this.parameters) { + const value = this.parameters[key]; + if (Array.isArray(value)) { + value.forEach(val => appendParam(key, val)) + } else { + appendParam(key, value); + } } return queryString; } diff --git a/test/FusionAuthClientTest.ts b/test/FusionAuthClientTest.ts index 82a9efa..041e8fa 100644 --- a/test/FusionAuthClientTest.ts +++ b/test/FusionAuthClientTest.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, FusionAuth, All Rights Reserved + * Copyright (c) 2019-2024, FusionAuth, All Rights Reserved * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ 'use strict'; -import {ApplicationRequest, FusionAuthClient, GrantType} from '../index'; +import { ApplicationRequest, FusionAuthClient, GrantType, SearchResponse } from '../index'; import * as chai from 'chai'; import ClientResponse from "../src/ClientResponse"; @@ -114,7 +114,7 @@ describe('#FusionAuthClient()', function () { } }); - it('Create, Patch and Delete a User', async () => { + it('Create, Patch, Search, and Delete a User', async () => { let clientResponse = await client.createUser(null, { user: { email: 'nodejs@fusionauth.io', @@ -130,8 +130,10 @@ describe('#FusionAuthClient()', function () { chai.expect(clientResponse.response).to.have.property('user'); chai.expect(clientResponse.response.user).to.have.property('id'); + const userId = clientResponse.response.user.id; + // Patch the user - clientResponse = await client.patchUser(clientResponse.response.user.id, { + clientResponse = await client.patchUser(userId, { user: { firstName: "Jan" } @@ -142,20 +144,46 @@ describe('#FusionAuthClient()', function () { chai.expect(clientResponse.response).to.have.property('user'); chai.expect(clientResponse.response.user.firstName).to.equal("Jan"); - clientResponse = await client.deleteUser(clientResponse.response.user.id); - chai.assert.strictEqual(clientResponse.statusCode, 200); - // Browser will return empty, node will return null, account for both scenarios - if (clientResponse.response === null) { - chai.assert.isNull(clientResponse.response); - } else { - chai.assert.isUndefined(clientResponse.response); + // create a second user and search them both + clientResponse = await client.createUser(null, { + user: { + email: 'node2@fusionauth.io', + firstName: 'Joan', + password: 'password' + }, + skipVerification: true, + sendSetPasswordEmail: false + }); + + const secondUserId = clientResponse.response.user.id; + const bothUsers = [userId, secondUserId]; + + const searchResp: ClientResponse = await client.searchUsersByIds(bothUsers); + chai.assert.strictEqual(searchResp.statusCode, 200); + chai.assert.strictEqual(searchResp.response.total, 2); + // make sure each user was returned + bothUsers.forEach(id => chai.assert.isNotNull(searchResp.response.users.find(user => user.id = id))); + + // delete both users + for (const id of bothUsers) { + clientResponse = await client.deleteUser(id); + chai.assert.strictEqual(clientResponse.statusCode, 200); + // Browser will return empty, node will return null, account for both scenarios + if (clientResponse.response === null) { + chai.assert.isNull(clientResponse.response); + } else { + chai.assert.isUndefined(clientResponse.response); + } } - try { - await client.retrieveUserByEmail('nodejs@fusionauth.io'); - chai.expect.fail("The user should have been deleted!"); - } catch (clientResponse) { - chai.assert.strictEqual(clientResponse.statusCode, 404); + // check that they are gone + for (const email of ['nodejs@fusionauth.io', 'node2@fusionauth.io']) { + try { + await client.retrieveUserByEmail(email); + chai.expect.fail(`The user with ${email} should have been deleted!`); + } catch (clientResponse) { + chai.assert.strictEqual(clientResponse.statusCode, 404); + } } }); From e7239f014e486a4806c30ae4b5176c652c8855fb Mon Sep 17 00:00:00 2001 From: Lyle Schemmerling Date: Wed, 3 Jan 2024 17:02:26 -0700 Subject: [PATCH 2/2] encode the key --- src/DefaultRESTClient.ts | 82 ++++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/src/DefaultRESTClient.ts b/src/DefaultRESTClient.ts index 21bb9c1..b64381f 100644 --- a/src/DefaultRESTClient.ts +++ b/src/DefaultRESTClient.ts @@ -14,10 +14,10 @@ * language governing permissions and limitations under the License. */ -import IRESTClient, {ErrorResponseHandler, ResponseHandler} from "./IRESTClient"; +import IRESTClient, { ErrorResponseHandler, ResponseHandler } from "./IRESTClient"; import ClientResponse from "./ClientResponse"; -import fetch, {BodyInit, RequestCredentials, Response} from 'node-fetch'; -import {URLSearchParams} from "url"; +import fetch, { BodyInit, RequestCredentials, Response } from 'node-fetch'; +import { URLSearchParams } from "url"; /** * @author Brett P @@ -37,6 +37,42 @@ export default class DefaultRESTClient implements IRESTClient constructor(public host: string) { } + /** + * A function that returns the JSON form of the response text. + * + * @param response + * @constructor + */ + static async JSONResponseHandler(response: Response): Promise> { + let clientResponse = new ClientResponse(); + + clientResponse.statusCode = response.status; + let type = response.headers.get("content-type"); + if (type && type.startsWith("application/json")) { + clientResponse.response = await response.json(); + } + + return clientResponse; + } + + /** + * A function that returns the JSON form of the response text. + * + * @param response + * @constructor + */ + static async ErrorJSONResponseHandler(response: Response): Promise> { + let clientResponse = new ClientResponse(); + + clientResponse.statusCode = response.status; + let type = response.headers.get("content-type"); + if (type && type.startsWith("application/json")) { + clientResponse.exception = await response.json(); + } + + return clientResponse; + } + /** * Sets the authorization header using a key * @@ -86,7 +122,7 @@ export default class DefaultRESTClient implements IRESTClient if (body) { body.forEach((value, name, searchParams) => { if (value && value.length > 0 && value != "null" && value != "undefined") { - body2.set(name,value); + body2.set(name, value); } }); body = body2; @@ -209,7 +245,7 @@ export default class DefaultRESTClient implements IRESTClient let queryString = ''; const appendParam = (key: string, param: string) => { queryString += (queryString.length === 0) ? '?' : '&'; - queryString += key + '=' + encodeURIComponent(param); + queryString += encodeURIComponent(key) + '=' + encodeURIComponent(param); } for (let key in this.parameters) { const value = this.parameters[key]; @@ -221,40 +257,4 @@ export default class DefaultRESTClient implements IRESTClient } return queryString; } - - /** - * A function that returns the JSON form of the response text. - * - * @param response - * @constructor - */ - static async JSONResponseHandler(response: Response): Promise> { - let clientResponse = new ClientResponse(); - - clientResponse.statusCode = response.status; - let type = response.headers.get("content-type"); - if (type && type.startsWith("application/json")) { - clientResponse.response = await response.json(); - } - - return clientResponse; - } - - /** - * A function that returns the JSON form of the response text. - * - * @param response - * @constructor - */ - static async ErrorJSONResponseHandler(response: Response): Promise> { - let clientResponse = new ClientResponse(); - - clientResponse.statusCode = response.status; - let type = response.headers.get("content-type"); - if (type && type.startsWith("application/json")) { - clientResponse.exception = await response.json(); - } - - return clientResponse; - } }