Skip to content

Commit

Permalink
Merge pull request #2 from hyperwallet/master
Browse files Browse the repository at this point in the history
Merge master
  • Loading branch information
akreisman-epam committed Dec 21, 2018
2 parents 0693be0 + 1082707 commit 564c30a
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 45 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ To write an app using the SDK
postalCode: "78701",
};

client.createUser(userDate, function(errors, body, res) {
client.createUser(userData, function(errors, body, res) {
if (errors) {
console.log("Create User Failed");
console.log(errors);
Expand Down
80 changes: 49 additions & 31 deletions src/utils/ApiClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,12 @@ export default class ApiClient {
* @param {api-callback} callback - The callback for this call
*/
doPost(partialUrl, data, params, callback) {
let contentType = "json";
let processResponse = this.wrapCallback(callback);
let contentType = "application/json";
let accept = "application/json";
let requestDataPromise = new Promise((resolve) => resolve(data));
if (this.isEncrypted) {
contentType = "application/jose+json";
processResponse = this.processEncryptedResponse("POST", callback);
accept = "application/jose+json";
requestDataPromise = this.encryption.encrypt(data);
}
requestDataPromise.then((requestData) => {
Expand All @@ -98,10 +98,10 @@ export default class ApiClient {
.auth(this.username, this.password)
.set("User-Agent", `Hyperwallet Node SDK v${this.version}`)
.type(contentType)
.accept("json")
.accept(accept)
.query(params)
.send(requestData)
.end(processResponse);
.end(this.wrapCallback("POST", callback));
}).catch(() => callback("Failed to encrypt body for POST request", undefined, undefined));
}

Expand All @@ -114,12 +114,12 @@ export default class ApiClient {
* @param {api-callback} callback - The callback for this call
*/
doPut(partialUrl, data, params, callback) {
let contentType = "json";
let processResponse = this.wrapCallback(callback);
let contentType = "application/json";
let accept = "application/json";
let requestDataPromise = new Promise((resolve) => resolve(data));
if (this.isEncrypted) {
contentType = "application/jose+json";
processResponse = this.processEncryptedResponse("PUT", callback);
accept = "application/jose+json";
requestDataPromise = this.encryption.encrypt(data);
}
requestDataPromise.then((requestData) => {
Expand All @@ -128,10 +128,10 @@ export default class ApiClient {
.auth(this.username, this.password)
.set("User-Agent", `Hyperwallet Node SDK v${this.version}`)
.type(contentType)
.accept("json")
.accept(accept)
.query(params)
.send(requestData)
.end(processResponse);
.end(this.wrapCallback("PUT", callback));
}).catch(() => callback("Failed to encrypt body for PUT request", undefined, undefined));
}

Expand All @@ -143,44 +143,57 @@ export default class ApiClient {
* @param {api-callback} callback - The callback for this call
*/
doGet(partialUrl, params, callback) {
let contentType = "json";
let contentType = "application/json";
let accept = "application/json";
if (this.isEncrypted) {
contentType = "application/jose+json";
accept = "application/jose+json";
}
request
.get(`${this.server}/rest/v3/${partialUrl}`)
.auth(this.username, this.password)
.set("User-Agent", `Hyperwallet Node SDK v${this.version}`)
.type(contentType)
.accept("json")
.accept(accept)
.query(params)
.end(this.isEncrypted ? this.processEncryptedResponse("GET", callback) : this.wrapCallback(callback));
.end(this.wrapCallback("GET", callback));
}

/**
* Wrap a callback to process possible API and network errors
*
* @param {string} httpMethod - The http method that is currently processing
* @param {api-callback} callback - The final callback
* @returns {function(err: Object, res: Object)} - The super agent callback
*
* @private
*/
wrapCallback(callback = () => null) {
wrapCallback(httpMethod, callback = () => null) {
return (err, res) => {
this.processResponse(err, res, callback);
if (res && ((!this.isEncrypted && res.type !== "application/json") || (this.isEncrypted && res.type !== "application/jose+json"))) {
callback([{
message: "Invalid Content-Type specified in Response Header",
}], res ? res.body : undefined, res);
return;
}
if (this.isEncrypted) {
this.processEncryptedResponse(httpMethod, err, res, callback);
} else {
this.processNonEncryptedResponse(err, res, callback);
}
};
}

/**
* Process response from server
* Process non encrypted response from server
*
* @param {Object} err - Error object
* @param {Object} res - Response object
* @param {api-callback} callback - The final callback
*
* @private
*/
processResponse(err, res, callback) {
processNonEncryptedResponse(err, res, callback) {
if (!err) {
callback(undefined, res.body, res);
return;
Expand All @@ -199,25 +212,30 @@ export default class ApiClient {
}

/**
* Makes decryption for encrypted response bodies
* Process encrypted response from server
*
* @param {string} httpMethod - The http method that is currently processing
* @param {api-callback} callback - The callback method to be invoked after decryption
* @param {Object} err - Error object
* @param {Object} res - Response object
* @param {api-callback} callback - The final callback
*
* @private
*/
processEncryptedResponse(httpMethod, callback) {
return (error, response) => {
if (!error) {
const responseBody = response.rawResponse ? response.rawResponse : response.text;
this.encryption.decrypt(responseBody)
.then((decryptedData) => {
callback(undefined, JSON.parse(decryptedData.payload.toString()), decryptedData);
})
.catch(() => callback(`Failed to decrypt response for ${httpMethod} request`, responseBody, responseBody));
} else {
this.processResponse(error, response, callback);
processEncryptedResponse(httpMethod, err, res, callback) {
if (!err) {
let responseBody = res.rawResponse ? res.rawResponse : res.text;
try {
responseBody = this.encryption.base64Encode(JSON.parse(res.text));
} catch (e) {
// nothing to do
}
};
this.encryption.decrypt(responseBody)
.then((decryptedData) => {
callback(undefined, JSON.parse(decryptedData.payload.toString()), decryptedData);
})
.catch(() => callback(`Failed to decrypt response for ${httpMethod} request`, responseBody, responseBody));
} else {
this.processNonEncryptedResponse(err, res, callback);
}
}
}
29 changes: 29 additions & 0 deletions src/utils/Encryption.js
Original file line number Diff line number Diff line change
Expand Up @@ -310,4 +310,33 @@ export default class Encryption {
callback(!error && response.statusCode === 200);
});
}

/**
* Convert encrypted string to array of Buffer
*
* @param {string} encryptedBody - Encrypted body to be decoded
*/
base64Decode(encryptedBody) {
const parts = encryptedBody.split(".");
const decodedParts = [];
parts.forEach(elem => {
decodedParts.push(jose.util.base64url.decode(elem));
});
const decodedBody = {};
decodedBody.parts = decodedParts;
return decodedBody;
}

/**
* Convert array of Buffer to encrypted string
*
* @param {string} decodedBody - Array of Buffer to be decoded to encrypted string
*/
base64Encode(decodedBody) {
const encodedParts = [];
decodedBody.parts.forEach(part => {
encodedParts.push(jose.util.base64url.encode(Buffer.from(JSON.parse(JSON.stringify(part)).data)));
});
return encodedParts.join(".");
}
}
82 changes: 69 additions & 13 deletions test/utils/ApiClient.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,10 +176,12 @@ describe("utils/ApiClient", () => {
.filteringPath(() => "/")
.matchHeader("Authorization", authHeader)
.matchHeader("User-Agent", `Hyperwallet Node SDK v${packageJson.version}`)
.matchHeader("Accept", "application/json")
.matchHeader("Accept", "application/jose+json")
.matchHeader("Content-Type", "application/jose+json")
.post("/", /.+/)
.reply(201, encryptedBody);
.reply(200, encryption.base64Decode(encryptedBody), {
"Content-Type": "application/jose+json",
});

clientWithEncryption.doPost("test", { message: "Test message" }, {}, (err, body, res) => {
expect(err).to.be.undefined();
Expand Down Expand Up @@ -376,10 +378,10 @@ describe("utils/ApiClient", () => {
.filteringPath(() => "/")
.matchHeader("Authorization", authHeader)
.matchHeader("User-Agent", `Hyperwallet Node SDK v${packageJson.version}`)
.matchHeader("Accept", "application/json")
.matchHeader("Accept", "application/jose+json")
.matchHeader("Content-Type", "application/jose+json")
.put("/", /.+/)
.reply(201, encryptedBody);
.reply(201, encryption.base64Decode(encryptedBody), { "Content-Type": "application/jose+json" });

clientWithEncryption.doPut("test", { message: "Test message" }, {}, (err, body, res) => {
expect(err).to.be.undefined();
Expand Down Expand Up @@ -448,10 +450,10 @@ describe("utils/ApiClient", () => {
.filteringPath(() => "/")
.matchHeader("Authorization", authHeader)
.matchHeader("User-Agent", `Hyperwallet Node SDK v${packageJson.version}`)
.matchHeader("Accept", "application/json")
.matchHeader("Accept", "application/jose+json")
.matchHeader("Content-Type", "application/jose+json")
.put("/", /.+/)
.reply(201, encryptedBody);
.reply(201, encryption.base64Decode(encryptedBody), { "Content-Type": "application/jose+json" });

clientWithEncryption.doPut("test", { message: "Test message" }, {}, (err, body, res) => {
err.should.be.deep.equal("Failed to decrypt response for PUT request");
Expand All @@ -478,15 +480,15 @@ describe("utils/ApiClient", () => {
.filteringPath(() => "/")
.matchHeader("Authorization", authHeader)
.matchHeader("User-Agent", `Hyperwallet Node SDK v${packageJson.version}`)
.matchHeader("Accept", "application/json")
.matchHeader("Accept", "application/jose+json")
.matchHeader("Content-Type", "application/jose+json")
.put("/", /.+/)
.reply(404, {
errors: [
"test1",
"test2",
],
});
}, { "Content-Type": "application/jose+json" });

clientWithEncryption.doPut("test", { message: "Test message" }, {}, (err, body, res) => {
err.should.be.deep.equal([
Expand Down Expand Up @@ -644,9 +646,9 @@ describe("utils/ApiClient", () => {
.filteringPath(() => "/")
.matchHeader("Authorization", authHeader)
.matchHeader("User-Agent", `Hyperwallet Node SDK v${packageJson.version}`)
.matchHeader("Accept", "application/json")
.matchHeader("Accept", "application/jose+json")
.get("/")
.reply(200, encryptedBody);
.reply(200, encryption.base64Decode(encryptedBody), { "Content-Type": "application/jose+json" });

clientWithEncryption.doGet("test", {}, (err, body, res) => {
expect(err).to.be.undefined();
Expand Down Expand Up @@ -687,9 +689,10 @@ describe("utils/ApiClient", () => {
const rawRes = {
body: "test",
status: 200,
type: "application/json",
};

const callback = client.wrapCallback((err, body, res) => {
const callback = client.wrapCallback("POST", (err, body, res) => {
expect(err).to.be.undefined();

body.should.be.equal("test");
Expand All @@ -711,9 +714,10 @@ describe("utils/ApiClient", () => {
],
},
status: 404,
type: "application/json",
};

const callback = client.wrapCallback((err, body, res) => {
const callback = client.wrapCallback("POST", (err, body, res) => {
err.should.be.deep.equal(["test1", "test2"]);
body.should.be.deep.equal({
errors: [
Expand All @@ -734,9 +738,10 @@ describe("utils/ApiClient", () => {
const rawRes = {
body: "test",
status: 404,
type: "application/json",
};

const callback = client.wrapCallback((err, body, res) => {
const callback = client.wrapCallback("POST", (err, body, res) => {
err.should.be.deep.equal([{
message: "Could not communicate with test-server",
code: "COMMUNICATION_ERROR",
Expand All @@ -748,5 +753,56 @@ describe("utils/ApiClient", () => {
});
callback(new Error(), rawRes);
});

it("should call callback with 'body' and 'res' and application/jose+json Content-Type", (cb) => {
const clientPath = path.join(__dirname, "..", "resources", "private-jwkset1");
const hwPath = path.join(__dirname, "..", "resources", "public-jwkset1");
const clientWithEncryption = new ApiClient("test-username", "test-password", "https://test-server", {
clientPrivateKeySetPath: clientPath,
hyperwalletKeySetPath: hwPath,
});

const encryption = new Encryption(clientPath, hwPath);
const testMessage = {
message: "Test message",
};

encryption.encrypt(testMessage).then((encryptedBody) => {
const callback = clientWithEncryption.wrapCallback("POST", (err, body, res) => {
expect(err).to.be.undefined();
expect(res).not.to.be.undefined();
body.should.be.deep.equal(testMessage);

cb();
});
const rawRes = {
text: JSON.stringify(encryption.base64Decode(encryptedBody)),
status: 200,
type: "application/jose+json",
};
callback(undefined, rawRes);
});
});

it("should call callback with static error message as 'errors', when Content-Type is wrong", (cb) => {
const client = new ApiClient("test-username", "test-password", "test-server");

const rawRes = {
body: "test",
status: 200,
type: "wrongContentType",
};

const callback = client.wrapCallback("POST", (err, body, res) => {
err.should.be.deep.equal([{
message: "Invalid Content-Type specified in Response Header",
}]);
body.should.be.equal("test");
rawRes.should.be.deep.equal(res);

cb();
});
callback(new Error(), rawRes);
});
});
});

0 comments on commit 564c30a

Please sign in to comment.