Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tests: improve coverage #156

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
4 changes: 2 additions & 2 deletions lib/ddp.js
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,8 @@ class DDP extends EventEmitter {
* Emits a new event. Wraps emitting in a setTimeout (macrotask)
* @override
*/
emit() {
setTimeout(super.emit.bind(this, ...arguments), 0);
emit(...args) {
setTimeout(super.emit.bind(this, ...args), 0);
}

/**
Expand Down
6,031 changes: 1,672 additions & 4,359 deletions package-lock.json

Large diffs are not rendered by default.

28 changes: 12 additions & 16 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,30 +28,26 @@
},
"homepage": "https://github.com/TheRealNate/meteor-react-native#readme",
"dependencies": {
"@meteorrn/core": "^2.8.1-rc.0",
"@meteorrn/minimongo": "1.0.1",
"@meteorrn/minimongo": "^1.0.2",
"ejson": "2.2.3",
"eventemitter3": "^5.0.1"
},
"devDependencies": {
"@babel/core": "7.19.6",
"@babel/preset-env": "7.19.4",
"@babel/register": "7.18.9",
"@babel/core": "^7.24.9",
"@babel/preset-env": "^7.24.8",
"@babel/register": "^7.24.6",
"@istanbuljs/nyc-config-babel": "3.0.0",
"@react-native-community/netinfo": "*",
"babel-plugin-istanbul": "6.1.1",
"c8": "7.12.0",
"babel-plugin-istanbul": "^7.0.0",
"c8": "^10.1.2",
"chai": "4.3.6",
"husky": "0.14.3",
"jsdoc": "^4.0.2",
"lint-staged": "7.3.0",
"metro-react-native-babel-preset": "0.66.2",
"mocha": "10.1.0",
"mock-socket": "9.1.5",
"nyc": "15.1.0",
"prettier": "2.7.1",
"jsdoc": "^4.0.3",
"metro-react-native-babel-preset": "^0.77.0",
"mocha": "^10.6.0",
"mock-socket": "^9.3.1",
"nyc": "^17.0.0",
"prettier": "^3.3.3",
"react": "*",
"rewiremock": "3.14.3",
"sinon": "12.0.1"
},
"optionalDependencies": {
Expand Down
6 changes: 3 additions & 3 deletions src/Call.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import Data from './Data';
* @param eventName {string} required, the method to call
* @param args {...array} optional arguments
*/
export default function (eventName) {
var args = Array.prototype.slice.call(arguments, 1);
export default function (eventName, ...args) {
let callback;
if (args.length && typeof args[args.length - 1] === 'function') {
var callback = args.pop();
callback = args.pop();
}

const id = Data.ddp.method(eventName, args);
Expand Down
54 changes: 40 additions & 14 deletions src/Data.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,13 @@ const Data = {
* @returns {string} the connection url
*/
getUrl() {
return this._endpoint.substring(0, this._endpoint.indexOf('/websocket'));
const ep = this._endpoint;
if (!ep) {
throw new Error(
`Expected a configured endpoint, got ${ep}, did you forget to call Meteor.connect({...})?`
);
}
return ep.substring(0, ep.indexOf('/websocket'));
},

/**
Expand Down Expand Up @@ -106,26 +112,46 @@ const Data = {
* @param cb {function}
*/
onChange(cb) {
this.db.on('change', cb);
this.ddp.on('connected', cb);
this.ddp.on('disconnected', cb);
this.on('loggingIn', cb);
this.on('loggingOut', cb);
this.on('change', cb);
// In order to specifiy which event caused the change
// we wrap them all to bubble the name into the callback
// and also provide a way to safely remove the listeners
const wrap = (ctx) =>
function (...args) {
cb.call(this, ctx, ...args);
};
const wrappers = {
change: wrap('change'),
connected: wrap('connected'),
disconnected: wrap('disconnected'),
loggingIn: wrap('loggingIn'),
loggingOut: wrap('loggingOut'),
};
this._onChangeWrappers[cb] = wrappers;
this.db.on('change', wrappers.change);
this.ddp.on('connected', wrappers.connected);
this.ddp.on('disconnected', wrappers.disconnected);
this.on('loggingIn', wrappers.loggingIn);
this.on('loggingOut', wrappers.loggingOut);
this.on('change', wrappers.change);
},
_onChangeWrappers: {},

/**
* Stops listening the events from `Data.onChange`.
* Requires the **exact same callback function** to work properly!
* @param cb {function}
*/
offChange(cb) {
this.db.off('change', cb);
this.ddp.off('connected', cb);
this.ddp.off('disconnected', cb);
this.off('loggingIn', cb);
this.off('loggingOut', cb);
this.off('change', cb);
const wrappers = this._onChangeWrappers[cb];
if (wrappers) {
this.db.off('change', wrappers.change);
this.ddp.off('connected', wrappers.connected);
this.ddp.off('disconnected', wrappers.disconnected);
this.off('loggingIn', wrappers.loggingIn);
this.off('loggingOut', wrappers.loggingOut);
this.off('change', wrappers.change);
delete this._onChangeWrappers[cb];
}
},

/**
Expand Down Expand Up @@ -175,7 +201,7 @@ const Data = {
* @param callback {function}
*/
waitDdpConnected(callback) {
if (this.ddp && this.ddp.status == 'connected') {
if (this.ddp && this.ddp.status === 'connected') {
callback();
} else if (this.ddp) {
this.ddp.once('connected', callback);
Expand Down
20 changes: 16 additions & 4 deletions src/Meteor.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import withTracker from './components/withTracker';
import useTracker from './components/useTracker';

import ReactiveDict from './ReactiveDict';
import User from './user/User';

/**
* @namespace Meteor
Expand Down Expand Up @@ -138,6 +139,12 @@ const Meteor = {
...options,
});

if (Data.ddp && this.ddp) {
Data._cbs = [];
Data.ddp.socket.removeAllListeners();
Data.ddp.removeAllListeners();
}

Data.ddp = ddp;
this.ddp = ddp;

Expand All @@ -155,7 +162,12 @@ const Meteor = {
if (this.isVerbose) {
console.info('Connected to DDP server.');
}
this._loadInitialUser().then(() => {

// XXX: in tests we can't access this._loadInitialUser,
// so instead we use User._loadInitialUser().
// It's also questionable to implicitly assume a dependency
// that was not declared in this file using this._loadInitialUser
User._loadInitialUser().then(() => {
this._subscriptionsRestart();
});
this._reactiveDict.set('connected', true);
Expand Down Expand Up @@ -216,6 +228,7 @@ const Meteor = {
const sub = Data.subscriptions[i];
idsMap.set(sub.subIdRemember, sub.id);
}

for (var i in message.subs) {
const subId = idsMap.get(message.subs[i]);
if (subId) {
Expand Down Expand Up @@ -326,8 +339,7 @@ const Meteor = {
}
}
},
subscribe(name) {
let params = Array.prototype.slice.call(arguments, 1);
subscribe(name, ...params) {
let callbacks = {};
if (params.length) {
let lastParam = params[params.length - 1];
Expand Down Expand Up @@ -362,7 +374,7 @@ const Meteor = {
// being invalidated, we will require N matching subscribe calls to keep
// them all active.

let existing = false;
let existing = null;
for (let i in Data.subscriptions) {
const sub = Data.subscriptions[i];
if (sub.inactive && sub.name === name && EJSON.equals(sub.params, params))
Expand Down
6 changes: 3 additions & 3 deletions src/Mongo.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Collection } from './Collection';

const Mongo = { Collection };

/**
* Mongo namespace, contains only Mongo.Collection
* @namespace Mongo
* @see Collection
*/
export default {
Collection,
};
export default Mongo;
60 changes: 38 additions & 22 deletions src/user/Accounts.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,30 @@ import Meteor from '../Meteor.js';
* @class
* @see https://docs.meteor.com/api/accounts
* @see https://docs.meteor.com/api/passwords
* @see https://github.com/meteor/meteor/blob/devel/packages/accounts-password/password_client.js
*/
class AccountsPassword {
_hashPassword = hashPassword;

/**
*
* @param options {object}
* @param options.username {string=} username is optional, if an email is given
* @param options.email {string=} email is optional, if a username is given
* @param callback {function(e:Error)=} optional callback that is invoked with one optional error argument
* Create a new user.
* @param {Object} options
* @param {String} options.username A unique name for this user.
* @param {String} options.email The user's email address.
* @param {String} options.password The user's password. This is __not__ sent in plain text over the wire.
* @param {Object} options.profile The user's profile, typically including the `name` field.
* @param {Function} [callback] Client only, optional callback. Called with no arguments on success, or with a single `Error` argument on failure.
*/
createUser = (options, callback = () => {}) => {
options = { ...options };

if (typeof options.password !== 'string') {
throw new Error('options.password must be a string');
}
if (!options.password) {
return callback(new Error('Password may not be empty'));
}

// Replace password with the hashed password.
options.password = hashPassword(options.password);

Expand All @@ -44,20 +56,23 @@ class AccountsPassword {
* @param callback {function(e:Error)=} optional callback that is invoked with one optional error argument
*/
changePassword = (oldPassword, newPassword, callback = () => {}) => {
//TODO check Meteor.user() to prevent if not logged
if (!User.user()) {
return callback(new Error('Must be logged in to change password'));
}

if (typeof newPassword != 'string' || !newPassword) {
// TODO make callback(new Error(...)) instead
return callback('Password may not be empty');
if (!(typeof newPassword === 'string' || newPassword instanceof String)) {
return callback(new Error('Password must be a string'));
}

if (!newPassword) {
return callback(new Error('Password may not be empty'));
}

call(
'changePassword',
oldPassword ? hashPassword(oldPassword) : null,
hashPassword(newPassword),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it really hashed here? Had a look at Meteors password_server.js, looks like the newPassword is hashed on the server side

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok got it, it sending the object with the sha256 hash, not the string.

(err, res) => {
callback(err);
}
callback
);
};

Expand All @@ -69,17 +84,10 @@ class AccountsPassword {
*/
forgotPassword = (options, callback = () => {}) => {
if (!options.email) {
// TODO check this! I doubt it's implemented on the server
// to let the client decide which email to use.
// The only valid scenario is, when users have multiple emails
// but even then the prop should be optional as not ever user
// will have multiple emails
return callback('Must pass options.email');
return callback(new Error('Must pass options.email'));
}

call('forgotPassword', options, (err) => {
callback(err);
});
call('forgotPassword', options, callback);
};

/**
Expand All @@ -90,8 +98,16 @@ class AccountsPassword {
* @param callback {function(e:Error)=} optional callback that is invoked with one optional error argument
*/
resetPassword = (token, newPassword, callback = () => {}) => {
if (!(typeof token === 'string' || token instanceof String)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the type checking different from e.g the createUser function here?

return callback(new Error('Token must be a string'));
}

if (!(typeof newPassword === 'string' || newPassword instanceof String)) {
return callback(new Error('Password must be a string'));
}

if (!newPassword) {
return callback('Must pass a new password');
return callback(new Error('Password may not be empty'));
}

call('resetPassword', token, hashPassword(newPassword), (err, result) => {
Expand Down
Loading
Loading