pusher-js
Version:
Pusher Channels JavaScript library for browsers, React Native, NodeJS and web workers
346 lines (318 loc) • 12.2 kB
JavaScript
const Errors = require("core/errors");
const Logger = require('core/logger').default;
const EncryptedChannel = require("core/channels/encrypted_channel").default;
const Factory = require("core/utils/factory").default;
const Mocks = require("mocks");
const nacl = require("tweetnacl");
const utf8 = require("@stablelib/utf8");
const base64 = require("@stablelib/base64");
describe("EncryptedChannel", function() {
var pusher;
var channel;
var authorizer;
var factorySpy;
const secretUTF8 = "It Must Be Thirty Two Characters";
const secretBytes = utf8.encode(secretUTF8);
const secretBase64 = base64.encode(secretBytes);
const nonceUTF8 = "aaaaaaaaaaaaaaaaaaaaaaaa";
const nonceBytes = utf8.encode(nonceUTF8);
const nonceBase64 = base64.encode(nonceBytes);
const testEncrypt = function(payload) {
let payloadBytes = utf8.encode(JSON.stringify(payload));
let bytes = nacl.secretbox(payloadBytes, nonceBytes, secretBytes);
return base64.encode(bytes);
};
beforeEach(function() {
pusher = Mocks.getPusher({ foo: "bar" });
channel = new EncryptedChannel("private-encrypted-test", pusher, nacl);
authorizer = Mocks.getAuthorizer();
factorySpy = spyOn(Factory, "createAuthorizer").and.returnValue(authorizer);
});
describe("after construction", function() {
it("#subscribed should be false", function() {
expect(channel.subscribed).toEqual(false);
});
it("#subscriptionPending should be false", function() {
expect(channel.subscriptionPending).toEqual(false);
});
it("#subscriptionCancelled should be false", function() {
expect(channel.subscriptionCancelled).toEqual(false);
});
});
describe("#authorize", function() {
it("should create and call an authorizer", function() {
channel.authorize("1.23", function() {});
expect(Factory.createAuthorizer.calls.count()).toEqual(1);
expect(Factory.createAuthorizer).toHaveBeenCalledWith(channel, {
foo: "bar"
});
});
it("should call back with only authorization data", function() {
let callback = jasmine.createSpy("callback");
channel.authorize("1.23", callback);
expect(callback).not.toHaveBeenCalled();
authorizer._callback(false, {
shared_secret: secretBase64,
foo: "bar"
});
expect(callback).toHaveBeenCalledWith(null, { foo: "bar" });
});
it("should callback an error if no shared_secret included in auth data", function() {
let callback = jasmine.createSpy("callback");
channel.authorize("1.23", callback);
authorizer._callback(null, {
foo: "bar"
});
// For some reason comparing the Error types doesn't work properly in
// Safari on Mojave. Manually check the arguments.
expect(callback.calls.count()).toEqual(1)
let args = callback.calls.first().args;
expect(args.length).toEqual(2)
expect(args[0]).toEqual(jasmine.any(Error))
expect(args[0].message).toEqual(
"No shared_secret key in auth payload for encrypted channel: private-encrypted-test"
);
expect(args[1]).toEqual(null);
});
describe("with custom authorizer", function() {
beforeEach(function() {
pusher = Mocks.getPusher({
authorizer: function(channel, options) {
return authorizer;
}
});
channel = new EncryptedChannel("private-test-custom-auth", pusher, nacl);
factorySpy.and.callThrough();
});
it("should call the authorizer", function() {
let callback = jasmine.createSpy("callback");
channel.authorize("1.23", callback);
authorizer._callback(false, {
shared_secret: secretBase64,
foo: "bar"
});
expect(callback).toHaveBeenCalledWith(null, { foo: "bar" });
});
});
});
describe("#trigger", function() {
beforeEach(function() {
let callback = function() {};
channel.authorize("1.23", callback);
});
it("should raise an exception if called", function() {
expect(() => channel.trigger('whatever', {})).toThrow(
jasmine.any(Errors.UnsupportedFeature)
);
});
});
describe("#disconnect", function() {
it("should set subscribed to false", function() {
channel.handleEvent({
event: "pusher_internal:subscription_succeeded"
});
channel.disconnect();
expect(channel.subscribed).toEqual(false);
});
});
describe("#handleEvent", function() {
it("should not emit pusher_internal:* events", function() {
let callback = jasmine.createSpy("callback");
channel.bind("pusher_internal:test", callback);
channel.bind_global(callback);
channel.handleEvent({
event: "pusher_internal:test"
});
expect(callback).not.toHaveBeenCalled();
});
describe("on pusher_internal:subscription_succeeded", function() {
it("should emit pusher:subscription_succeeded", function() {
let callback = jasmine.createSpy("callback");
channel.bind("pusher:subscription_succeeded", callback);
channel.handleEvent({
event: "pusher_internal:subscription_succeeded",
data: "123"
});
expect(callback).toHaveBeenCalledWith("123");
});
it("should set #subscribed to true", function() {
channel.handleEvent({
event: "pusher_internal:subscription_succeeded",
data: "123"
});
expect(channel.subscribed).toEqual(true);
});
it("should set #subscriptionPending to false", function() {
channel.handleEvent({
event: "pusher_internal:subscription_succeeded",
data: "123"
});
expect(channel.subscriptionPending).toEqual(false);
});
});
describe("pusher_internal:subscription_succeeded but subscription cancelled", function() {
it("should not emit pusher:subscription_succeeded", function() {
let callback = jasmine.createSpy("callback");
channel.bind("pusher:subscription_succeeded", callback);
channel.cancelSubscription();
channel.handleEvent({
event: "pusher_internal:subscription_succeeded",
data: "123"
});
expect(callback).not.toHaveBeenCalled();
});
it("should set #subscribed to true", function() {
channel.cancelSubscription();
channel.handleEvent({
event: "pusher_internal:subscription_succeeded",
data: "123"
});
expect(channel.subscribed).toEqual(true);
});
it("should set #subscriptionPending to false", function() {
channel.cancelSubscription();
channel.handleEvent({
event: "pusher_internal:subscription_succeeded",
data: "123"
});
expect(channel.subscriptionPending).toEqual(false);
});
it("should call #pusher.unsubscribe", function() {
expect(pusher.unsubscribe).not.toHaveBeenCalled();
channel.cancelSubscription();
channel.handleEvent({
event: "pusher_internal:subscription_succeeded",
data: "123"
});
expect(pusher.unsubscribe).toHaveBeenCalledWith(channel.name);
});
});
describe("on other events", function() {
beforeEach(function() {
// in order to decrypt encrypted events, we need to get a shared secret
// from the authorizer.
let callback = function() {};
channel.authorize("1.23", callback);
authorizer._callback(false, {
shared_secret: secretBase64,
foo: "bar"
});
});
it("should decrypt the event payload and emit the event", function() {
let payload = { test: "payload" };
let encryptedPayload = {
nonce: nonceBase64,
ciphertext: testEncrypt(payload)
};
let boundCallback = jasmine.createSpy("boundCallback");
channel.bind("something", boundCallback);
channel.handleEvent({
event: "something",
data: encryptedPayload
});
expect(boundCallback).toHaveBeenCalledWith(payload);
});
it("should emit pusher: prefixed events unmodified", function() {
let payload = { test: "payload" };
let boundCallback = jasmine.createSpy("boundCallback");
channel.bind("pusher:subscription_error", boundCallback);
channel.handleEvent({
event: "pusher:subscription_error",
data: payload
});
expect(boundCallback).toHaveBeenCalledWith(payload, {});
});
it("should not swallow errors thrown by the handler", function() {
// previously, if the handler threw an error, when called with the
// parsed json, we tried again with a string. This test aims to check
// that an error thrown by a handler should not be caught by the lib
let payload = { test: "payload" };
let encryptedPayload = {
nonce: nonceBase64,
ciphertext: testEncrypt(payload)
};
let callCount = 0;
channel.bind("something", (data)=> {
if (callCount == 0) {
callCount++;
throw new Error("some error");
}
})
expect(function() {
channel.handleEvent({
event: "something",
data: encryptedPayload
});
}).toThrow()
});
describe("with rotated shared key", function() {
const newSecretUTF8 = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
const newSecretBytes = utf8.encode(newSecretUTF8);
const newSecretBase64 = base64.encode(newSecretBytes);
const newTestEncrypt = function(payload) {
let payloadBytes = utf8.encode(JSON.stringify(payload));
let bytes = nacl.secretbox(payloadBytes, nonceBytes, newSecretBytes);
return base64.encode(bytes);
};
beforeEach(function() {
pusher.connection = {
socket_id: "9.37"
};
authorizer._callback = null
});
it("should request new key from authorizer and decrypt event", function() {
let payload = { test: "payload" };
let encryptedPayload = {
nonce: nonceBase64,
ciphertext: newTestEncrypt(payload)
};
let boundCallback = jasmine.createSpy("boundCallback");
channel.bind("something", boundCallback);
channel.handleEvent({
event: "something",
data: encryptedPayload
});
authorizer._callback(false, {
shared_secret: newSecretBase64,
foo: "bar"
});
expect(boundCallback).toHaveBeenCalledWith(payload);
});
it("should log a warning if it fails to decrypt event after requesting a new key from the auth endpoint", function() {
let encryptedPayload = {
nonce: nonceBase64,
ciphertext: base64.encode('garbage-ciphertext')
};
spyOn(Logger, "error");
channel.handleEvent({
event: "something",
data: encryptedPayload
});
authorizer._callback(false, {
shared_secret: newSecretBase64,
foo: "bar"
});
expect(Logger.error).toHaveBeenCalledWith(
"Failed to decrypt event with new key. Dropping encrypted event"
);
});
it("should log a warning if it fails to call the auth endpoint after failing to decrypt an event", function() {
let payload = { test: "payload" };
let encryptedPayload = {
nonce: nonceBase64,
ciphertext: newTestEncrypt(payload)
};
spyOn(Logger, "error");
channel.handleEvent({
event: "something",
data: encryptedPayload
});
authorizer._callback(true, "ERROR");
expect(Logger.error).toHaveBeenCalledWith(
"Failed to make a request to the authEndpoint: ERROR. Unable to fetch new key, so dropping encrypted event"
);
});
});
});
});
});