mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Merge branch 'release-2.9' into meteor-no-fibers-base
# Conflicts: # packages/meteor/package.js
This commit is contained in:
@@ -67,7 +67,6 @@ tools/runners/run-app.js
|
||||
tools/runners/run-mongo.js
|
||||
tools/runners/run-proxy.js
|
||||
tools/runners/run-selenium.js
|
||||
tools/runners/run-updater.js
|
||||
|
||||
tools/packaging/package-client.js
|
||||
tools/packaging/package-map.js
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
## 2.8.1, Unreleased
|
||||
|
||||
#### Highlights
|
||||
|
||||
#### Breaking Changes
|
||||
|
||||
#### Migration Steps
|
||||
|
||||
#### Meteor Version Release
|
||||
* `facebook-oauth@1.12.0`
|
||||
- Updated default version of Facebook GraphAPI to v15
|
||||
|
||||
|
||||
## v2.8, 2022-10-19
|
||||
|
||||
#### Highlights
|
||||
@@ -77,7 +90,7 @@ Read our [Migration Guide](https://guide.meteor.com/2.8-migration.html) for this
|
||||
|
||||
For making this great framework even better!
|
||||
|
||||
## v2.7.3, 2022-05-31
|
||||
## v2.7.3, 2022-05-3
|
||||
|
||||
#### Highlights
|
||||
* `accounts-passwordless@2.1.2`:
|
||||
|
||||
@@ -315,6 +315,15 @@ Accounts.setAdditionalFindUserOnExternalLogin(({serviceName, serviceData}) => {
|
||||
}
|
||||
})
|
||||
```
|
||||
{% apibox "AccountsServer#registerLoginHandler" %}
|
||||
|
||||
Use this to register your own custom authentication method. This is also used by all of the other inbuilt accounts packages to integrate with the accounts system.
|
||||
|
||||
There can be multiple login handlers that are registered. When a login request is made, it will go through all these handlers to find its own handler.
|
||||
|
||||
The registered handler callback is called with a single argument, the `options` object which comes from the login method. For example, if you want to login with a plaintext password, `options` could be `{ user: { username: <username> }, password: <password> }`,or `{ user: { email: <email> }, password: <password> }`.
|
||||
|
||||
The login handler should return `undefined` if it's not going to handle the login request or else the login result object.
|
||||
|
||||
<h2 id="accounts_rate_limit">Rate Limiting</h2>
|
||||
|
||||
|
||||
@@ -83,6 +83,28 @@ Meteor.call(
|
||||
'This is a test of Email.send.'
|
||||
);
|
||||
```
|
||||
{% apibox "Email.sendAsync" %}
|
||||
|
||||
`sendAsync` only works on the server. It has the same behavior as `Email.send`, but returns a Promise.
|
||||
If you defined `Email.customTransport`, the `callAsync` method returns the return value from the `customTransport` method or a Promise, if this method is async.
|
||||
|
||||
```js
|
||||
// Server: Define a method that the client can call.
|
||||
Meteor.methods({
|
||||
sendEmail(to, from, subject, text) {
|
||||
// Make sure that all arguments are strings.
|
||||
check([to, from, subject, text], [String]);
|
||||
|
||||
// Let other method calls from the same client start running, without
|
||||
// waiting for the email sending to complete.
|
||||
this.unblock();
|
||||
|
||||
return Email.sendAsync({ to, from, subject, text }).catch(err => {
|
||||
//
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
{% apibox "Email.hookSend" %}
|
||||
|
||||
|
||||
@@ -67,7 +67,6 @@ tools/runners/run-app.js
|
||||
tools/runners/run-mongo.js
|
||||
tools/runners/run-proxy.js
|
||||
tools/runners/run-selenium.js
|
||||
tools/runners/run-updater.js
|
||||
|
||||
tools/packaging/package-client.js
|
||||
tools/packaging/package-map.js
|
||||
|
||||
@@ -57,3 +57,8 @@ npm install -g meteor --ignore-meteor-setup-exec-path
|
||||
```
|
||||
|
||||
(or by setting the environment variable `npm_config_ignore_meteor_setup_exec_path=true`)
|
||||
|
||||
### Proxy configuration
|
||||
|
||||
Setting the `https_proxy` or `HTTPS_PROXY` environment variable to a valid proxy URL will cause the
|
||||
downloader to use the configured proxy to retrieve the Meteor files.
|
||||
@@ -1,4 +1,5 @@
|
||||
const { DownloaderHelper } = require('node-downloader-helper');
|
||||
const HttpsProxyAgent = require('https-proxy-agent');
|
||||
const cliProgress = require('cli-progress');
|
||||
const Seven = require('node-7z');
|
||||
const path = require('path');
|
||||
@@ -143,6 +144,15 @@ try {
|
||||
|
||||
download();
|
||||
|
||||
function generateProxyAgent() {
|
||||
const proxyUrl = process.env.https_proxy || process.env.HTTPS_PROXY;
|
||||
if (!proxyUrl) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return new HttpsProxyAgent(proxyUrl);
|
||||
}
|
||||
|
||||
function download() {
|
||||
const start = Date.now();
|
||||
const downloadProgress = new cliProgress.SingleBar(
|
||||
@@ -158,6 +168,9 @@ function download() {
|
||||
retry: { maxRetries: 5, delay: 5000 },
|
||||
override: true,
|
||||
fileName: tarGzName,
|
||||
httpsRequestOptions: {
|
||||
agent: generateProxyAgent()
|
||||
}
|
||||
});
|
||||
|
||||
dl.on('progress', ({ progress }) => {
|
||||
|
||||
@@ -318,7 +318,7 @@ export class AccountsServer extends AccountsCommon {
|
||||
// If user is not found, try a case insensitive lookup
|
||||
if (!user) {
|
||||
selector = this._selectorForFastCaseInsensitiveLookup(fieldName, fieldValue);
|
||||
const candidateUsers = Meteor.users.find(selector, options).fetch();
|
||||
const candidateUsers = Meteor.users.find(selector, { ...options, limit: 2 }).fetch();
|
||||
// No match if multiple candidates are found
|
||||
if (candidateUsers.length === 1) {
|
||||
user = candidateUsers[0];
|
||||
@@ -434,7 +434,7 @@ export class AccountsServer extends AccountsCommon {
|
||||
// If the login is allowed and isn't aborted by a validate login hook
|
||||
// callback, log in the user.
|
||||
//
|
||||
_attemptLogin(
|
||||
async _attemptLogin(
|
||||
methodInvocation,
|
||||
methodName,
|
||||
methodArgs,
|
||||
@@ -494,18 +494,18 @@ export class AccountsServer extends AccountsCommon {
|
||||
// Ensure that thrown exceptions are caught and that login hook
|
||||
// callbacks are still called.
|
||||
//
|
||||
_loginMethod(
|
||||
async _loginMethod(
|
||||
methodInvocation,
|
||||
methodName,
|
||||
methodArgs,
|
||||
type,
|
||||
fn
|
||||
) {
|
||||
return this._attemptLogin(
|
||||
return await this._attemptLogin(
|
||||
methodInvocation,
|
||||
methodName,
|
||||
methodArgs,
|
||||
tryLoginMethod(type, fn)
|
||||
await tryLoginMethod(type, fn)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -547,19 +547,14 @@ export class AccountsServer extends AccountsCommon {
|
||||
/// LOGIN HANDLERS
|
||||
///
|
||||
|
||||
// The main entry point for auth packages to hook in to login.
|
||||
//
|
||||
// A login handler is a login method which can return `undefined` to
|
||||
// indicate that the login request is not handled by this handler.
|
||||
//
|
||||
// @param name {String} Optional. The service name, used by default
|
||||
// if a specific service name isn't returned in the result.
|
||||
//
|
||||
// @param handler {Function} A function that receives an options object
|
||||
// (as passed as an argument to the `login` method) and returns one of:
|
||||
// - `undefined`, meaning don't handle;
|
||||
// - a login method result object
|
||||
|
||||
/**
|
||||
* @summary Registers a new login handler.
|
||||
* @locus Server
|
||||
* @param {String} [name] The type of login method like oauth, password, etc.
|
||||
* @param {Function} handler A function that receives an options object
|
||||
* (as passed as an argument to the `login` method) and returns one of
|
||||
* `undefined`, meaning don't handle or a login method result object.
|
||||
*/
|
||||
registerLoginHandler(name, handler) {
|
||||
if (! handler) {
|
||||
handler = name;
|
||||
@@ -587,11 +582,10 @@ export class AccountsServer extends AccountsCommon {
|
||||
// Try all of the registered login handlers until one of them doesn't
|
||||
// return `undefined`, meaning it handled this call to `login`. Return
|
||||
// that return value.
|
||||
_runLoginHandlers(methodInvocation, options) {
|
||||
async _runLoginHandlers(methodInvocation, options) {
|
||||
for (let handler of this._loginHandlers) {
|
||||
const result = tryLoginMethod(
|
||||
handler.name,
|
||||
() => handler.handler.call(methodInvocation, options)
|
||||
const result = await tryLoginMethod(handler.name, async () =>
|
||||
await handler.handler.call(methodInvocation, options)
|
||||
);
|
||||
|
||||
if (result) {
|
||||
@@ -599,7 +593,10 @@ export class AccountsServer extends AccountsCommon {
|
||||
}
|
||||
|
||||
if (result !== undefined) {
|
||||
throw new Meteor.Error(400, "A login handler should return a result or undefined");
|
||||
throw new Meteor.Error(
|
||||
400,
|
||||
'A login handler should return a result or undefined'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -644,14 +641,15 @@ export class AccountsServer extends AccountsCommon {
|
||||
// If successful, returns {token: reconnectToken, id: userId}
|
||||
// If unsuccessful (for example, if the user closed the oauth login popup),
|
||||
// throws an error describing the reason
|
||||
methods.login = function (options) {
|
||||
methods.login = async function (options) {
|
||||
// Login handlers should really also check whatever field they look at in
|
||||
// options, but we don't enforce it.
|
||||
check(options, Object);
|
||||
|
||||
const result = accounts._runLoginHandlers(this, options);
|
||||
const result = await accounts._runLoginHandlers(this, options);
|
||||
//console.log({result});
|
||||
|
||||
return accounts._attemptLogin(this, "login", arguments, result);
|
||||
return await accounts._attemptLogin(this, "login", arguments, result);
|
||||
};
|
||||
|
||||
methods.logout = function () {
|
||||
@@ -1512,10 +1510,10 @@ const cloneAttemptWithConnection = (connection, attempt) => {
|
||||
return clonedAttempt;
|
||||
};
|
||||
|
||||
const tryLoginMethod = (type, fn) => {
|
||||
const tryLoginMethod = async (type, fn) => {
|
||||
let result;
|
||||
try {
|
||||
result = fn();
|
||||
result = await fn();
|
||||
}
|
||||
catch (e) {
|
||||
result = {error: e};
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import bcrypt from 'bcrypt'
|
||||
import {Accounts} from "meteor/accounts-base";
|
||||
|
||||
const bcryptHash = Meteor.wrapAsync(bcrypt.hash);
|
||||
const bcryptCompare = Meteor.wrapAsync(bcrypt.compare);
|
||||
import { hash as bcryptHash, compare as bcryptCompare } from 'bcrypt';
|
||||
import { Accounts } from "meteor/accounts-base";
|
||||
|
||||
// Utility for grabbing user
|
||||
const getUserById = (id, options) => Meteor.users.findOne(id, Accounts._addDefaultFieldSelector(options));
|
||||
@@ -48,9 +45,9 @@ const getPasswordString = password => {
|
||||
// SHA256 before bcrypt) or an object with properties `digest` and
|
||||
// `algorithm` (in which case we bcrypt `password.digest`).
|
||||
//
|
||||
const hashPassword = password => {
|
||||
const hashPassword = async password => {
|
||||
password = getPasswordString(password);
|
||||
return bcryptHash(password, Accounts._bcryptRounds());
|
||||
return await bcryptHash(password, Accounts._bcryptRounds());
|
||||
};
|
||||
|
||||
// Extract the number of rounds used in the specified bcrypt hash.
|
||||
@@ -74,7 +71,7 @@ const getRoundsFromBcryptHash = hash => {
|
||||
// The user parameter needs at least user._id and user.services
|
||||
Accounts._checkPasswordUserFields = {_id: 1, services: 1};
|
||||
//
|
||||
Accounts._checkPassword = (user, password) => {
|
||||
const checkPasswordAsync = async (user, password) => {
|
||||
const result = {
|
||||
userId: user._id
|
||||
};
|
||||
@@ -83,15 +80,16 @@ Accounts._checkPassword = (user, password) => {
|
||||
const hash = user.services.password.bcrypt;
|
||||
const hashRounds = getRoundsFromBcryptHash(hash);
|
||||
|
||||
if (! bcryptCompare(formattedPassword, hash)) {
|
||||
if (! await bcryptCompare(formattedPassword, hash)) {
|
||||
result.error = Accounts._handleError("Incorrect password", false);
|
||||
} else if (hash && Accounts._bcryptRounds() != hashRounds) {
|
||||
// The password checks out, but the user's bcrypt hash needs to be updated.
|
||||
Meteor.defer(() => {
|
||||
|
||||
Meteor.defer(async () => {
|
||||
Meteor.users.update({ _id: user._id }, {
|
||||
$set: {
|
||||
'services.password.bcrypt':
|
||||
bcryptHash(formattedPassword, Accounts._bcryptRounds())
|
||||
await bcryptHash(formattedPassword, Accounts._bcryptRounds())
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -99,7 +97,13 @@ Accounts._checkPassword = (user, password) => {
|
||||
|
||||
return result;
|
||||
};
|
||||
const checkPassword = Accounts._checkPassword;
|
||||
|
||||
const checkPassword = async (user, password) => {
|
||||
return Promise.await(checkPasswordAsync(user, password));
|
||||
};
|
||||
|
||||
Accounts._checkPassword = checkPassword;
|
||||
Accounts._checkPasswordAsync = checkPasswordAsync;
|
||||
|
||||
///
|
||||
/// LOGIN
|
||||
@@ -163,7 +167,7 @@ const passwordValidator = Match.OneOf(
|
||||
//
|
||||
// Note that neither password option is secure without SSL.
|
||||
//
|
||||
Accounts.registerLoginHandler("password", options => {
|
||||
Accounts.registerLoginHandler("password", async options => {
|
||||
if (!options.password)
|
||||
return undefined; // don't handle
|
||||
|
||||
@@ -188,7 +192,7 @@ Accounts.registerLoginHandler("password", options => {
|
||||
Accounts._handleError("User has no password set");
|
||||
}
|
||||
|
||||
const result = checkPassword(user, options.password);
|
||||
const result = await checkPasswordAsync(user, options.password);
|
||||
// This method is added by the package accounts-2fa
|
||||
// First the login is validated, then the code situation is checked
|
||||
if (
|
||||
@@ -258,7 +262,7 @@ Accounts.setUsername = (userId, newUsername) => {
|
||||
// Let the user change their own password if they know the old
|
||||
// password. `oldPassword` and `newPassword` should be objects with keys
|
||||
// `digest` and `algorithm` (representing the SHA256 of the password).
|
||||
Meteor.methods({changePassword: function (oldPassword, newPassword) {
|
||||
Meteor.methods({changePassword: async function (oldPassword, newPassword) {
|
||||
check(oldPassword, passwordValidator);
|
||||
check(newPassword, passwordValidator);
|
||||
|
||||
@@ -278,12 +282,12 @@ Meteor.methods({changePassword: function (oldPassword, newPassword) {
|
||||
Accounts._handleError("User has no password set");
|
||||
}
|
||||
|
||||
const result = checkPassword(user, oldPassword);
|
||||
const result = await checkPasswordAsync(user, oldPassword);
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
const hashed = hashPassword(newPassword);
|
||||
const hashed = await hashPassword(newPassword);
|
||||
|
||||
// It would be better if this removed ALL existing tokens and replaced
|
||||
// the token for the current connection with a new one, but that would
|
||||
@@ -316,10 +320,10 @@ Meteor.methods({changePassword: function (oldPassword, newPassword) {
|
||||
* @param {Object} options.logout Logout all current connections with this userId (default: true)
|
||||
* @importFromPackage accounts-base
|
||||
*/
|
||||
Accounts.setPassword = (userId, newPlaintextPassword, options) => {
|
||||
check(userId, String)
|
||||
check(newPlaintextPassword, Match.Where(str => Match.test(str, String) && str.length <= Meteor.settings?.packages?.accounts?.passwordMaxLength || 256))
|
||||
check(options, Match.Maybe({ logout: Boolean }))
|
||||
Accounts.setPasswordAsync = async (userId, newPlaintextPassword, options) => {
|
||||
check(userId, String);
|
||||
check(newPlaintextPassword, Match.Where(str => Match.test(str, String) && str.length <= Meteor.settings?.packages?.accounts?.passwordMaxLength || 256));
|
||||
check(options, Match.Maybe({ logout: Boolean }));
|
||||
options = { logout: true , ...options };
|
||||
|
||||
const user = getUserById(userId, {fields: {_id: 1}});
|
||||
@@ -331,7 +335,7 @@ Accounts.setPassword = (userId, newPlaintextPassword, options) => {
|
||||
$unset: {
|
||||
'services.password.reset': 1
|
||||
},
|
||||
$set: {'services.password.bcrypt': hashPassword(newPlaintextPassword)}
|
||||
$set: {'services.password.bcrypt': await hashPassword(newPlaintextPassword)}
|
||||
};
|
||||
|
||||
if (options.logout) {
|
||||
@@ -341,6 +345,19 @@ Accounts.setPassword = (userId, newPlaintextPassword, options) => {
|
||||
Meteor.users.update({_id: user._id}, update);
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Forcibly change the password for a user.
|
||||
* @locus Server
|
||||
* @param {String} userId The id of the user to update.
|
||||
* @param {String} newPassword A new password for the user.
|
||||
* @param {Object} [options]
|
||||
* @param {Object} options.logout Logout all current connections with this userId (default: true)
|
||||
* @importFromPackage accounts-base
|
||||
*/
|
||||
Accounts.setPassword = (userId, newPlaintextPassword, options) => {
|
||||
return Promise.await(Accounts.setPasswordAsync(userId, newPlaintextPassword, options));
|
||||
};
|
||||
|
||||
|
||||
///
|
||||
/// RESETTING VIA EMAIL
|
||||
@@ -560,15 +577,15 @@ Accounts.sendEnrollmentEmail = (userId, email, extraTokenData, extraParams) => {
|
||||
|
||||
// Take token from sendResetPasswordEmail or sendEnrollmentEmail, change
|
||||
// the users password, and log them in.
|
||||
Meteor.methods({resetPassword: function (...args) {
|
||||
Meteor.methods({resetPassword: async function (...args) {
|
||||
const token = args[0];
|
||||
const newPassword = args[1];
|
||||
return Accounts._loginMethod(
|
||||
return await Accounts._loginMethod(
|
||||
this,
|
||||
"resetPassword",
|
||||
args,
|
||||
"password",
|
||||
() => {
|
||||
async () => {
|
||||
check(token, String);
|
||||
check(newPassword, passwordValidator);
|
||||
|
||||
@@ -617,7 +634,7 @@ Meteor.methods({resetPassword: function (...args) {
|
||||
error: new Meteor.Error(403, "Token has invalid email address")
|
||||
};
|
||||
|
||||
const hashed = hashPassword(newPassword);
|
||||
const hashed = await hashPassword(newPassword);
|
||||
|
||||
// NOTE: We're about to invalidate tokens on the user, who we might be
|
||||
// logged in as. Make sure to avoid logging ourselves out if this
|
||||
@@ -712,9 +729,9 @@ Accounts.sendVerificationEmail = (userId, email, extraTokenData, extraParams) =>
|
||||
|
||||
// Take token from sendVerificationEmail, mark the email as verified,
|
||||
// and log them in.
|
||||
Meteor.methods({verifyEmail: function (...args) {
|
||||
Meteor.methods({verifyEmail: async function (...args) {
|
||||
const token = args[0];
|
||||
return Accounts._loginMethod(
|
||||
return await Accounts._loginMethod(
|
||||
this,
|
||||
"verifyEmail",
|
||||
args,
|
||||
@@ -888,7 +905,7 @@ Accounts.removeEmail = (userId, email) => {
|
||||
// does the actual user insertion.
|
||||
//
|
||||
// returns the user id
|
||||
const createUser = options => {
|
||||
const createUser = async options => {
|
||||
// Unknown keys allowed, because a onCreateUserHook can take arbitrary
|
||||
// options.
|
||||
check(options, Match.ObjectIncluding({
|
||||
@@ -903,22 +920,22 @@ const createUser = options => {
|
||||
|
||||
const user = {services: {}};
|
||||
if (password) {
|
||||
const hashed = hashPassword(password);
|
||||
const hashed = await hashPassword(password);
|
||||
user.services.password = { bcrypt: hashed };
|
||||
}
|
||||
|
||||
return Accounts._createUserCheckingDuplicates({ user, email, username, options })
|
||||
return Accounts._createUserCheckingDuplicates({ user, email, username, options });
|
||||
};
|
||||
|
||||
// method for create user. Requests come from the client.
|
||||
Meteor.methods({createUser: function (...args) {
|
||||
Meteor.methods({createUser: async function (...args) {
|
||||
const options = args[0];
|
||||
return Accounts._loginMethod(
|
||||
return await Accounts._loginMethod(
|
||||
this,
|
||||
"createUser",
|
||||
args,
|
||||
"password",
|
||||
() => {
|
||||
async () => {
|
||||
// createUser() above does more checking.
|
||||
check(options, Object);
|
||||
if (Accounts._options.forbidClientAccountCreation)
|
||||
@@ -926,7 +943,7 @@ Meteor.methods({createUser: function (...args) {
|
||||
error: new Meteor.Error(403, "Signups forbidden")
|
||||
};
|
||||
|
||||
const userId = Accounts.createUserVerifyingEmail(options);
|
||||
const userId = await Accounts.createUserVerifyingEmail(options);
|
||||
|
||||
// client gets logged in as the new user afterwards.
|
||||
return {userId: userId};
|
||||
@@ -948,10 +965,10 @@ Meteor.methods({createUser: function (...args) {
|
||||
* @param {Object} options.profile The user's profile, typically including the `name` field.
|
||||
* @importFromPackage accounts-base
|
||||
* */
|
||||
Accounts.createUserVerifyingEmail = (options) => {
|
||||
Accounts.createUserVerifyingEmail = async (options) => {
|
||||
options = { ...options };
|
||||
// Create user. result contains id and token.
|
||||
const userId = createUser(options);
|
||||
const userId = await createUser(options);
|
||||
// safety belt. createUser is supposed to throw on error. send 500 error
|
||||
// instead of sending a verification email with empty userid.
|
||||
if (! userId)
|
||||
@@ -976,14 +993,15 @@ Accounts.createUserVerifyingEmail = (options) => {
|
||||
// Unlike the client version, this does not log you in as this user
|
||||
// after creation.
|
||||
//
|
||||
// returns userId or throws an error if it can't create
|
||||
// returns Promise<userId> or throws an error if it can't create
|
||||
//
|
||||
// XXX add another argument ("server options") that gets sent to onCreateUser,
|
||||
// which is always empty when called from the createUser method? eg, "admin:
|
||||
// true", which we want to prevent the client from setting, but which a custom
|
||||
// method calling Accounts.createUser could set?
|
||||
//
|
||||
Accounts.createUser = (options, callback) => {
|
||||
|
||||
Accounts.createUserAsync = async (options, callback) => {
|
||||
options = { ...options };
|
||||
|
||||
// XXX allow an optional callback?
|
||||
@@ -994,6 +1012,23 @@ Accounts.createUser = (options, callback) => {
|
||||
return createUser(options);
|
||||
};
|
||||
|
||||
// Create user directly on the server.
|
||||
//
|
||||
// Unlike the client version, this does not log you in as this user
|
||||
// after creation.
|
||||
//
|
||||
// returns userId or throws an error if it can't create
|
||||
//
|
||||
// XXX add another argument ("server options") that gets sent to onCreateUser,
|
||||
// which is always empty when called from the createUser method? eg, "admin:
|
||||
// true", which we want to prevent the client from setting, but which a custom
|
||||
// method calling Accounts.createUser could set?
|
||||
//
|
||||
|
||||
Accounts.createUser = (options, callback) => {
|
||||
return Promise.await(Accounts.createUserAsync(options, callback));
|
||||
};
|
||||
|
||||
///
|
||||
/// PASSWORD-SPECIFIC INDEXES ON USERS
|
||||
///
|
||||
|
||||
@@ -1747,7 +1747,7 @@ if (Meteor.isServer) (() => {
|
||||
|
||||
Tinytest.addAsync(
|
||||
'passwords - allow custom bcrypt rounds',
|
||||
(test, done) => {
|
||||
async (test, done) => {
|
||||
const getUserHashRounds = user =>
|
||||
Number(user.services.password.bcrypt.substring(4, 6));
|
||||
|
||||
@@ -1768,7 +1768,7 @@ if (Meteor.isServer) (() => {
|
||||
const defaultRounds = Accounts._bcryptRounds();
|
||||
const customRounds = 11;
|
||||
Accounts._options.bcryptRounds = customRounds;
|
||||
Accounts._checkPassword(user1, password);
|
||||
await Accounts._checkPasswordAsync(user1, password);
|
||||
Meteor.setTimeout(() => {
|
||||
user1 = Meteor.users.findOne(userId1);
|
||||
rounds = getUserHashRounds(user1);
|
||||
|
||||
@@ -12,7 +12,7 @@ var xFrameOptions = defaultXFrameOptions;
|
||||
const BrowserPolicy = require("meteor/browser-policy-common").BrowserPolicy;
|
||||
BrowserPolicy.framing = {};
|
||||
|
||||
_.extend(BrowserPolicy.framing, {
|
||||
Object.assign(BrowserPolicy.framing, {
|
||||
// Exported for tests and browser-policy-common.
|
||||
_constructXFrameOptions: function () {
|
||||
return xFrameOptions;
|
||||
|
||||
@@ -5,7 +5,7 @@ Package.describe({
|
||||
|
||||
Package.onUse(function (api) {
|
||||
api.use("modules");
|
||||
api.use(["underscore", "browser-policy-common"], "server");
|
||||
api.use(["browser-policy-common"], "server");
|
||||
api.imply(["browser-policy-common"], "server");
|
||||
api.mainModule("browser-policy-framing.js", "server");
|
||||
});
|
||||
|
||||
@@ -1,17 +1,34 @@
|
||||
BrowserPolicy._setRunningTest();
|
||||
|
||||
var toObject = function(list, values) {
|
||||
if (list == null) return {};
|
||||
var result = {};
|
||||
for (var i = 0, length = list.length; i < length; i++) {
|
||||
if (values) {
|
||||
result[list[i]] = values[i];
|
||||
} else {
|
||||
result[list[i][0]] = list[i][1];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
var cspsEqual = function (csp1, csp2) {
|
||||
var cspToObj = function (csp) {
|
||||
csp = csp.substring(0, csp.length - 1);
|
||||
var parts = _.map(csp.split("; "), function (part) {
|
||||
var parts = csp.split("; ").map(function (part) {
|
||||
return part.split(" ");
|
||||
});
|
||||
var keys = _.map(parts, _.first);
|
||||
var values = _.map(parts, _.rest);
|
||||
_.each(values, function (value) {
|
||||
var keys = parts.map(part => part[0]);
|
||||
var values = parts.map((part) => {
|
||||
const [head, ...tail] = part;
|
||||
return tail;
|
||||
});
|
||||
values.forEach(function (value) {
|
||||
value.sort();
|
||||
});
|
||||
return _.object(keys, values);
|
||||
|
||||
return toObject(keys, values);
|
||||
};
|
||||
|
||||
return EJSON.equals(cspToObj(csp1), cspToObj(csp2));
|
||||
@@ -137,11 +154,11 @@ Tinytest.add("browser-policy - csp", function (test) {
|
||||
"default-src 'none'; frame-src https://foo.com; " +
|
||||
"object-src http://foo.com https://foo.com;"));
|
||||
|
||||
// Check that frame-ancestors property is set correctly.
|
||||
BrowserPolicy.content.allowFrameAncestorsOrigin("https://foo.com/");
|
||||
test.isTrue(cspsEqual(BrowserPolicy.content._constructCsp(),
|
||||
"default-src 'none'; frame-src https://foo.com; " +
|
||||
"object-src http://foo.com https://foo.com; " +
|
||||
// Check that frame-ancestors property is set correctly.
|
||||
BrowserPolicy.content.allowFrameAncestorsOrigin("https://foo.com/");
|
||||
test.isTrue(cspsEqual(BrowserPolicy.content._constructCsp(),
|
||||
"default-src 'none'; frame-src https://foo.com; " +
|
||||
"object-src http://foo.com https://foo.com; " +
|
||||
"frame-ancestors https://foo.com;"));
|
||||
|
||||
// CSP2 options: nonce
|
||||
@@ -188,4 +205,4 @@ Tinytest.add("browser-policy - X-Content-Type-Options", function (test) {
|
||||
test.equal(BrowserPolicy.content._xContentTypeOptions(), "nosniff");
|
||||
BrowserPolicy.content.allowContentTypeSniffing();
|
||||
test.equal(BrowserPolicy.content._xContentTypeOptions(), undefined);
|
||||
});
|
||||
});
|
||||
@@ -11,6 +11,6 @@ Package.onUse(function (api) {
|
||||
});
|
||||
|
||||
Package.onTest(function (api) {
|
||||
api.use(["tinytest", "browser-policy", "ejson", "underscore"], "server");
|
||||
api.use(["tinytest", "browser-policy", "ejson"], "server");
|
||||
api.addFiles("browser-policy-test.js", "server");
|
||||
});
|
||||
|
||||
@@ -14,7 +14,6 @@ Package.onUse(function (api) {
|
||||
Package.onTest(function (api) {
|
||||
api.use([
|
||||
'tinytest',
|
||||
'underscore',
|
||||
'ejson'
|
||||
]);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Tinytest.add("diff-sequence - diff changes ordering", function (test) {
|
||||
var makeDocs = function (ids) {
|
||||
return _.map(ids, function (id) { return {_id: id};});
|
||||
return ids.map(function (id) { return {_id: id};});
|
||||
};
|
||||
var testMutation = function (a, b) {
|
||||
var aa = makeDocs(a);
|
||||
@@ -10,12 +10,12 @@ Tinytest.add("diff-sequence - diff changes ordering", function (test) {
|
||||
|
||||
addedBefore: function (id, doc, before) {
|
||||
if (before === null) {
|
||||
aaCopy.push( _.extend({_id: id}, doc));
|
||||
aaCopy.push( Object.assign({_id: id}, doc));
|
||||
return;
|
||||
}
|
||||
for (var i = 0; i < aaCopy.length; i++) {
|
||||
if (aaCopy[i]._id === before) {
|
||||
aaCopy.splice(i, 0, _.extend({_id: id}, doc));
|
||||
aaCopy.splice(i, 0, Object.assign({_id: id}, doc));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -29,12 +29,12 @@ Tinytest.add("diff-sequence - diff changes ordering", function (test) {
|
||||
}
|
||||
}
|
||||
if (before === null) {
|
||||
aaCopy.push( _.extend({_id: id}, found));
|
||||
aaCopy.push( Object.assign({_id: id}, found));
|
||||
return;
|
||||
}
|
||||
for (i = 0; i < aaCopy.length; i++) {
|
||||
if (aaCopy[i]._id === before) {
|
||||
aaCopy.splice(i, 0, _.extend({_id: id}, found));
|
||||
aaCopy.splice(i, 0, Object.assign({_id: id}, found));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,7 @@ Tinytest.add("diff-sequence - diff", function (test) {
|
||||
for (var i = 1; i <= origLen; i++)
|
||||
oldResults[i-1] = {_id: i};
|
||||
|
||||
var newResults = _.map(newOldIdx, function(n) {
|
||||
var newResults = newOldIdx.map(function(n) {
|
||||
var doc = {_id: Math.abs(n)};
|
||||
if (n < 0)
|
||||
doc.changed = true;
|
||||
@@ -89,7 +89,7 @@ Tinytest.add("diff-sequence - diff", function (test) {
|
||||
return -1;
|
||||
};
|
||||
|
||||
var results = _.clone(oldResults);
|
||||
var results = [...oldResults];
|
||||
var observer = {
|
||||
addedBefore: function(id, fields, before) {
|
||||
var before_idx;
|
||||
@@ -97,7 +97,7 @@ Tinytest.add("diff-sequence - diff", function (test) {
|
||||
before_idx = results.length;
|
||||
else
|
||||
before_idx = find (results, before);
|
||||
var doc = _.extend({_id: id}, fields);
|
||||
var doc = Object.assign({_id: id}, fields);
|
||||
test.isFalse(before_idx < 0 || before_idx > results.length);
|
||||
results.splice(before_idx, 0, doc);
|
||||
},
|
||||
@@ -157,4 +157,3 @@ Tinytest.add("diff-sequence - diff", function (test) {
|
||||
diffTest(3, [-3, -2, -1]);
|
||||
diffTest(10, [-2, 7, 4, 6, 11, -3, -8, 9]);
|
||||
});
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ Package.onUse(function(api) {
|
||||
});
|
||||
|
||||
Package.onTest(function(api) {
|
||||
api.use(['tinytest', 'underscore']);
|
||||
api.use(['tinytest']);
|
||||
api.use(['es5-shim', 'ecmascript', 'babel-compiler']);
|
||||
api.addFiles('runtime-tests.js');
|
||||
api.addFiles('transpilation-tests.js', 'server');
|
||||
|
||||
@@ -216,7 +216,7 @@ Tinytest.add('ecmascript - runtime - block scope', test => {
|
||||
});
|
||||
}
|
||||
|
||||
_.each(thunks, f => f());
|
||||
thunks.forEach(f => f());
|
||||
test.equal(buf, [0, 1, 2]);
|
||||
}
|
||||
});
|
||||
|
||||
71
packages/ejson/ejson.d.ts
vendored
Normal file
71
packages/ejson/ejson.d.ts
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
export interface EJSONableCustomType {
|
||||
clone?(): EJSONableCustomType;
|
||||
equals?(other: Object): boolean;
|
||||
toJSONValue(): JSONable;
|
||||
typeName(): string;
|
||||
}
|
||||
|
||||
export type EJSONableProperty =
|
||||
| number
|
||||
| string
|
||||
| boolean
|
||||
| Object
|
||||
| number[]
|
||||
| string[]
|
||||
| Object[]
|
||||
| Date
|
||||
| Uint8Array
|
||||
| EJSONableCustomType
|
||||
| undefined
|
||||
| null;
|
||||
|
||||
export interface EJSONable {
|
||||
[key: string]: EJSONableProperty;
|
||||
}
|
||||
|
||||
export interface JSONable {
|
||||
[key: string]:
|
||||
| number
|
||||
| string
|
||||
| boolean
|
||||
| Object
|
||||
| number[]
|
||||
| string[]
|
||||
| Object[]
|
||||
| undefined
|
||||
| null;
|
||||
}
|
||||
|
||||
export interface EJSON extends EJSONable {}
|
||||
|
||||
export namespace EJSON {
|
||||
function addType(
|
||||
name: string,
|
||||
factory: (val: JSONable) => EJSONableCustomType
|
||||
): void;
|
||||
|
||||
function clone<T>(val: T): T;
|
||||
|
||||
function equals(
|
||||
a: EJSON,
|
||||
b: EJSON,
|
||||
options?: { keyOrderSensitive?: boolean | undefined }
|
||||
): boolean;
|
||||
|
||||
function fromJSONValue(val: JSONable): any;
|
||||
|
||||
function isBinary(x: Object): x is Uint8Array;
|
||||
function newBinary(size: number): Uint8Array;
|
||||
|
||||
function parse(str: string): EJSON;
|
||||
|
||||
function stringify(
|
||||
val: EJSON,
|
||||
options?: {
|
||||
indent?: boolean | number | string | undefined;
|
||||
canonical?: boolean | undefined;
|
||||
}
|
||||
): string;
|
||||
|
||||
function toJSONValue(val: EJSON): JSONable;
|
||||
}
|
||||
3
packages/ejson/package-types.json
Normal file
3
packages/ejson/package-types.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"typesEntry": "ejson.d.ts"
|
||||
}
|
||||
@@ -5,6 +5,7 @@ Package.describe({
|
||||
|
||||
Package.onUse(function onUse(api) {
|
||||
api.use(['ecmascript', 'base64']);
|
||||
api.addAssets('ejson.d.ts', 'server');
|
||||
api.mainModule('ejson.js');
|
||||
api.export('EJSON');
|
||||
});
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Meteor } from 'meteor/meteor';
|
||||
import { Log } from 'meteor/logging';
|
||||
import { Hook } from 'meteor/callback-hook';
|
||||
|
||||
import Future from 'fibers/future';
|
||||
import url from 'url';
|
||||
import nodemailer from 'nodemailer';
|
||||
import wellKnow from 'nodemailer/lib/well-known';
|
||||
@@ -25,7 +24,7 @@ export const EmailInternals = {
|
||||
|
||||
const MailComposer = EmailInternals.NpmModules.mailcomposer.module;
|
||||
|
||||
const makeTransport = function(mailUrlString) {
|
||||
const makeTransport = function (mailUrlString) {
|
||||
const mailUrl = new URL(mailUrlString);
|
||||
|
||||
if (mailUrl.protocol !== 'smtp:' && mailUrl.protocol !== 'smtps:') {
|
||||
@@ -60,7 +59,7 @@ const makeTransport = function(mailUrlString) {
|
||||
};
|
||||
|
||||
// More info: https://nodemailer.com/smtp/well-known/
|
||||
const knownHostsTransport = function(settings = undefined, url = undefined) {
|
||||
const knownHostsTransport = function (settings = undefined, url = undefined) {
|
||||
let service, user, password;
|
||||
|
||||
const hasSettings = settings && Object.keys(settings).length;
|
||||
@@ -110,7 +109,7 @@ const knownHostsTransport = function(settings = undefined, url = undefined) {
|
||||
};
|
||||
EmailTest.knowHostsTransport = knownHostsTransport;
|
||||
|
||||
const getTransport = function() {
|
||||
const getTransport = function () {
|
||||
const packageSettings = Meteor.settings.packages?.email || {};
|
||||
// We delay this check until the first call to Email.send, in case someone
|
||||
// set process.env.MAIL_URL in startup code. Then we store in a cache until
|
||||
@@ -138,40 +137,40 @@ const getTransport = function() {
|
||||
};
|
||||
|
||||
let nextDevModeMailId = 0;
|
||||
let output_stream = process.stdout;
|
||||
|
||||
EmailTest._getAndIncNextDevModeMailId = function () {
|
||||
return nextDevModeMailId++;
|
||||
};
|
||||
|
||||
// Testing hooks
|
||||
EmailTest.overrideOutputStream = function(stream) {
|
||||
EmailTest.resetNextDevModeMailId = function () {
|
||||
nextDevModeMailId = 0;
|
||||
output_stream = stream;
|
||||
};
|
||||
|
||||
EmailTest.restoreOutputStream = function() {
|
||||
output_stream = process.stdout;
|
||||
};
|
||||
const devModeSendAsync = function (mail, options) {
|
||||
const stream = options?.stream || process.stdout;
|
||||
return new Promise((resolve, reject) => {
|
||||
let devModeMailId = EmailTest._getAndIncNextDevModeMailId();
|
||||
|
||||
const devModeSend = function(mail) {
|
||||
let devModeMailId = nextDevModeMailId++;
|
||||
|
||||
const stream = output_stream;
|
||||
|
||||
// This approach does not prevent other writers to stdout from interleaving.
|
||||
stream.write('====== BEGIN MAIL #' + devModeMailId + ' ======\n');
|
||||
stream.write(
|
||||
'(Mail not sent; to enable sending, set the MAIL_URL ' +
|
||||
// This approach does not prevent other writers to stdout from interleaving.
|
||||
const output = ['====== BEGIN MAIL #' + devModeMailId + ' ======\n'];
|
||||
output.push(
|
||||
'(Mail not sent; to enable sending, set the MAIL_URL ' +
|
||||
'environment variable.)\n'
|
||||
);
|
||||
const readStream = new MailComposer(mail).compile().createReadStream();
|
||||
readStream.pipe(stream, { end: false });
|
||||
const future = new Future();
|
||||
readStream.on('end', function() {
|
||||
stream.write('====== END MAIL #' + devModeMailId + ' ======\n');
|
||||
future.return();
|
||||
);
|
||||
const readStream = new MailComposer(mail).compile().createReadStream();
|
||||
readStream.on('data', buffer => {
|
||||
output.push(buffer.toString());
|
||||
});
|
||||
readStream.on('end', function () {
|
||||
output.push('====== END MAIL #' + devModeMailId + ' ======\n');
|
||||
stream.write(output.join(''), () => resolve());
|
||||
});
|
||||
readStream.on('error', (err) => reject(err));
|
||||
});
|
||||
future.wait();
|
||||
};
|
||||
|
||||
const smtpSend = function(transport, mail) {
|
||||
const smtpSend = function (transport, mail) {
|
||||
transport._syncSendMail(mail);
|
||||
};
|
||||
|
||||
@@ -186,7 +185,7 @@ const sendHooks = new Hook();
|
||||
* false to skip sending.
|
||||
* @returns {{ stop: function, callback: function }}
|
||||
*/
|
||||
Email.hookSend = function(f) {
|
||||
Email.hookSend = function (f) {
|
||||
return sendHooks.register(f);
|
||||
};
|
||||
|
||||
@@ -231,25 +230,77 @@ Email.customTransport = undefined;
|
||||
* You can create a `MailComposer` object via
|
||||
* `new EmailInternals.NpmModules.mailcomposer.module`.
|
||||
*/
|
||||
Email.send = function(options) {
|
||||
if (options.mailComposer) {
|
||||
options = options.mailComposer.mail;
|
||||
Email.send = function (options) {
|
||||
if (Email.customTransport) {
|
||||
// Preserve current behavior
|
||||
const email = options.mailComposer ? options.mailComposer.mail : options;
|
||||
let send = true;
|
||||
sendHooks.forEach((hook) => {
|
||||
send = hook(email);
|
||||
return send;
|
||||
});
|
||||
if (!send) {
|
||||
return;
|
||||
}
|
||||
const packageSettings = Meteor.settings.packages?.email || {};
|
||||
Email.customTransport({ packageSettings, ...email });
|
||||
return;
|
||||
}
|
||||
// Using Fibers Promise.await
|
||||
return Promise.await(Email.sendAsync(options));
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Send an email with asyncronous method. Capture Throws an `Error` on failure to contact mail server
|
||||
* or if mail server returns an error. All fields should match
|
||||
* [RFC5322](http://tools.ietf.org/html/rfc5322) specification.
|
||||
*
|
||||
* If the `MAIL_URL` environment variable is set, actually sends the email.
|
||||
* Otherwise, prints the contents of the email to standard out.
|
||||
*
|
||||
* Note that this package is based on **nodemailer**, so make sure to refer to
|
||||
* [the documentation](http://nodemailer.com/)
|
||||
* when using the `attachments` or `mailComposer` options.
|
||||
*
|
||||
* @locus Server
|
||||
* @return {Promise}
|
||||
* @param {Object} options
|
||||
* @param {String} [options.from] "From:" address (required)
|
||||
* @param {String|String[]} options.to,cc,bcc,replyTo
|
||||
* "To:", "Cc:", "Bcc:", and "Reply-To:" addresses
|
||||
* @param {String} [options.inReplyTo] Message-ID this message is replying to
|
||||
* @param {String|String[]} [options.references] Array (or space-separated string) of Message-IDs to refer to
|
||||
* @param {String} [options.messageId] Message-ID for this message; otherwise, will be set to a random value
|
||||
* @param {String} [options.subject] "Subject:" line
|
||||
* @param {String} [options.text|html] Mail body (in plain text and/or HTML)
|
||||
* @param {String} [options.watchHtml] Mail body in HTML specific for Apple Watch
|
||||
* @param {String} [options.icalEvent] iCalendar event attachment
|
||||
* @param {Object} [options.headers] Dictionary of custom headers - e.g. `{ "header name": "header value" }`. To set an object under a header name, use `JSON.stringify` - e.g. `{ "header name": JSON.stringify({ tracking: { level: 'full' } }) }`.
|
||||
* @param {Object[]} [options.attachments] Array of attachment objects, as
|
||||
* described in the [nodemailer documentation](https://nodemailer.com/message/attachments/).
|
||||
* @param {MailComposer} [options.mailComposer] A [MailComposer](https://nodemailer.com/extras/mailcomposer/#e-mail-message-fields)
|
||||
* object representing the message to be sent. Overrides all other options.
|
||||
* You can create a `MailComposer` object via
|
||||
* `new EmailInternals.NpmModules.mailcomposer.module`.
|
||||
*/
|
||||
Email.sendAsync = async function (options) {
|
||||
|
||||
const email = options.mailComposer ? options.mailComposer.mail : options;
|
||||
|
||||
let send = true;
|
||||
sendHooks.forEach(hook => {
|
||||
send = hook(options);
|
||||
sendHooks.forEach((hook) => {
|
||||
send = hook(email);
|
||||
return send;
|
||||
});
|
||||
if (!send) return;
|
||||
|
||||
const customTransport = Email.customTransport;
|
||||
if (customTransport) {
|
||||
const packageSettings = Meteor.settings.packages?.email || {};
|
||||
customTransport({ packageSettings, ...options });
|
||||
if (!send) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Email.customTransport) {
|
||||
const packageSettings = Meteor.settings.packages?.email || {};
|
||||
return Email.customTransport({ packageSettings, ...email });
|
||||
}
|
||||
|
||||
const mailUrlEnv = process.env.MAIL_URL;
|
||||
const mailUrlSettings = Meteor.settings.packages?.email;
|
||||
|
||||
@@ -263,8 +314,8 @@ Email.send = function(options) {
|
||||
|
||||
if (mailUrlEnv || mailUrlSettings) {
|
||||
const transport = getTransport();
|
||||
smtpSend(transport, options);
|
||||
smtpSend(transport, email);
|
||||
return;
|
||||
}
|
||||
devModeSend(options);
|
||||
return devModeSendAsync(email, options);
|
||||
};
|
||||
|
||||
21
packages/email/email_test_helpers.js
Normal file
21
packages/email/email_test_helpers.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import streamBuffers from 'stream-buffers';
|
||||
|
||||
export const devWarningBanner =
|
||||
'(Mail not sent; to enable ' +
|
||||
'sending, set the MAIL_URL environment variable.)\n';
|
||||
|
||||
export const smokeEmailTest = (testFunction) => {
|
||||
// This only tests dev mode, so don't run the test if this is deployed.
|
||||
if (process.env.MAIL_URL) return;
|
||||
const stream = new streamBuffers.WritableStreamBuffer();
|
||||
EmailTest.resetNextDevModeMailId();
|
||||
testFunction(stream);
|
||||
};
|
||||
|
||||
export const canonicalize = (string) => {
|
||||
// Remove generated content for test.equal to succeed.
|
||||
return string
|
||||
.replace(/Message-ID: <[^<>]*>\r\n/, 'Message-ID: <...>\r\n')
|
||||
.replace(/Date: (?!dummy).*\r\n/, 'Date: ...\r\n')
|
||||
.replace(/(boundary="|^--)--[^\s"]+?(-Part|")/gm, '$1--...$2');
|
||||
};
|
||||
@@ -1,304 +1,85 @@
|
||||
import streamBuffers from 'stream-buffers';
|
||||
import { Email } from 'meteor/email';
|
||||
import { smokeEmailTest } from './email_test_helpers';
|
||||
import { TEST_CASES } from './email_tests_data';
|
||||
|
||||
const devWarningBanner = "(Mail not sent; to enable " +
|
||||
"sending, set the MAIL_URL environment variable.)\n";
|
||||
const CUSTOM_TRANSPORT_SETTINGS = {
|
||||
email: { service: '1on1', user: 'test', password: 'pwd' },
|
||||
};
|
||||
|
||||
function smokeEmailTest(testFunction) {
|
||||
// This only tests dev mode, so don't run the test if this is deployed.
|
||||
if (process.env.MAIL_URL) return;
|
||||
const sleep = (ms) => {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
};
|
||||
|
||||
try {
|
||||
const stream = new streamBuffers.WritableStreamBuffer;
|
||||
EmailTest.overrideOutputStream(stream);
|
||||
// Create dynamic sync tests
|
||||
TEST_CASES.forEach(({ title, options, testCalls }) => {
|
||||
Tinytest.add(`[Sync] ${title}`, function (test) {
|
||||
smokeEmailTest((stream) => {
|
||||
Object.entries(options).forEach(([key, option]) => {
|
||||
const testCall = testCalls[key];
|
||||
Email.send({ ...option, stream });
|
||||
testCall(test, stream);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
testFunction(stream);
|
||||
// Create dynamic async tests
|
||||
TEST_CASES.forEach(({ title, options, testCalls }) => {
|
||||
Tinytest.addAsync(`[Async] ${title}`, function (test, onComplete) {
|
||||
smokeEmailTest((stream) => {
|
||||
const allPromises = Object.entries(options).map(([key, option]) => {
|
||||
const testCall = testCalls[key];
|
||||
return Email.sendAsync({ ...option, stream }).then(() => {
|
||||
testCall(test, stream);
|
||||
});
|
||||
});
|
||||
Promise.all(allPromises).then(() => onComplete());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
} finally {
|
||||
EmailTest.restoreOutputStream();
|
||||
// Individual sync tests
|
||||
|
||||
Tinytest.add(
|
||||
'[Sync] email - alternate API is used for sending gets data',
|
||||
function (test) {
|
||||
smokeEmailTest(function (stream) {
|
||||
Email.customTransport = (options) => {
|
||||
test.equal(options.from, 'foo@example.com');
|
||||
};
|
||||
Email.send({
|
||||
from: 'foo@example.com',
|
||||
to: 'bar@example.com',
|
||||
text: '*Cool*, man',
|
||||
html: '<i>Cool</i>, man',
|
||||
stream,
|
||||
});
|
||||
test.equal(stream.getContentsAsString('utf8'), false);
|
||||
});
|
||||
|
||||
smokeEmailTest(function (stream) {
|
||||
Meteor.settings.packages = CUSTOM_TRANSPORT_SETTINGS;
|
||||
Email.customTransport = (options) => {
|
||||
test.equal(options.from, 'foo@example.com');
|
||||
test.equal(options.packageSettings?.service, '1on1');
|
||||
};
|
||||
|
||||
Email.send({
|
||||
from: 'foo@example.com',
|
||||
to: 'bar@example.com',
|
||||
text: '*Cool*, man',
|
||||
html: '<i>Cool</i>, man',
|
||||
stream,
|
||||
});
|
||||
|
||||
test.equal(stream.getContentsAsString('utf8'), false);
|
||||
});
|
||||
Email.customTransport = undefined;
|
||||
Meteor.settings.packages = undefined;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function canonicalize(string) {
|
||||
// Remove generated content for test.equal to succeed.
|
||||
return string.replace(/Message-ID: <[^<>]*>\r\n/, "Message-ID: <...>\r\n")
|
||||
.replace(/Date: (?!dummy).*\r\n/, "Date: ...\r\n")
|
||||
.replace(/(boundary="|^--)--[^\s"]+?(-Part|")/mg, "$1--...$2");
|
||||
}
|
||||
|
||||
Tinytest.add("email - fully customizable", function (test) {
|
||||
smokeEmailTest(function(stream) {
|
||||
Email.send({
|
||||
from: "foo@example.com",
|
||||
to: "bar@example.com",
|
||||
cc: ["friends@example.com", "enemies@example.com"],
|
||||
subject: "This is the subject",
|
||||
text: "This is the body\nof the message\nFrom us.",
|
||||
headers: {
|
||||
'X-Meteor-Test': 'a custom header',
|
||||
'Date': 'dummy',
|
||||
},
|
||||
});
|
||||
// XXX brittle if mailcomposer changes header order, etc
|
||||
test.equal(canonicalize(stream.getContentsAsString("utf8")),
|
||||
"====== BEGIN MAIL #0 ======\n" +
|
||||
devWarningBanner +
|
||||
"Content-Type: text/plain; charset=utf-8\r\n" +
|
||||
"X-Meteor-Test: a custom header\r\n" +
|
||||
"Date: dummy\r\n" +
|
||||
"From: foo@example.com\r\n" +
|
||||
"To: bar@example.com\r\n" +
|
||||
"Cc: friends@example.com, enemies@example.com\r\n" +
|
||||
"Subject: This is the subject\r\n" +
|
||||
"Message-ID: <...>\r\n" +
|
||||
"Content-Transfer-Encoding: 7bit\r\n" +
|
||||
"MIME-Version: 1.0\r\n" +
|
||||
"\r\n" +
|
||||
"This is the body\n" +
|
||||
"of the message\n" +
|
||||
"From us.\r\n" +
|
||||
"====== END MAIL #0 ======\n");
|
||||
});
|
||||
});
|
||||
|
||||
Tinytest.add("email - undefined headers sends properly", function (test) {
|
||||
smokeEmailTest(function (stream) {
|
||||
Email.send({
|
||||
from: "foo@example.com",
|
||||
to: "bar@example.com",
|
||||
subject: "This is the subject",
|
||||
text: "This is the body\nof the message\nFrom us.",
|
||||
});
|
||||
|
||||
test.matches(canonicalize(stream.getContentsAsString("utf8")),
|
||||
/^====== BEGIN MAIL #0 ======$[\s\S]+^To: bar@example.com$/m);
|
||||
});
|
||||
});
|
||||
|
||||
Tinytest.add("email - multiple e-mails same stream", function (test) {
|
||||
smokeEmailTest(function (stream) {
|
||||
Email.send({
|
||||
from: "foo@example.com",
|
||||
to: "bar@example.com",
|
||||
subject: "This is the subject",
|
||||
text: "This is the body\nof the message\nFrom us.",
|
||||
});
|
||||
|
||||
const contents = canonicalize(stream.getContentsAsString("utf8"));
|
||||
test.matches(contents, /^====== BEGIN MAIL #0 ======$/m);
|
||||
test.matches(contents, /^From: foo@example.com$/m);
|
||||
test.matches(contents, /^To: bar@example.com$/m);
|
||||
|
||||
Email.send({
|
||||
from: "qux@example.com",
|
||||
to: "baz@example.com",
|
||||
subject: "This is important",
|
||||
text: "This is another message\nFrom Qux.",
|
||||
});
|
||||
|
||||
const contents2 = canonicalize(stream.getContentsAsString("utf8"));
|
||||
test.matches(contents2, /^====== BEGIN MAIL #1 ======$/m);
|
||||
test.matches(contents2, /^From: qux@example.com$/m);
|
||||
test.matches(contents2, /^To: baz@example.com$/m);
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
Tinytest.add("email - using mail composer", function (test) {
|
||||
smokeEmailTest(function (stream) {
|
||||
// Test direct MailComposer usage.
|
||||
const mc = new EmailInternals.NpmModules.mailcomposer.module({
|
||||
from: "a@b.com",
|
||||
text: "body"
|
||||
});
|
||||
Email.send({mailComposer: mc});
|
||||
test.equal(canonicalize(stream.getContentsAsString("utf8")),
|
||||
"====== BEGIN MAIL #0 ======\n" +
|
||||
devWarningBanner +
|
||||
"Content-Type: text/plain; charset=utf-8\r\n" +
|
||||
"From: a@b.com\r\n" +
|
||||
"Message-ID: <...>\r\n" +
|
||||
"Content-Transfer-Encoding: 7bit\r\n" +
|
||||
"Date: ...\r\n" +
|
||||
"MIME-Version: 1.0\r\n" +
|
||||
"\r\n" +
|
||||
"body\r\n" +
|
||||
"====== END MAIL #0 ======\n");
|
||||
});
|
||||
});
|
||||
|
||||
Tinytest.add("email - date auto generated", function (test) {
|
||||
smokeEmailTest(function (stream) {
|
||||
// Test if date header is automatically generated, if not specified
|
||||
Email.send({
|
||||
from: "foo@example.com",
|
||||
to: "bar@example.com",
|
||||
subject: "This is the subject",
|
||||
text: "This is the body\nof the message\nFrom us.",
|
||||
headers: {
|
||||
'X-Meteor-Test': 'a custom header',
|
||||
},
|
||||
});
|
||||
|
||||
test.matches(canonicalize(stream.getContentsAsString("utf8")),
|
||||
/^Date: .+$/m);
|
||||
});
|
||||
});
|
||||
|
||||
Tinytest.add("email - long lines", function (test) {
|
||||
smokeEmailTest(function (stream) {
|
||||
// Test that long header lines get wrapped with single leading whitespace,
|
||||
// and that long body lines get wrapped with quoted-printable conventions.
|
||||
Email.send({
|
||||
from: "foo@example.com",
|
||||
to: "bar@example.com",
|
||||
subject: "This is a very very very very very very very very very very very very long subject",
|
||||
text: "This is a very very very very very very very very very very very very long text",
|
||||
});
|
||||
|
||||
test.equal(canonicalize(stream.getContentsAsString("utf8")),
|
||||
"====== BEGIN MAIL #0 ======\n" +
|
||||
devWarningBanner +
|
||||
"Content-Type: text/plain; charset=utf-8\r\n" +
|
||||
"From: foo@example.com\r\n" +
|
||||
"To: bar@example.com\r\n" +
|
||||
"Subject: This is a very very very very very very very very " +
|
||||
"very very very\r\n very long subject\r\n" +
|
||||
"Message-ID: <...>\r\n" +
|
||||
"Content-Transfer-Encoding: quoted-printable\r\n" +
|
||||
"Date: ...\r\n" +
|
||||
"MIME-Version: 1.0\r\n" +
|
||||
"\r\n" +
|
||||
"This is a very very very very very very very very very very " +
|
||||
"very very long =\r\ntext\r\n" +
|
||||
"====== END MAIL #0 ======\n");
|
||||
});
|
||||
});
|
||||
|
||||
Tinytest.add("email - unicode", function (test) {
|
||||
smokeEmailTest(function (stream) {
|
||||
// Test that unicode characters in header and body get encoded.
|
||||
Email.send({
|
||||
from: "foo@example.com",
|
||||
to: "bar@example.com",
|
||||
subject: "\u263a",
|
||||
text: "I \u2665 Meteor",
|
||||
});
|
||||
|
||||
test.equal(canonicalize(stream.getContentsAsString("utf8")),
|
||||
"====== BEGIN MAIL #0 ======\n" +
|
||||
devWarningBanner +
|
||||
"Content-Type: text/plain; charset=utf-8\r\n" +
|
||||
"From: foo@example.com\r\n" +
|
||||
"To: bar@example.com\r\n" +
|
||||
"Subject: =?UTF-8?B?4pi6?=\r\n" +
|
||||
"Message-ID: <...>\r\n" +
|
||||
"Content-Transfer-Encoding: quoted-printable\r\n" +
|
||||
"Date: ...\r\n" +
|
||||
"MIME-Version: 1.0\r\n" +
|
||||
"\r\n" +
|
||||
"I =E2=99=A5 Meteor\r\n" +
|
||||
"====== END MAIL #0 ======\n");
|
||||
});
|
||||
});
|
||||
|
||||
Tinytest.add("email - text and html", function (test) {
|
||||
smokeEmailTest(function (stream) {
|
||||
// Test including both text and HTML versions of message.
|
||||
Email.send({
|
||||
from: "foo@example.com",
|
||||
to: "bar@example.com",
|
||||
text: "*Cool*, man",
|
||||
html: "<i>Cool</i>, man",
|
||||
});
|
||||
|
||||
test.equal(canonicalize(stream.getContentsAsString("utf8")),
|
||||
"====== BEGIN MAIL #0 ======\n" +
|
||||
devWarningBanner +
|
||||
"Content-Type: multipart/alternative;\r\n" +
|
||||
' boundary="--...-Part_1"\r\n' +
|
||||
"From: foo@example.com\r\n" +
|
||||
"To: bar@example.com\r\n" +
|
||||
"Message-ID: <...>\r\n" +
|
||||
"Date: ...\r\n" +
|
||||
"MIME-Version: 1.0\r\n" +
|
||||
"\r\n" +
|
||||
"----...-Part_1\r\n" +
|
||||
"Content-Type: text/plain; charset=utf-8\r\n" +
|
||||
"Content-Transfer-Encoding: 7bit\r\n" +
|
||||
"\r\n" +
|
||||
"*Cool*, man\r\n" +
|
||||
"----...-Part_1\r\n" +
|
||||
"Content-Type: text/html; charset=utf-8\r\n" +
|
||||
"Content-Transfer-Encoding: 7bit\r\n" +
|
||||
"\r\n" +
|
||||
"<i>Cool</i>, man\r\n" +
|
||||
"----...-Part_1--\r\n" +
|
||||
"====== END MAIL #0 ======\n");
|
||||
});
|
||||
});
|
||||
|
||||
Tinytest.add("email - alternate API is used for sending gets data", function(test) {
|
||||
smokeEmailTest(function(stream) {
|
||||
Email.customTransport = (options) => {
|
||||
test.equal(options.from, 'foo@example.com');
|
||||
};
|
||||
Email.send({
|
||||
from: "foo@example.com",
|
||||
to: "bar@example.com",
|
||||
text: "*Cool*, man",
|
||||
html: "<i>Cool</i>, man",
|
||||
});
|
||||
test.equal(stream.getContentsAsString("utf8"), false);
|
||||
});
|
||||
|
||||
smokeEmailTest(function(stream) {
|
||||
Meteor.settings.packages = { email: { service: '1on1', user: 'test', password: 'pwd' } };
|
||||
Email.customTransport = (options) => {
|
||||
test.equal(options.from, 'foo@example.com');
|
||||
test.equal(options.packageSettings?.service, '1on1');
|
||||
};
|
||||
|
||||
Email.send({
|
||||
from: "foo@example.com",
|
||||
to: "bar@example.com",
|
||||
text: "*Cool*, man",
|
||||
html: "<i>Cool</i>, man",
|
||||
});
|
||||
|
||||
test.equal(stream.getContentsAsString("utf8"), false);
|
||||
});
|
||||
Email.customTransport = undefined;
|
||||
Meteor.settings.packages = undefined;
|
||||
});
|
||||
|
||||
Tinytest.add("email - URL string for known hosts", function(test) {
|
||||
const oneTransport = EmailTest.knowHostsTransport({ service: '1und1', user: 'test', password: 'pwd' });
|
||||
test.equal(oneTransport.transporter.auth.type, 'LOGIN');
|
||||
test.equal(oneTransport.transporter.auth.user, 'test');
|
||||
|
||||
const aolUrlTransport = EmailTest.knowHostsTransport(null, 'AOL://test:pwd@aol.com');
|
||||
test.equal(aolUrlTransport.transporter.auth.user, 'test');
|
||||
test.equal(aolUrlTransport.transporter.auth.type, 'LOGIN');
|
||||
|
||||
const outlookTransport = EmailTest.knowHostsTransport(null, 'Outlook365://firstname.lastname%40hotmail.com:password@hotmail.com');
|
||||
const outlookTransport2 = EmailTest.knowHostsTransport(undefined, 'Outlook365://firstname.lastname@hotmail.com:password@hotmail.com');
|
||||
test.equal(outlookTransport.transporter.auth.user, 'firstname.lastname%40hotmail.com');
|
||||
test.equal(outlookTransport.options.auth.user, 'firstname.lastname%40hotmail.com');
|
||||
test.equal(outlookTransport.transporter.options.service, 'outlook365');
|
||||
test.equal(outlookTransport2.transporter.auth.user, 'firstname.lastname%40hotmail.com');
|
||||
test.equal(outlookTransport2.transporter.options.service, 'outlook365');
|
||||
|
||||
const hotmailTransport = EmailTest.knowHostsTransport(undefined, 'Hotmail://firstname.lastname@hotmail.com:password@hotmail.com');
|
||||
console.dir(hotmailTransport);
|
||||
test.equal(hotmailTransport.transporter.options.service, 'hotmail');
|
||||
|
||||
const falseService = { service: '1on1', user: 'test', password: 'pwd' };
|
||||
const errorMsg = 'Could not recognize e-mail service. See list at https://nodemailer.com/smtp/well-known/ for services that we can configure for you.';
|
||||
test.throws(() => EmailTest.knowHostsTransport(falseService), errorMsg);
|
||||
test.throws(() => EmailTest.knowHostsTransport(null, 'smtp://bbb:bb@bb.com'), errorMsg);
|
||||
});
|
||||
|
||||
Tinytest.add("email - hooks stop the sending", function(test) {
|
||||
Tinytest.add('[Sync] email - hooks stop the sending', function (test) {
|
||||
// Register hooks
|
||||
const hook1 = Email.hookSend((options) => {
|
||||
// Test that we get options through
|
||||
@@ -313,17 +94,218 @@ Tinytest.add("email - hooks stop the sending", function(test) {
|
||||
const hook3 = Email.hookSend(() => {
|
||||
console.log('FAIL');
|
||||
});
|
||||
smokeEmailTest(function(stream) {
|
||||
smokeEmailTest(function (stream) {
|
||||
Email.send({
|
||||
from: "foo@example.com",
|
||||
to: "bar@example.com",
|
||||
text: "*Cool*, man",
|
||||
html: "<i>Cool</i>, man",
|
||||
from: 'foo@example.com',
|
||||
to: 'bar@example.com',
|
||||
text: '*Cool*, man',
|
||||
html: '<i>Cool</i>, man',
|
||||
stream,
|
||||
});
|
||||
|
||||
test.equal(stream.getContentsAsString("utf8"), false);
|
||||
test.equal(stream.getContentsAsString('utf8'), false);
|
||||
});
|
||||
hook1.stop();
|
||||
hook2.stop();
|
||||
hook3.stop();
|
||||
});
|
||||
|
||||
// Individual Async tests
|
||||
|
||||
Tinytest.addAsync(
|
||||
'[Async] email - alternate API is used for sending gets data',
|
||||
function (test, onComplete) {
|
||||
const allPromises = [];
|
||||
smokeEmailTest((stream) => {
|
||||
Email.customTransport = (options) => {
|
||||
test.equal(options.from, 'foo@example.com');
|
||||
};
|
||||
allPromises.push(
|
||||
Email.sendAsync({
|
||||
from: 'foo@example.com',
|
||||
to: 'bar@example.com',
|
||||
text: '*Cool*, man',
|
||||
html: '<i>Cool</i>, man',
|
||||
stream,
|
||||
}).then(() => {
|
||||
test.equal(stream.getContentsAsString('utf8'), false);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
smokeEmailTest(function (stream) {
|
||||
Meteor.settings.packages = CUSTOM_TRANSPORT_SETTINGS;
|
||||
Email.customTransport = (options) => {
|
||||
test.equal(options.from, 'foo@example.com');
|
||||
test.equal(options.packageSettings?.service, '1on1');
|
||||
};
|
||||
|
||||
allPromises.push(
|
||||
Email.sendAsync({
|
||||
from: 'foo@example.com',
|
||||
to: 'bar@example.com',
|
||||
text: '*Cool*, man',
|
||||
html: '<i>Cool</i>, man',
|
||||
stream,
|
||||
}).then(() => {
|
||||
test.equal(stream.getContentsAsString('utf8'), false);
|
||||
})
|
||||
);
|
||||
});
|
||||
Promise.all(allPromises).then(() => {
|
||||
Email.customTransport = undefined;
|
||||
Meteor.settings.packages = undefined;
|
||||
onComplete();
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
Tinytest.addAsync(
|
||||
'[Async] email - hooks stop the sending',
|
||||
function (test, onComplete) {
|
||||
// Register hooks
|
||||
const hook1 = Email.hookSend((options) => {
|
||||
// Test that we get options through
|
||||
test.equal(options.from, 'foo@example.com');
|
||||
console.log('EXECUTE');
|
||||
return true;
|
||||
});
|
||||
const hook2 = Email.hookSend(() => {
|
||||
console.log('STOP');
|
||||
return false;
|
||||
});
|
||||
const hook3 = Email.hookSend(() => {
|
||||
console.log('FAIL');
|
||||
});
|
||||
smokeEmailTest((stream) => {
|
||||
Email.sendAsync({
|
||||
from: 'foo@example.com',
|
||||
to: 'bar@example.com',
|
||||
text: '*Cool*, man',
|
||||
html: '<i>Cool</i>, man',
|
||||
stream,
|
||||
}).then(() => {
|
||||
test.equal(stream.getContentsAsString('utf8'), false);
|
||||
hook1.stop();
|
||||
hook2.stop();
|
||||
hook3.stop();
|
||||
onComplete();
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// Another tests
|
||||
|
||||
Tinytest.add('[Sync] email - URL string for known hosts', function (test) {
|
||||
const oneTransport = EmailTest.knowHostsTransport({
|
||||
service: '1und1',
|
||||
user: 'test',
|
||||
password: 'pwd',
|
||||
});
|
||||
test.equal(oneTransport.transporter.auth.type, 'LOGIN');
|
||||
test.equal(oneTransport.transporter.auth.user, 'test');
|
||||
|
||||
const aolUrlTransport = EmailTest.knowHostsTransport(
|
||||
null,
|
||||
'AOL://test:pwd@aol.com'
|
||||
);
|
||||
test.equal(aolUrlTransport.transporter.auth.user, 'test');
|
||||
test.equal(aolUrlTransport.transporter.auth.type, 'LOGIN');
|
||||
|
||||
const outlookTransport = EmailTest.knowHostsTransport(
|
||||
null,
|
||||
'Outlook365://firstname.lastname%40hotmail.com:password@hotmail.com'
|
||||
);
|
||||
const outlookTransport2 = EmailTest.knowHostsTransport(
|
||||
undefined,
|
||||
'Outlook365://firstname.lastname@hotmail.com:password@hotmail.com'
|
||||
);
|
||||
test.equal(
|
||||
outlookTransport.transporter.auth.user,
|
||||
'firstname.lastname%40hotmail.com'
|
||||
);
|
||||
test.equal(
|
||||
outlookTransport.options.auth.user,
|
||||
'firstname.lastname%40hotmail.com'
|
||||
);
|
||||
test.equal(outlookTransport.transporter.options.service, 'outlook365');
|
||||
test.equal(
|
||||
outlookTransport2.transporter.auth.user,
|
||||
'firstname.lastname%40hotmail.com'
|
||||
);
|
||||
test.equal(outlookTransport2.transporter.options.service, 'outlook365');
|
||||
|
||||
const hotmailTransport = EmailTest.knowHostsTransport(
|
||||
undefined,
|
||||
'Hotmail://firstname.lastname@hotmail.com:password@hotmail.com'
|
||||
);
|
||||
console.dir(hotmailTransport);
|
||||
test.equal(hotmailTransport.transporter.options.service, 'hotmail');
|
||||
|
||||
const falseService = CUSTOM_TRANSPORT_SETTINGS.email;
|
||||
const errorMsg =
|
||||
'Could not recognize e-mail service. See list at https://nodemailer.com/smtp/well-known/ for services that we can configure for you.';
|
||||
test.throws(() => EmailTest.knowHostsTransport(falseService), errorMsg);
|
||||
test.throws(
|
||||
() => EmailTest.knowHostsTransport(null, 'smtp://bbb:bb@bb.com'),
|
||||
errorMsg
|
||||
);
|
||||
});
|
||||
|
||||
Tinytest.addAsync(
|
||||
'[Async] email - with custom transport exception',
|
||||
async function (test) {
|
||||
Meteor.settings.packages = CUSTOM_TRANSPORT_SETTINGS;
|
||||
Email.customTransport = (options) => {
|
||||
test.equal(options.from, 'foo@example.com');
|
||||
test.equal(options.packageSettings?.service, '1on1');
|
||||
throw new Meteor.Error('Expected error');
|
||||
};
|
||||
await Email.sendAsync({
|
||||
from: 'foo@example.com',
|
||||
to: 'bar@example.com',
|
||||
}).catch((err) => {
|
||||
test.equal(err.error, 'Expected error');
|
||||
});
|
||||
Meteor.settings.packages = undefined;
|
||||
Email.customTransport = undefined;
|
||||
}
|
||||
);
|
||||
|
||||
Tinytest.addAsync(
|
||||
'[Async] email - with custom transport long time running',
|
||||
async function (test) {
|
||||
Meteor.settings.packages = CUSTOM_TRANSPORT_SETTINGS;
|
||||
Email.customTransport = async (options) => {
|
||||
await sleep(3000);
|
||||
test.equal(options.from, 'foo@example.com');
|
||||
test.equal(options.packageSettings?.service, '1on1');
|
||||
};
|
||||
await Email.sendAsync({
|
||||
from: 'foo@example.com',
|
||||
to: 'bar@example.com',
|
||||
});
|
||||
Meteor.settings.packages = undefined;
|
||||
Email.customTransport = undefined;
|
||||
}
|
||||
);
|
||||
|
||||
Tinytest.addAsync(
|
||||
'[Sync] email - with custom transport long time running',
|
||||
function (test, onComplete) {
|
||||
Meteor.settings.packages = CUSTOM_TRANSPORT_SETTINGS;
|
||||
Email.customTransport = async (options) => {
|
||||
await sleep(3000);
|
||||
test.equal(options.from, 'foo@example.com');
|
||||
test.equal(options.packageSettings?.service, '1on1');
|
||||
Meteor.settings.packages = undefined;
|
||||
Email.customTransport = undefined;
|
||||
onComplete();
|
||||
};
|
||||
Email.send({
|
||||
from: 'foo@example.com',
|
||||
to: 'bar@example.com',
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
254
packages/email/email_tests_data.js
Normal file
254
packages/email/email_tests_data.js
Normal file
@@ -0,0 +1,254 @@
|
||||
import { canonicalize, devWarningBanner } from './email_test_helpers';
|
||||
|
||||
export const TEST_CASES = [
|
||||
{
|
||||
title: 'email - fully customizable',
|
||||
options: {
|
||||
0: {
|
||||
from: 'foo@example.com',
|
||||
to: 'bar@example.com',
|
||||
cc: ['friends@example.com', 'enemies@example.com'],
|
||||
subject: 'This is the subject',
|
||||
text: 'This is the body\nof the message\nFrom us.',
|
||||
headers: {
|
||||
'X-Meteor-Test': 'a custom header',
|
||||
Date: 'dummy',
|
||||
},
|
||||
},
|
||||
},
|
||||
testCalls: {
|
||||
0: (test, stream) => {
|
||||
// XXX brittle if mailcomposer changes header order, etc
|
||||
test.equal(
|
||||
canonicalize(stream.getContentsAsString('utf8')),
|
||||
'====== BEGIN MAIL #0 ======\n' +
|
||||
devWarningBanner +
|
||||
'Content-Type: text/plain; charset=utf-8\r\n' +
|
||||
'X-Meteor-Test: a custom header\r\n' +
|
||||
'Date: dummy\r\n' +
|
||||
'From: foo@example.com\r\n' +
|
||||
'To: bar@example.com\r\n' +
|
||||
'Cc: friends@example.com, enemies@example.com\r\n' +
|
||||
'Subject: This is the subject\r\n' +
|
||||
'Message-ID: <...>\r\n' +
|
||||
'Content-Transfer-Encoding: 7bit\r\n' +
|
||||
'MIME-Version: 1.0\r\n' +
|
||||
'\r\n' +
|
||||
'This is the body\n' +
|
||||
'of the message\n' +
|
||||
'From us.\r\n' +
|
||||
'====== END MAIL #0 ======\n'
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'email - undefined headers sends properly',
|
||||
options: {
|
||||
0: {
|
||||
from: 'foo@example.com',
|
||||
to: 'bar@example.com',
|
||||
subject: 'This is the subject',
|
||||
text: 'This is the body\nof the message\nFrom us.',
|
||||
},
|
||||
},
|
||||
testCalls: {
|
||||
0: (test, stream) => {
|
||||
test.matches(
|
||||
canonicalize(stream.getContentsAsString('utf8')),
|
||||
/^====== BEGIN MAIL #0 ======$[\s\S]+^To: bar@example.com$/m
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'email - multiple e-mails same stream',
|
||||
options: {
|
||||
0: {
|
||||
from: 'foo@example.com',
|
||||
to: 'bar@example.com',
|
||||
subject: 'This is the subject',
|
||||
text: 'This is the body\nof the message\nFrom us.',
|
||||
},
|
||||
1: {
|
||||
from: 'qux@example.com',
|
||||
to: 'baz@example.com',
|
||||
subject: 'This is important',
|
||||
text: 'This is another message\nFrom Qux.',
|
||||
},
|
||||
},
|
||||
|
||||
testCalls: {
|
||||
0: (test, stream) => {
|
||||
const contents = canonicalize(stream.getContentsAsString('utf8'));
|
||||
test.matches(contents, /^====== BEGIN MAIL #0 ======$/m);
|
||||
test.matches(contents, /^From: foo@example.com$/m);
|
||||
test.matches(contents, /^To: bar@example.com$/m);
|
||||
},
|
||||
1: (test, stream) => {
|
||||
const contents2 = canonicalize(stream.getContentsAsString('utf8'));
|
||||
test.matches(contents2, /^====== BEGIN MAIL #1 ======$/m);
|
||||
test.matches(contents2, /^From: qux@example.com$/m);
|
||||
test.matches(contents2, /^To: baz@example.com$/m);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'email - using mail composer',
|
||||
options: {
|
||||
0: {
|
||||
mailComposer: new EmailInternals.NpmModules.mailcomposer.module({
|
||||
from: 'a@b.com',
|
||||
text: 'body',
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
||||
testCalls: {
|
||||
0: (test, stream) => {
|
||||
test.equal(
|
||||
canonicalize(stream.getContentsAsString('utf8')),
|
||||
'====== BEGIN MAIL #0 ======\n' +
|
||||
devWarningBanner +
|
||||
'Content-Type: text/plain; charset=utf-8\r\n' +
|
||||
'From: a@b.com\r\n' +
|
||||
'Message-ID: <...>\r\n' +
|
||||
'Content-Transfer-Encoding: 7bit\r\n' +
|
||||
'Date: ...\r\n' +
|
||||
'MIME-Version: 1.0\r\n' +
|
||||
'\r\n' +
|
||||
'body\r\n' +
|
||||
'====== END MAIL #0 ======\n'
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'email - date auto generated',
|
||||
options: {
|
||||
0: {
|
||||
from: 'foo@example.com',
|
||||
to: 'bar@example.com',
|
||||
subject: 'This is the subject',
|
||||
text: 'This is the body\nof the message\nFrom us.',
|
||||
headers: {
|
||||
'X-Meteor-Test': 'a custom header',
|
||||
},
|
||||
},
|
||||
},
|
||||
testCalls: {
|
||||
0: (test, stream) => {
|
||||
test.matches(
|
||||
canonicalize(stream.getContentsAsString('utf8')),
|
||||
/^Date: .+$/m
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'email - long lines',
|
||||
options: {
|
||||
0: {
|
||||
from: 'foo@example.com',
|
||||
to: 'bar@example.com',
|
||||
subject:
|
||||
'This is a very very very very very very very very very very very very long subject',
|
||||
text: 'This is a very very very very very very very very very very very very long text',
|
||||
},
|
||||
},
|
||||
testCalls: {
|
||||
0: (test, stream) => {
|
||||
test.equal(
|
||||
canonicalize(stream.getContentsAsString('utf8')),
|
||||
'====== BEGIN MAIL #0 ======\n' +
|
||||
devWarningBanner +
|
||||
'Content-Type: text/plain; charset=utf-8\r\n' +
|
||||
'From: foo@example.com\r\n' +
|
||||
'To: bar@example.com\r\n' +
|
||||
'Subject: This is a very very very very very very very very ' +
|
||||
'very very very\r\n very long subject\r\n' +
|
||||
'Message-ID: <...>\r\n' +
|
||||
'Content-Transfer-Encoding: quoted-printable\r\n' +
|
||||
'Date: ...\r\n' +
|
||||
'MIME-Version: 1.0\r\n' +
|
||||
'\r\n' +
|
||||
'This is a very very very very very very very very very very ' +
|
||||
'very very long =\r\ntext\r\n' +
|
||||
'====== END MAIL #0 ======\n'
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'email - unicode',
|
||||
options: {
|
||||
0: {
|
||||
from: 'foo@example.com',
|
||||
to: 'bar@example.com',
|
||||
subject: '\u263a',
|
||||
text: 'I \u2665 Meteor',
|
||||
},
|
||||
},
|
||||
testCalls: {
|
||||
0: (test, stream) => {
|
||||
test.equal(
|
||||
canonicalize(stream.getContentsAsString('utf8')),
|
||||
'====== BEGIN MAIL #0 ======\n' +
|
||||
devWarningBanner +
|
||||
'Content-Type: text/plain; charset=utf-8\r\n' +
|
||||
'From: foo@example.com\r\n' +
|
||||
'To: bar@example.com\r\n' +
|
||||
'Subject: =?UTF-8?B?4pi6?=\r\n' +
|
||||
'Message-ID: <...>\r\n' +
|
||||
'Content-Transfer-Encoding: quoted-printable\r\n' +
|
||||
'Date: ...\r\n' +
|
||||
'MIME-Version: 1.0\r\n' +
|
||||
'\r\n' +
|
||||
'I =E2=99=A5 Meteor\r\n' +
|
||||
'====== END MAIL #0 ======\n'
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'email - text and html',
|
||||
options: {
|
||||
0: {
|
||||
from: 'foo@example.com',
|
||||
to: 'bar@example.com',
|
||||
text: '*Cool*, man',
|
||||
html: '<i>Cool</i>, man',
|
||||
},
|
||||
},
|
||||
testCalls: {
|
||||
0: (test, stream) => {
|
||||
test.equal(
|
||||
canonicalize(stream.getContentsAsString('utf8')),
|
||||
'====== BEGIN MAIL #0 ======\n' +
|
||||
devWarningBanner +
|
||||
'Content-Type: multipart/alternative;\r\n' +
|
||||
' boundary="--...-Part_1"\r\n' +
|
||||
'From: foo@example.com\r\n' +
|
||||
'To: bar@example.com\r\n' +
|
||||
'Message-ID: <...>\r\n' +
|
||||
'Date: ...\r\n' +
|
||||
'MIME-Version: 1.0\r\n' +
|
||||
'\r\n' +
|
||||
'----...-Part_1\r\n' +
|
||||
'Content-Type: text/plain; charset=utf-8\r\n' +
|
||||
'Content-Transfer-Encoding: 7bit\r\n' +
|
||||
'\r\n' +
|
||||
'*Cool*, man\r\n' +
|
||||
'----...-Part_1\r\n' +
|
||||
'Content-Type: text/html; charset=utf-8\r\n' +
|
||||
'Content-Transfer-Encoding: 7bit\r\n' +
|
||||
'\r\n' +
|
||||
'<i>Cool</i>, man\r\n' +
|
||||
'----...-Part_1--\r\n' +
|
||||
'====== END MAIL #0 ======\n'
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,10 +1,26 @@
|
||||
# Changelog
|
||||
## 1.8.0 - unreleased
|
||||
### Breaking changes
|
||||
- N/A
|
||||
|
||||
## 1.12.0 - UNRELEASED
|
||||
### Changes
|
||||
- Updated default version of Facebook GraphAPI to v15
|
||||
|
||||
## 1.11.0 - 2022-03-24
|
||||
### Changes
|
||||
- Updated default version of Facebook GraphAPI to v12
|
||||
|
||||
## 1.10.0 - 2021-09-14
|
||||
### Changes
|
||||
- Added login handler hook, like in the Google package for easier management in React Native and similar apps. [PR](https://github.com/meteor/meteor/pull/11603)
|
||||
|
||||
## 1.9.1 - 2021-08-12
|
||||
### Changes
|
||||
- Allow usage of `http` package both v1 and v2 for backward compatibility
|
||||
|
||||
## 1.9.0 - 2021-06-24
|
||||
### Changes
|
||||
- Upgrade default Facebook API to v10 [#11362](https://github.com/meteor/meteor/pull/11362)
|
||||
|
||||
## 1.8.0 - 2021-04-15
|
||||
### Changes
|
||||
- Updated to use Facebook GraphAPI v10
|
||||
- You can now override the default API version by setting `Meteor.settings.public.packages.facebook-oauth.apiVersion` to for example `8.0`
|
||||
|
||||
## 1.7.3 - 2020-10-05
|
||||
|
||||
@@ -30,7 +30,7 @@ Facebook.requestCredential = (options, credentialRequestCompleteCallback) => {
|
||||
|
||||
const loginStyle = OAuth._loginStyle('facebook', config, options);
|
||||
|
||||
const API_VERSION = Meteor.settings?.public?.packages?.['facebook-oauth']?.apiVersion || '13.0';
|
||||
const API_VERSION = Meteor.settings?.public?.packages?.['facebook-oauth']?.apiVersion || '15.0';
|
||||
|
||||
let loginUrl =
|
||||
`https://www.facebook.com/v${API_VERSION}/dialog/oauth?client_id=${config.appId}` +
|
||||
|
||||
@@ -4,13 +4,13 @@ import { Accounts } from 'meteor/accounts-base';
|
||||
|
||||
const API_VERSION = Meteor.settings?.public?.packages?.['facebook-oauth']?.apiVersion || '13.0';
|
||||
|
||||
Facebook.handleAuthFromAccessToken = (accessToken, expiresAt) => {
|
||||
Facebook.handleAuthFromAccessToken = async (accessToken, expiresAt) => {
|
||||
// include basic fields from facebook
|
||||
// https://developers.facebook.com/docs/facebook-login/permissions/
|
||||
const whitelisted = ['id', 'email', 'name', 'first_name', 'last_name',
|
||||
'middle_name', 'name_format', 'picture', 'short_name'];
|
||||
|
||||
const identity = getIdentity(accessToken, whitelisted);
|
||||
const identity = await getIdentity(accessToken, whitelisted);
|
||||
|
||||
const fields = {};
|
||||
whitelisted.forEach(field => fields[field] = identity[field]);
|
||||
@@ -34,8 +34,8 @@ Accounts.registerLoginHandler(request => {
|
||||
return Accounts.updateOrCreateUserFromExternalService('facebook', facebookData.serviceData, facebookData.options);
|
||||
});
|
||||
|
||||
OAuth.registerService('facebook', 2, null, query => {
|
||||
const response = getTokenResponse(query);
|
||||
OAuth.registerService('facebook', 2, null, async query => {
|
||||
const response = await getTokenResponse(query);
|
||||
const { accessToken } = response;
|
||||
const { expiresIn } = response;
|
||||
|
||||
@@ -52,7 +52,7 @@ function getAbsoluteUrlOptions(query) {
|
||||
const redirectUrl = new URL(state.redirectUrl);
|
||||
return {
|
||||
rootUrl: redirectUrl.origin,
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`Failed to complete OAuth handshake with Facebook because it was not able to obtain the redirect url from the state and you are using overrideRootUrlFromStateRedirectUrl.`, e
|
||||
@@ -61,73 +61,86 @@ function getAbsoluteUrlOptions(query) {
|
||||
}
|
||||
}
|
||||
|
||||
// returns an object containing:
|
||||
// - accessToken
|
||||
// - expiresIn: lifetime of token in seconds
|
||||
const getTokenResponse = query => {
|
||||
const config = ServiceConfiguration.configurations.findOne({service: 'facebook'});
|
||||
if (!config)
|
||||
throw new ServiceConfiguration.ConfigError();
|
||||
/**
|
||||
* @typedef {Object} UserAccessToken
|
||||
* @property {string} accessToken - User access Token
|
||||
* @property {number} expiresIn - lifetime of token in seconds
|
||||
*/
|
||||
/**
|
||||
* @async
|
||||
* @function getTokenResponse
|
||||
* @param {Object} query - An object with the code.
|
||||
* @returns {Promise<UserAccessToken>} - Promise with an Object containing the accessToken and expiresIn (lifetime of token in seconds)
|
||||
*/
|
||||
const getTokenResponse = async (query) => {
|
||||
const config = ServiceConfiguration.configurations.findOne({
|
||||
service: 'facebook',
|
||||
});
|
||||
if (!config) throw new ServiceConfiguration.ConfigError();
|
||||
|
||||
let responseContent;
|
||||
try {
|
||||
const absoluteUrlOptions = getAbsoluteUrlOptions(query);
|
||||
const redirectUri = OAuth._redirectUri('facebook', config, undefined, absoluteUrlOptions);
|
||||
|
||||
const absoluteUrlOptions = getAbsoluteUrlOptions(query);
|
||||
const redirectUri = OAuth._redirectUri('facebook', config, undefined, absoluteUrlOptions);
|
||||
// Request an access token
|
||||
responseContent = HTTP.get(
|
||||
`https://graph.facebook.com/v${API_VERSION}/oauth/access_token`, {
|
||||
params: {
|
||||
client_id: config.appId,
|
||||
redirect_uri: redirectUri,
|
||||
client_secret: OAuth.openSecret(config.secret),
|
||||
code: query.code
|
||||
}
|
||||
}).data;
|
||||
} catch (err) {
|
||||
throw Object.assign(
|
||||
new Error(`Failed to complete OAuth handshake with Facebook. ${err.message}`),
|
||||
{ response: err.response },
|
||||
);
|
||||
}
|
||||
|
||||
const fbAccessToken = responseContent.access_token;
|
||||
const fbExpires = responseContent.expires_in;
|
||||
|
||||
if (!fbAccessToken) {
|
||||
throw new Error("Failed to complete OAuth handshake with facebook " +
|
||||
`-- can't find access token in HTTP response. ${responseContent}`);
|
||||
}
|
||||
return {
|
||||
accessToken: fbAccessToken,
|
||||
expiresIn: fbExpires
|
||||
};
|
||||
return OAuth._fetch(
|
||||
`https://graph.facebook.com/v${API_VERSION}/oauth/access_token`,
|
||||
'GET',
|
||||
{
|
||||
queryParams: {
|
||||
client_id: config.appId,
|
||||
redirect_uri: redirectUri,
|
||||
client_secret: OAuth.openSecret(config.secret),
|
||||
code: query.code,
|
||||
},
|
||||
}
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then(data => {
|
||||
const fbAccessToken = data.access_token;
|
||||
const fbExpires = data.expires_in;
|
||||
if (!fbAccessToken) {
|
||||
throw new Error("Failed to complete OAuth handshake with facebook " +
|
||||
`-- can't find access token in HTTP response. ${data}`);
|
||||
}
|
||||
return {
|
||||
accessToken: fbAccessToken,
|
||||
expiresIn: fbExpires
|
||||
};
|
||||
})
|
||||
.catch((err) => {
|
||||
throw Object.assign(
|
||||
new Error(
|
||||
`Failed to complete OAuth handshake with Facebook. ${err.message}`
|
||||
),
|
||||
{ response: err.response }
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const getIdentity = (accessToken, fields) => {
|
||||
const config = ServiceConfiguration.configurations.findOne({service: 'facebook'});
|
||||
if (!config)
|
||||
throw new ServiceConfiguration.ConfigError();
|
||||
const getIdentity = async (accessToken, fields) => {
|
||||
const config = ServiceConfiguration.configurations.findOne({
|
||||
service: 'facebook',
|
||||
});
|
||||
if (!config) throw new ServiceConfiguration.ConfigError();
|
||||
|
||||
// Generate app secret proof that is a sha256 hash of the app access token, with the app secret as the key
|
||||
// https://developers.facebook.com/docs/graph-api/securing-requests#appsecret_proof
|
||||
const hmac = crypto.createHmac('sha256', OAuth.openSecret(config.secret));
|
||||
hmac.update(accessToken);
|
||||
|
||||
try {
|
||||
return HTTP.get(`https://graph.facebook.com/v${API_VERSION}/me`, {
|
||||
params: {
|
||||
access_token: accessToken,
|
||||
appsecret_proof: hmac.digest('hex'),
|
||||
fields: fields.join(",")
|
||||
}
|
||||
}).data;
|
||||
} catch (err) {
|
||||
throw Object.assign(
|
||||
new Error(`Failed to fetch identity from Facebook. ${err.message}`),
|
||||
{ response: err.response },
|
||||
);
|
||||
}
|
||||
return OAuth._fetch(`https://graph.facebook.com/v${API_VERSION}/me`, 'GET', {
|
||||
queryParams: {
|
||||
access_token: accessToken,
|
||||
appsecret_proof: hmac.digest('hex'),
|
||||
fields: fields.join(','),
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.catch((err) => {
|
||||
throw Object.assign(
|
||||
new Error(`Failed to fetch identity from Facebook. ${err.message}`),
|
||||
{ response: err.response }
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
Facebook.retrieveCredential = (credentialToken, credentialSecret) =>
|
||||
|
||||
@@ -7,7 +7,6 @@ Package.onUse(api => {
|
||||
api.use('ecmascript', ['client', 'server']);
|
||||
api.use('oauth2', ['client', 'server']);
|
||||
api.use('oauth', ['client', 'server']);
|
||||
api.use('http@1.4.4 || 2.0.0', ['server']);
|
||||
api.use('random', 'client');
|
||||
api.use('service-configuration', ['client', 'server']);
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ Template.serverFacts.helpers({
|
||||
factsByPackage: () => Facts.server.find(),
|
||||
facts: function () {
|
||||
const factArray = [];
|
||||
_.each(this, function (value, name) {
|
||||
Object.entries(this).forEach(function ([name, value]) {
|
||||
if (name !== '_id')
|
||||
factArray.push({name: name, value: value});
|
||||
});
|
||||
|
||||
@@ -8,8 +8,7 @@ Package.onUse(function (api) {
|
||||
'ecmascript',
|
||||
'facts-base',
|
||||
'mongo',
|
||||
'templating@1.2.13',
|
||||
'underscore',
|
||||
'templating@1.2.13'
|
||||
], 'client');
|
||||
|
||||
api.imply('facts-base');
|
||||
|
||||
4
packages/fetch/fetch.d.ts
vendored
Normal file
4
packages/fetch/fetch.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
export declare function fetch(): typeof globalThis.fetch;
|
||||
export declare var Headers: typeof globalThis.Headers;
|
||||
export declare var Request: typeof globalThis.Request;
|
||||
export declare var Response: typeof globalThis.Response;
|
||||
3
packages/fetch/package-types.json
Normal file
3
packages/fetch/package-types.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"typesEntry": "fetch.d.ts"
|
||||
}
|
||||
@@ -19,6 +19,7 @@ Package.onUse(function(api) {
|
||||
api.mainModule("legacy.js", "legacy");
|
||||
api.mainModule("server.js", "server");
|
||||
|
||||
api.addAssets("fetch.d.ts", "server");
|
||||
// The other exports (Headers, Request, Response) can be imported
|
||||
// explicitly from the "meteor/fetch" package.
|
||||
api.export("fetch");
|
||||
|
||||
@@ -85,8 +85,8 @@ Tinytest.add("geojson-utils - points distance generated tests", function (test)
|
||||
6846704.0253010122, 1368055.9401701286, 14041503.0409814864,
|
||||
18560499.7346975356, 3793112.6186894816];
|
||||
|
||||
_.each(tests, function (pair, testN) {
|
||||
var distance = GeoJSON.pointDistance.apply(this, _.map(pair, toGeoJSONPoint));
|
||||
tests.forEach(function (pair, testN) {
|
||||
var distance = GeoJSON.pointDistance.apply(this, pair.map(toGeoJSONPoint));
|
||||
test.isTrue(Math.abs(distance - answers[testN]) < 0.000001,
|
||||
"Wrong distance between points " + JSON.stringify(pair) + ": " + distance + ", " + Math.abs(distance - answers[testN]) + " differenc");
|
||||
});
|
||||
|
||||
@@ -11,7 +11,6 @@ Package.onUse(function (api) {
|
||||
|
||||
Package.onTest(function (api) {
|
||||
api.use('tinytest');
|
||||
api.use('underscore');
|
||||
api.use('geojson-utils');
|
||||
api.addFiles(['geojson-utils.tests.js'], 'client');
|
||||
});
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
Github = {};
|
||||
|
||||
OAuth.registerService('github', 2, null, (query) => {
|
||||
const accessTokenCall = Meteor.wrapAsync(getAccessToken);
|
||||
const accessToken = accessTokenCall(query);
|
||||
const identityCall = Meteor.wrapAsync(getIdentity);
|
||||
const identity = identityCall(accessToken);
|
||||
const emailsCall = Meteor.wrapAsync(getEmails);
|
||||
const emails = emailsCall(accessToken);
|
||||
OAuth.registerService('github', 2, null, async (query) => {
|
||||
const accessToken = await getAccessToken(query);
|
||||
const identity = await getIdentity(accessToken);
|
||||
const emails = await getEmails(accessToken);
|
||||
const primaryEmail = emails.find((email) => email.primary);
|
||||
|
||||
return {
|
||||
@@ -31,7 +28,7 @@ OAuth.registerService('github', 2, null, (query) => {
|
||||
let userAgent = 'Meteor';
|
||||
if (Meteor.release) userAgent += `/${Meteor.release}`;
|
||||
|
||||
const getAccessToken = async (query, callback) => {
|
||||
const getAccessToken = async (query) => {
|
||||
const config = ServiceConfiguration.configurations.findOne({
|
||||
service: 'github'
|
||||
});
|
||||
@@ -68,18 +65,16 @@ const getAccessToken = async (query, callback) => {
|
||||
);
|
||||
}
|
||||
if (response.error) {
|
||||
callback(response.error);
|
||||
// if the http response was a json object with an error attribute
|
||||
throw new Error(
|
||||
`Failed to complete OAuth handshake with GitHub. ${response.error}`
|
||||
);
|
||||
} else {
|
||||
callback(null, response.access_token);
|
||||
return response.access_token;
|
||||
}
|
||||
};
|
||||
|
||||
const getIdentity = async (accessToken, callback) => {
|
||||
const getIdentity = async (accessToken) => {
|
||||
try {
|
||||
const request = await fetch('https://api.github.com/user', {
|
||||
method: 'GET',
|
||||
@@ -89,11 +84,8 @@ const getIdentity = async (accessToken, callback) => {
|
||||
Authorization: `token ${accessToken}`
|
||||
} // http://developer.github.com/v3/#user-agent-required
|
||||
});
|
||||
const response = await request.json();
|
||||
callback(null, response);
|
||||
return response;
|
||||
return await request.json();
|
||||
} catch (err) {
|
||||
callback(err.message);
|
||||
throw Object.assign(
|
||||
new Error(`Failed to fetch identity from Github. ${err.message}`),
|
||||
{ response: err.response }
|
||||
@@ -101,7 +93,7 @@ const getIdentity = async (accessToken, callback) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getEmails = async (accessToken, callback) => {
|
||||
const getEmails = async (accessToken) => {
|
||||
try {
|
||||
const request = await fetch('https://api.github.com/user/emails', {
|
||||
method: 'GET',
|
||||
@@ -111,11 +103,8 @@ const getEmails = async (accessToken, callback) => {
|
||||
Authorization: `token ${accessToken}`
|
||||
} // http://developer.github.com/v3/#user-agent-required
|
||||
});
|
||||
const response = await request.json();
|
||||
callback(null, response);
|
||||
return response;
|
||||
return await request.json();
|
||||
} catch (err) {
|
||||
callback(err.message, []);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,40 +5,46 @@ import { fetch } from 'meteor/fetch';
|
||||
const hasOwn = Object.prototype.hasOwnProperty;
|
||||
|
||||
// https://developers.google.com/accounts/docs/OAuth2Login#userinfocall
|
||||
Google.whitelistedFields = ['id', 'email', 'verified_email', 'name', 'given_name',
|
||||
'family_name', 'picture', 'locale', 'timezone', 'gender'];
|
||||
Google.whitelistedFields = [
|
||||
'id',
|
||||
'email',
|
||||
'verified_email',
|
||||
'name',
|
||||
'given_name',
|
||||
'family_name',
|
||||
'picture',
|
||||
'locale',
|
||||
'timezone',
|
||||
'gender',
|
||||
];
|
||||
|
||||
const getServiceDataFromTokens = tokens => {
|
||||
const getServiceDataFromTokens = async (tokens, callback) => {
|
||||
const { accessToken, idToken } = tokens;
|
||||
const scopesCall = Meteor.wrapAsync(getScopes);
|
||||
let scopes;
|
||||
try {
|
||||
scopes = scopesCall(accessToken);
|
||||
} catch (err) {
|
||||
throw Object.assign(
|
||||
const scopes = await getScopes(accessToken).catch((err) => {
|
||||
const error = Object.assign(
|
||||
new Error(`Failed to fetch tokeninfo from Google. ${err.message}`),
|
||||
{ response: err.response }
|
||||
);
|
||||
}
|
||||
const identityCall = Meteor.wrapAsync(getIdentity);
|
||||
let identity;
|
||||
try {
|
||||
identity = identityCall(accessToken);
|
||||
} catch (err) {
|
||||
throw Object.assign(
|
||||
callback && callback(error);
|
||||
throw error;
|
||||
});
|
||||
|
||||
let identity = await getIdentity(accessToken).catch((err) => {
|
||||
const error = Object.assign(
|
||||
new Error(`Failed to fetch identity from Google. ${err.message}`),
|
||||
{ response: err.response }
|
||||
);
|
||||
}
|
||||
callback && callback(error);
|
||||
throw error;
|
||||
});
|
||||
const serviceData = {
|
||||
accessToken,
|
||||
idToken,
|
||||
scope: scopes
|
||||
scope: scopes,
|
||||
};
|
||||
|
||||
if (hasOwn.call(tokens, "expiresIn")) {
|
||||
serviceData.expiresAt =
|
||||
Date.now() + 1000 * parseInt(tokens.expiresIn, 10);
|
||||
if (hasOwn.call(tokens, 'expiresIn')) {
|
||||
serviceData.expiresAt = Date.now() + 1000 * parseInt(tokens.expiresIn, 10);
|
||||
}
|
||||
|
||||
const fields = Object.create(null);
|
||||
@@ -56,22 +62,25 @@ const getServiceDataFromTokens = tokens => {
|
||||
if (tokens.refreshToken) {
|
||||
serviceData.refreshToken = tokens.refreshToken;
|
||||
}
|
||||
|
||||
return {
|
||||
const returnValue = {
|
||||
serviceData,
|
||||
options: {
|
||||
profile: {
|
||||
name: identity.name
|
||||
}
|
||||
}
|
||||
name: identity.name,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
callback && callback(undefined, returnValue);
|
||||
|
||||
return returnValue;
|
||||
};
|
||||
|
||||
Accounts.registerLoginHandler(request => {
|
||||
Accounts.registerLoginHandler(async (request) => {
|
||||
if (request.googleSignIn !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log({ request });
|
||||
const tokens = {
|
||||
accessToken: request.accessToken,
|
||||
refreshToken: request.refreshToken,
|
||||
@@ -79,29 +88,38 @@ Accounts.registerLoginHandler(request => {
|
||||
};
|
||||
|
||||
if (request.serverAuthCode) {
|
||||
Object.assign(tokens, getTokens({
|
||||
code: request.serverAuthCode
|
||||
}));
|
||||
Object.assign(
|
||||
tokens,
|
||||
await getTokens({
|
||||
code: request.serverAuthCode,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = getServiceDataFromTokens(tokens);
|
||||
result = await getServiceDataFromTokens(tokens);
|
||||
} catch (err) {
|
||||
throw Object.assign(
|
||||
new Error(`Failed to complete OAuth handshake with Google. ${err.message}`),
|
||||
new Error(
|
||||
`Failed to complete OAuth handshake with Google. ${err.message}`
|
||||
),
|
||||
{ response: err.response }
|
||||
);
|
||||
}
|
||||
|
||||
return Accounts.updateOrCreateUserFromExternalService("google", {
|
||||
id: request.userId,
|
||||
idToken: request.idToken,
|
||||
accessToken: request.accessToken,
|
||||
email: request.email,
|
||||
picture: request.imageUrl,
|
||||
...result.serviceData,
|
||||
}, result.options);
|
||||
console.log({ result });
|
||||
return Accounts.updateOrCreateUserFromExternalService(
|
||||
'google',
|
||||
{
|
||||
id: request.userId,
|
||||
idToken: request.idToken,
|
||||
accessToken: request.accessToken,
|
||||
email: request.email,
|
||||
picture: request.imageUrl,
|
||||
...result.serviceData,
|
||||
},
|
||||
result.options
|
||||
);
|
||||
});
|
||||
|
||||
// returns an object containing:
|
||||
@@ -109,45 +127,48 @@ Accounts.registerLoginHandler(request => {
|
||||
// - expiresIn: lifetime of token in seconds
|
||||
// - refreshToken, if this is the first authorization request
|
||||
const getTokens = async (query, callback) => {
|
||||
const config = ServiceConfiguration.configurations.findOne({service: 'google'});
|
||||
if (!config)
|
||||
throw new ServiceConfiguration.ConfigError();
|
||||
const config = ServiceConfiguration.configurations.findOne({
|
||||
service: 'google',
|
||||
});
|
||||
if (!config) throw new ServiceConfiguration.ConfigError();
|
||||
|
||||
const content = new URLSearchParams({
|
||||
code: query.code,
|
||||
client_id: config.clientId,
|
||||
client_secret: OAuth.openSecret(config.secret),
|
||||
redirect_uri: OAuth._redirectUri('google', config),
|
||||
grant_type: 'authorization_code'
|
||||
grant_type: 'authorization_code',
|
||||
});
|
||||
const request = await fetch('https://accounts.google.com/o/oauth2/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: content,
|
||||
});
|
||||
const request = await fetch(
|
||||
"https://accounts.google.com/o/oauth2/token", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: content,
|
||||
});
|
||||
const response = await request.json();
|
||||
|
||||
if (response.error) { // if the http response was a json object with an error attribute
|
||||
callback(response.error);
|
||||
throw new Meteor.Error(`Failed to complete OAuth handshake with Google. ${response.error}`);
|
||||
if (response.error) {
|
||||
// if the http response was a json object with an error attribute
|
||||
callback && callback(response.error);
|
||||
throw new Meteor.Error(
|
||||
`Failed to complete OAuth handshake with Google. ${response.error}`
|
||||
);
|
||||
} else {
|
||||
const data = {
|
||||
accessToken: response.access_token,
|
||||
refreshToken: response.refresh_token,
|
||||
expiresIn: response.expires_in,
|
||||
idToken: response.id_token
|
||||
idToken: response.id_token,
|
||||
};
|
||||
callback(undefined, data);
|
||||
callback && callback(undefined, data);
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
const getTokensCall = Meteor.wrapAsync(getTokens);
|
||||
const getServiceData = query => getServiceDataFromTokens(getTokensCall(query));
|
||||
const getServiceData = async (query) =>
|
||||
getServiceDataFromTokens(await getTokens(query));
|
||||
|
||||
OAuth.registerService('google', 2, null, getServiceData);
|
||||
|
||||
@@ -159,14 +180,15 @@ const getIdentity = async (accessToken, callback) => {
|
||||
`https://www.googleapis.com/oauth2/v1/userinfo?${content.toString()}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: { Accept: 'application/json' }
|
||||
});
|
||||
headers: { Accept: 'application/json' },
|
||||
}
|
||||
);
|
||||
response = await request.json();
|
||||
} catch (e) {
|
||||
callback(e);
|
||||
callback && callback(e);
|
||||
throw new Meteor.Error(e.reason);
|
||||
}
|
||||
callback(undefined, response);
|
||||
callback && callback(undefined, response);
|
||||
return response;
|
||||
};
|
||||
|
||||
@@ -178,14 +200,15 @@ const getScopes = async (accessToken, callback) => {
|
||||
`https://www.googleapis.com/oauth2/v1/tokeninfo?${content.toString()}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: { Accept: 'application/json' }
|
||||
});
|
||||
headers: { Accept: 'application/json' },
|
||||
}
|
||||
);
|
||||
response = await request.json();
|
||||
} catch (e) {
|
||||
callback(e);
|
||||
callback && callback(e);
|
||||
throw new Meteor.Error(e.reason);
|
||||
}
|
||||
callback(undefined, response.scope.split(' '));
|
||||
callback && callback(undefined, response.scope.split(' '));
|
||||
return response.scope.split(' ');
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
Meetup = {};
|
||||
|
||||
OAuth.registerService('meetup', 2, null, query => {
|
||||
const response = getAccessToken(query);
|
||||
OAuth.registerService('meetup', 2, null, async query => {
|
||||
const response = await getAccessToken(query);
|
||||
const accessToken = response.access_token;
|
||||
const expiresAt = (+new Date) + (1000 * response.expires_in);
|
||||
const identity = getIdentity(accessToken);
|
||||
const identity = await getIdentity(accessToken);
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
@@ -33,50 +33,63 @@ OAuth.registerService('meetup', 2, null, query => {
|
||||
};
|
||||
});
|
||||
|
||||
const getAccessToken = query => {
|
||||
const getAccessToken = async query => {
|
||||
const config = ServiceConfiguration.configurations.findOne({service: 'meetup'});
|
||||
if (!config)
|
||||
throw new ServiceConfiguration.ConfigError();
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = HTTP.post(
|
||||
"https://secure.meetup.com/oauth2/access", {headers: {Accept: 'application/json'}, params: {
|
||||
code: query.code,
|
||||
client_id: config.clientId,
|
||||
client_secret: OAuth.openSecret(config.secret),
|
||||
grant_type: 'authorization_code',
|
||||
redirect_uri: OAuth._redirectUri('meetup', config),
|
||||
state: query.state
|
||||
}});
|
||||
} catch (err) {
|
||||
throw Object.assign(
|
||||
new Error(`Failed to complete OAuth handshake with Meetup. ${err.message}`),
|
||||
{ response: err.response }
|
||||
);
|
||||
}
|
||||
const body = OAuth._addValuesToQueryParams({
|
||||
code: query.code,
|
||||
client_id: config.clientId,
|
||||
client_secret: OAuth.openSecret(config.secret),
|
||||
grant_type: 'authorization_code',
|
||||
redirect_uri: OAuth._redirectUri('meetup', config),
|
||||
state: query.state
|
||||
});
|
||||
|
||||
if (response.data.error) { // if the http response was a json object with an error attribute
|
||||
throw new Error(`Failed to complete OAuth handshake with Meetup. ${response.data.error}`);
|
||||
} else {
|
||||
return response.data;
|
||||
}
|
||||
return OAuth._fetch('https://secure.meetup.com/oauth2/access', 'POST', {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body,
|
||||
})
|
||||
.then(data => data.json())
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
throw new Error(`Failed to complete OAuth handshake with Meetup. ${data.error.message}`);
|
||||
}
|
||||
return data;
|
||||
})
|
||||
.catch(err => {
|
||||
throw Object.assign(
|
||||
new Error(`Failed to complete OAuth handshake with Meetup. ${err.message}`),
|
||||
{ response: err.response },
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const getIdentity = accessToken => {
|
||||
try {
|
||||
const response = HTTP.get(
|
||||
"https://api.meetup.com/2/members",
|
||||
{params: {member_id: 'self', access_token: accessToken}});
|
||||
return response.data.results && response.data.results[0];
|
||||
} catch (err) {
|
||||
const getIdentity = async accessToken => {
|
||||
const body = OAuth._addValuesToQueryParams({
|
||||
member_id: 'self',
|
||||
access_token: accessToken
|
||||
});
|
||||
|
||||
return OAuth._fetch('https://api.meetup.com/2/members', 'POST', {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body,
|
||||
}).then(data => data.json())
|
||||
.then(({results = []}) => results.length && results[0])
|
||||
.catch(err => {
|
||||
throw Object.assign(
|
||||
new Error(`Failed to fetch identity from Meetup. ${err.message}`),
|
||||
{ response: err.response }
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Meetup.retrieveCredential = (credentialToken, credentialSecret) =>
|
||||
OAuth.retrieveCredential(credentialToken, credentialSecret);
|
||||
|
||||
@@ -7,7 +7,6 @@ Package.onUse(api => {
|
||||
api.use('ecmascript');
|
||||
api.use('oauth2', ['client', 'server']);
|
||||
api.use('oauth', ['client', 'server']);
|
||||
api.use('http@1.4.4 || 2.0.0', 'server');
|
||||
api.use('random', 'client');
|
||||
api.use('service-configuration', ['client', 'server']);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
OAuth.registerService("meteor-developer", 2, null, query => {
|
||||
const response = getTokens(query);
|
||||
OAuth.registerService("meteor-developer", 2, null, async query => {
|
||||
const response = await getTokens(query);
|
||||
const { accessToken } = response;
|
||||
const identity = getIdentity(accessToken);
|
||||
const identity = await getIdentity(accessToken);
|
||||
|
||||
const serviceData = {
|
||||
accessToken: OAuth.sealSecret(accessToken),
|
||||
@@ -28,69 +28,77 @@ OAuth.registerService("meteor-developer", 2, null, query => {
|
||||
// - expiresIn: lifetime of token in seconds
|
||||
// - refreshToken, if this is the first authorization request and we got a
|
||||
// refresh token from the server
|
||||
const getTokens = query => {
|
||||
const getTokens = async (query) => {
|
||||
const config = ServiceConfiguration.configurations.findOne({
|
||||
service: 'meteor-developer'
|
||||
service: 'meteor-developer',
|
||||
});
|
||||
if (!config)
|
||||
if (!config) {
|
||||
throw new ServiceConfiguration.ConfigError();
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = HTTP.post(
|
||||
MeteorDeveloperAccounts._server + "/oauth2/token", {
|
||||
params: {
|
||||
grant_type: "authorization_code",
|
||||
code: query.code,
|
||||
client_id: config.clientId,
|
||||
client_secret: OAuth.openSecret(config.secret),
|
||||
redirect_uri: OAuth._redirectUri('meteor-developer', config)
|
||||
}
|
||||
const body = OAuth._addValuesToQueryParams({
|
||||
grant_type: 'authorization_code',
|
||||
code: query.code,
|
||||
client_id: config.clientId,
|
||||
client_secret: OAuth.openSecret(config.secret),
|
||||
redirect_uri: OAuth._redirectUri('meteor-developer', config),
|
||||
}).toString();
|
||||
|
||||
return OAuth._fetch(
|
||||
MeteorDeveloperAccounts._server + '/oauth2/token',
|
||||
'POST',
|
||||
{
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body,
|
||||
}
|
||||
)
|
||||
.then((data) => data.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
throw new Error(
|
||||
'Failed to complete OAuth handshake with Meteor developer accounts. ' +
|
||||
(data ? data.error : 'No response data')
|
||||
);
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
throw Object.assign(
|
||||
new Error(
|
||||
"Failed to complete OAuth handshake with Meteor developer accounts. "
|
||||
+ err.message
|
||||
),
|
||||
{response: err.response}
|
||||
);
|
||||
}
|
||||
|
||||
if (! response.data || response.data.error) {
|
||||
// if the http response was a json object with an error attribute
|
||||
throw new Error(
|
||||
"Failed to complete OAuth handshake with Meteor developer accounts. " +
|
||||
(response.data ? response.data.error :
|
||||
"No response data")
|
||||
);
|
||||
} else {
|
||||
return {
|
||||
accessToken: response.data.access_token,
|
||||
refreshToken: response.data.refresh_token,
|
||||
expiresIn: response.data.expires_in
|
||||
};
|
||||
}
|
||||
return {
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token,
|
||||
expiresIn: data.expires_in,
|
||||
};
|
||||
})
|
||||
.catch((err) => {
|
||||
throw Object.assign(
|
||||
new Error(
|
||||
`Failed to complete OAuth handshake with Meteor developer accounts. ${err.message}`
|
||||
),
|
||||
{ response: err.response }
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const getIdentity = accessToken => {
|
||||
try {
|
||||
return HTTP.get(
|
||||
`${MeteorDeveloperAccounts._server}/api/v1/identity`,
|
||||
{
|
||||
headers: { Authorization: `Bearer ${accessToken}`}
|
||||
}
|
||||
).data;
|
||||
} catch (err) {
|
||||
throw Object.assign(
|
||||
new Error("Failed to fetch identity from Meteor developer accounts. " +
|
||||
err.message),
|
||||
{response: err.response}
|
||||
);
|
||||
}
|
||||
const getIdentity = async (accessToken) => {
|
||||
return OAuth._fetch(
|
||||
`${MeteorDeveloperAccounts._server}/api/v1/identity`,
|
||||
'GET',
|
||||
{
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
}
|
||||
)
|
||||
.then((data) => data.json())
|
||||
.catch((err) => {
|
||||
throw Object.assign(
|
||||
new Error(
|
||||
'Failed to fetch identity from Meteor developer accounts. ' +
|
||||
err.message
|
||||
),
|
||||
{ response: err.response }
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
MeteorDeveloperAccounts.retrieveCredential =
|
||||
(credentialToken, credentialSecret) =>
|
||||
MeteorDeveloperAccounts.retrieveCredential =
|
||||
(credentialToken, credentialSecret) =>
|
||||
OAuth.retrieveCredential(credentialToken, credentialSecret);
|
||||
|
||||
@@ -6,7 +6,6 @@ Package.describe({
|
||||
Package.onUse(api => {
|
||||
api.use('oauth2', ['client', 'server']);
|
||||
api.use('oauth', ['client', 'server']);
|
||||
api.use('http@1.4.4 || 2.0.0', ['server']);
|
||||
api.use(['ecmascript', 'service-configuration'], ['client', 'server']);
|
||||
api.use('random', 'client');
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Package.describe({
|
||||
summary: "Core Meteor environment",
|
||||
version: '1.10.1'
|
||||
version: '1.10.1-beta280.2'
|
||||
});
|
||||
|
||||
Package.registerBuildPlugin({
|
||||
|
||||
51
packages/minifier-css/minifier-async-tests.js
Normal file
51
packages/minifier-css/minifier-async-tests.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import { CssTools } from './minifier';
|
||||
const TEST_CASES = [
|
||||
['a \t\n{ color: red } \n', 'a{color:red}', 'whitespace check'],
|
||||
[
|
||||
'a \t\n{ color: red; margin: 1; } \n',
|
||||
'a{color:red;margin:1}',
|
||||
'only last one loses semicolon',
|
||||
],
|
||||
[
|
||||
'a \t\n{ color: red;;; margin: 1;;; } \n',
|
||||
'a{color:red;margin:1}',
|
||||
'more semicolons than needed',
|
||||
],
|
||||
['a , p \t\n{ color: red; } \n', 'a,p{color:red}', 'multiple selectors'],
|
||||
['body {}', '', 'removing empty rules'],
|
||||
[
|
||||
'*.my-class { color: #fff; }',
|
||||
'.my-class{color:#fff}',
|
||||
'removing universal selector',
|
||||
],
|
||||
[
|
||||
'p > *.my-class { color: #fff; }',
|
||||
'p>.my-class{color:#fff}',
|
||||
'removing optional whitespace around ">" in selector',
|
||||
],
|
||||
[
|
||||
'p + *.my-class { color: #fff; }',
|
||||
'p+.my-class{color:#fff}',
|
||||
'removing optional whitespace around "+" in selector',
|
||||
],
|
||||
[
|
||||
'a {\n\
|
||||
font:12px \'Helvetica\',"Arial",\'Nautica\';\n\
|
||||
background:url("/some/nice/picture.png");\n}',
|
||||
'a{font:12px Helvetica,Arial,Nautica;background:url(/some/nice/picture.png)}',
|
||||
'removing quotes in font and url (if possible)',
|
||||
],
|
||||
['/* no comments */ a { color: red; }', 'a{color:red}', 'remove comments'],
|
||||
];
|
||||
|
||||
Tinytest.addAsync(
|
||||
'[Async] minifier-css - simple CSS minification',
|
||||
async (test) => {
|
||||
const promises = TEST_CASES.map(([css, expected, desc]) =>
|
||||
CssTools.minifyCssAsync(css).then((minifiedCss) => {
|
||||
test.equal(minifiedCss[0], expected, desc);
|
||||
})
|
||||
);
|
||||
return Promise.all(promises);
|
||||
}
|
||||
);
|
||||
@@ -1,6 +1,5 @@
|
||||
import path from 'path';
|
||||
import url from 'url';
|
||||
import Future from 'fibers/future';
|
||||
import postcss from 'postcss';
|
||||
import cssnano from 'cssnano';
|
||||
|
||||
@@ -65,23 +64,21 @@ const CssTools = {
|
||||
* @return {String[]} Array containing the minified CSS.
|
||||
*/
|
||||
minifyCss(cssText) {
|
||||
const f = new Future;
|
||||
postcss([
|
||||
cssnano({ safe: true }),
|
||||
]).process(cssText, {
|
||||
from: void 0,
|
||||
}).then(result => {
|
||||
f.return(result.css);
|
||||
}).catch(error => {
|
||||
f.throw(error);
|
||||
});
|
||||
const minifiedCss = f.wait();
|
||||
return Promise.await(CssTools.minifyCssAsync(cssText));
|
||||
},
|
||||
|
||||
// Since this function has always returned an array, we'll wrap the
|
||||
// minified css string in an array before returning, even though we're
|
||||
// only ever returning one minified css string in that array (maintaining
|
||||
// backwards compatibility).
|
||||
return [minifiedCss];
|
||||
/**
|
||||
* Minify the passed in CSS string.
|
||||
*
|
||||
* @param {string} cssText CSS string to minify.
|
||||
* @return {Promise<String[]>} Array containing the minified CSS.
|
||||
*/
|
||||
async minifyCssAsync(cssText) {
|
||||
return await postcss([cssnano({ safe: true })])
|
||||
.process(cssText, {
|
||||
from: void 0,
|
||||
})
|
||||
.then((result) => [result.css]);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -187,6 +184,7 @@ if (typeof Profile !== 'undefined') {
|
||||
'parseCss',
|
||||
'stringifyCss',
|
||||
'minifyCss',
|
||||
'minifyCssAsync',
|
||||
'mergeCssAsts',
|
||||
'rewriteCssUrls',
|
||||
].forEach(funcName => {
|
||||
|
||||
@@ -19,6 +19,7 @@ Package.onTest(function (api) {
|
||||
api.use('tinytest');
|
||||
api.addFiles([
|
||||
'minifier-tests.js',
|
||||
'minifier-async-tests.js',
|
||||
'urlrewriting-tests.js'
|
||||
], 'server');
|
||||
});
|
||||
|
||||
@@ -220,8 +220,8 @@ makeInstaller = function (options) {
|
||||
var file = fileResolve(filesByModuleId[this.id], id);
|
||||
if (file) return file.module.id;
|
||||
var error = makeMissingError(id);
|
||||
if (fallback && isFunction(fallback.resolve)) {
|
||||
return fallback.resolve(id, this.id, error);
|
||||
if (fallback && isFunction(fallback)) {
|
||||
return fallback(id, this.id, error);
|
||||
}
|
||||
throw error;
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
let verifyErrors = Package['modules-runtime'].verifyErrors;
|
||||
|
||||
meteorInstall = makeInstaller({
|
||||
// On the client, make package resolution prefer the "browser" field of
|
||||
// package.json over the "module" field over the "main" field.
|
||||
@@ -8,15 +10,7 @@ meteorInstall = makeInstaller({
|
||||
mainFields: ["browser", "main", "module"],
|
||||
|
||||
fallback: function (id, parentId, error) {
|
||||
if (id && id.startsWith('meteor/')) {
|
||||
var packageName = id.split('/', 2)[1];
|
||||
throw new Error(
|
||||
'Cannot find package "' + packageName + '". ' +
|
||||
'Try "meteor add ' + packageName + '".'
|
||||
);
|
||||
}
|
||||
|
||||
throw error;
|
||||
verifyErrors(id, parentId, error);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
let verifyErrors = Package['modules-runtime'].verifyErrors;
|
||||
|
||||
meteorInstall = makeInstaller({
|
||||
// On the client, make package resolution prefer the "browser" field of
|
||||
// package.json over the "module" field over the "main" field.
|
||||
@@ -5,15 +7,7 @@ meteorInstall = makeInstaller({
|
||||
mainFields: ["browser", "module", "main"],
|
||||
|
||||
fallback: function (id, parentId, error) {
|
||||
if (id && id.startsWith('meteor/')) {
|
||||
var packageName = id.split('/', 2)[1];
|
||||
throw new Error(
|
||||
'Cannot find package "' + packageName + '". ' +
|
||||
'Try "meteor add ' + packageName + '".'
|
||||
);
|
||||
}
|
||||
|
||||
throw error;
|
||||
verifyErrors(id, parentId, error);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
12
packages/modules-runtime/errors/cannotFindMeteorPackage.js
Normal file
12
packages/modules-runtime/errors/cannotFindMeteorPackage.js
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* @description Default error message for when a package is not found
|
||||
* @param id{string}
|
||||
* @return {Error}
|
||||
*/
|
||||
cannotFindMeteorPackage = function(id) {
|
||||
var packageName = id.split('/', 2)[1];
|
||||
return new Error(
|
||||
'Cannot find package "' + packageName + '". ' +
|
||||
'Try "meteor add ' + packageName + '".'
|
||||
);
|
||||
};
|
||||
46
packages/modules-runtime/errors/importsErrors.js
Normal file
46
packages/modules-runtime/errors/importsErrors.js
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
*
|
||||
* @param id{string}
|
||||
* @return {{fromServer: (function(): Error), from: (function(location: string): boolean), fromClient: (function(): Error)}}
|
||||
*/
|
||||
imports = function (id) {
|
||||
/**
|
||||
*
|
||||
* @param location{string}
|
||||
* @return {boolean}
|
||||
*/
|
||||
var from =
|
||||
function (location) {
|
||||
if (!id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// XXX: removed last part of path so that it does not trigger false positives
|
||||
var path = String(id).split('/').slice(0, -1);
|
||||
|
||||
return path.some(function (subPath) {
|
||||
return subPath === location;
|
||||
});
|
||||
};
|
||||
|
||||
var fromClientError =
|
||||
function () {
|
||||
return new Error(
|
||||
'Unable to import on the server a module from a client directory: "' + id + '" \n (cross-boundary import) see: https://guide.meteor.com/structure.html#special-directories'
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
var fromServerError =
|
||||
function () {
|
||||
return new Error(
|
||||
'Unable to import on the client a module from a server directory: "' + id + '" \n (cross-boundary import) see: https://guide.meteor.com/structure.html#special-directories'
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
from: from,
|
||||
fromClientError: fromClientError,
|
||||
fromServerError: fromServerError
|
||||
};
|
||||
};
|
||||
@@ -5,17 +5,9 @@ meteorInstall = makeInstaller({
|
||||
|
||||
// The difference between legacy.js and modern.js is that this module
|
||||
// prefers "main" over "module" (see issue #10658).
|
||||
mainFields: ["browser", "main", "module"],
|
||||
mainFields: ['browser', 'main', 'module'],
|
||||
|
||||
fallback: function(id, parentId, error) {
|
||||
if (id && id.startsWith('meteor/')) {
|
||||
var packageName = id.split('/', 2)[1];
|
||||
throw new Error(
|
||||
'Cannot find package "' + packageName + '". ' +
|
||||
'Try "meteor add ' + packageName + '".'
|
||||
);
|
||||
}
|
||||
|
||||
throw error;
|
||||
fallback: function (id, parentId, error) {
|
||||
verifyErrors(id, parentId, error);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,17 +2,9 @@ meteorInstall = makeInstaller({
|
||||
// On the client, make package resolution prefer the "browser" field of
|
||||
// package.json over the "module" field over the "main" field.
|
||||
browser: true,
|
||||
mainFields: ["browser", "module", "main"],
|
||||
mainFields: ['browser', 'module', 'main'],
|
||||
|
||||
fallback: function(id, parentId, error) {
|
||||
if (id && id.startsWith('meteor/')) {
|
||||
var packageName = id.split('/', 2)[1];
|
||||
throw new Error(
|
||||
'Cannot find package "' + packageName + '". ' +
|
||||
'Try "meteor add ' + packageName + '".'
|
||||
);
|
||||
}
|
||||
|
||||
throw error;
|
||||
fallback: function (id, parentId, error) {
|
||||
verifyErrors(id, parentId, error);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,76 @@
|
||||
Tinytest.add('modules', function (test) {
|
||||
test.equal(typeof meteorInstall, "function");
|
||||
test.equal(typeof meteorInstall, 'function');
|
||||
var require = meteorInstall();
|
||||
test.equal(typeof require, "function");
|
||||
test.equal(typeof require, 'function');
|
||||
});
|
||||
|
||||
Tinytest.add('errors - standard', function (test) {
|
||||
var require = meteorInstall();
|
||||
test.throws(() => {
|
||||
require('meteor/foo');
|
||||
}, 'Cannot find package "foo". Try "meteor add foo".');
|
||||
});
|
||||
|
||||
Tinytest.add('errors - node_modules', function (test) {
|
||||
var require = meteorInstall();
|
||||
test.throws(() => {
|
||||
require('./node_modules/foo');
|
||||
}, "Cannot find module './node_modules/foo'");
|
||||
});
|
||||
|
||||
if (Meteor.isServer) {
|
||||
Tinytest.add('server - throwClientError', function (test) {
|
||||
var require = meteorInstall();
|
||||
test.throws(() => {
|
||||
require('./../server/main.js');
|
||||
}, "Cannot find module './../server/main.js'"
|
||||
);
|
||||
});
|
||||
Tinytest.add('server - client and server in path', function (test) {
|
||||
var require = meteorInstall();
|
||||
test.throws(() => {
|
||||
require('/client/graphql/client');
|
||||
},
|
||||
'Unable to import on the server a module from a client directory: "/client/graphql/client" \n' +
|
||||
' (cross-boundary import) see: https://guide.meteor.com/structure.html#special-directories'
|
||||
);
|
||||
});
|
||||
Tinytest.add('server - throwServerError', function (test) {
|
||||
var require = meteorInstall();
|
||||
test.throws(() => {
|
||||
require('./../client/main.js');
|
||||
},
|
||||
'Unable to import on the server a module from a client directory: "./../client/main.js" \n' +
|
||||
' (cross-boundary import) see: https://guide.meteor.com/structure.html#special-directories'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (Meteor.isClient) {
|
||||
Tinytest.add('client - throwClientError', function (test) {
|
||||
var require = meteorInstall();
|
||||
test.throws(() => {
|
||||
require('./../server/main.js');
|
||||
},
|
||||
'Unable to import on the client a module from a server directory: "./../server/main.js" \n' +
|
||||
' (cross-boundary import) see: https://guide.meteor.com/structure.html#special-directories'
|
||||
);
|
||||
});
|
||||
Tinytest.add('client - client and server in path', function (test) {
|
||||
var require = meteorInstall();
|
||||
test.throws(() => {
|
||||
require('/server/graphql/client');
|
||||
},
|
||||
'Unable to import on the client a module from a server directory: "/server/graphql/client" \n' +
|
||||
' (cross-boundary import) see: https://guide.meteor.com/structure.html#special-directories'
|
||||
);
|
||||
});
|
||||
Tinytest.add('client - throwServerError', function (test) {
|
||||
var require = meteorInstall();
|
||||
test.throws(() => {
|
||||
require('./../client/main.js');
|
||||
}, "Cannot find module './../client/main.js'");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -18,12 +18,16 @@ Package.onUse(function(api) {
|
||||
bare: true
|
||||
});
|
||||
|
||||
api.addFiles("modern.js", "modern");
|
||||
api.addFiles("legacy.js", "legacy");
|
||||
api.addFiles("server.js", "server");
|
||||
api.addFiles("profile.js");
|
||||
api.addFiles(['./errors/importsErrors.js',
|
||||
'./errors/cannotFindMeteorPackage.js']);
|
||||
api.addFiles('modern.js', 'modern');
|
||||
api.addFiles('legacy.js', 'legacy');
|
||||
api.addFiles('server.js', 'server');
|
||||
api.addFiles('profile.js');
|
||||
api.addFiles('verifyErrors.js');
|
||||
|
||||
api.export("meteorInstall");
|
||||
api.export('meteorInstall');
|
||||
api.export('verifyErrors');
|
||||
});
|
||||
|
||||
Package.onTest(function(api) {
|
||||
|
||||
@@ -28,7 +28,7 @@ makeInstallerOptions.fallback = function (id, parentId, error) {
|
||||
return Npm.require(id, error);
|
||||
}
|
||||
}
|
||||
|
||||
verifyErrors(id, parentId, error);
|
||||
throw error;
|
||||
};
|
||||
|
||||
|
||||
34
packages/modules-runtime/verifyErrors.js
Normal file
34
packages/modules-runtime/verifyErrors.js
Normal file
@@ -0,0 +1,34 @@
|
||||
|
||||
/**
|
||||
*
|
||||
* @param id{string}
|
||||
* @param parentId{string}
|
||||
* @param err {Error}
|
||||
*/
|
||||
verifyErrors = function (id, parentId,err) {
|
||||
|
||||
if (id && id.startsWith('meteor/')) {
|
||||
throw cannotFindMeteorPackage(id);
|
||||
}
|
||||
|
||||
if(!(id.startsWith('.') || id.startsWith('/'))) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (imports(id).from('node_modules')) {
|
||||
// Problem with node modules
|
||||
throw err;
|
||||
}
|
||||
|
||||
// custom errors
|
||||
if (Meteor.isServer && imports(id).from('client')) {
|
||||
throw imports(id).fromClientError();
|
||||
}
|
||||
if (Meteor.isClient && imports(id).from('server')) {
|
||||
throw imports(id).fromServerError();
|
||||
}
|
||||
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
@@ -1,3 +1,6 @@
|
||||
|
||||
var MongoDB = NpmModuleMongodb;
|
||||
|
||||
Tinytest.add(
|
||||
'collection - call Mongo.Collection without new',
|
||||
function (test) {
|
||||
@@ -203,3 +206,181 @@ Tinytest.add('collection - calling find with an invalid readPreference',
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Tinytest.add('collection - inserting a document with a binary should return a document with a binary',
|
||||
function(test) {
|
||||
if (Meteor.isServer) {
|
||||
const collection = new Mongo.Collection('testBinary1');
|
||||
const _id = Random.id();
|
||||
collection.insert({
|
||||
_id,
|
||||
binary: new MongoDB.Binary(Buffer.from('hello world'), 6)
|
||||
});
|
||||
|
||||
const doc = collection.findOne({ _id });
|
||||
test.ok(
|
||||
doc.binary instanceof MongoDB.Binary
|
||||
);
|
||||
test.equal(
|
||||
doc.binary.buffer,
|
||||
Buffer.from('hello world')
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Tinytest.add('collection - inserting a document with a binary (sub type 0) should return a document with a uint8array',
|
||||
function(test) {
|
||||
if (Meteor.isServer) {
|
||||
const collection = new Mongo.Collection('testBinary8');
|
||||
const _id = Random.id();
|
||||
collection.insert({
|
||||
_id,
|
||||
binary: new MongoDB.Binary(Buffer.from('hello world'), 0)
|
||||
});
|
||||
|
||||
const doc = collection.findOne({ _id });
|
||||
test.ok(
|
||||
doc.binary instanceof Uint8Array
|
||||
);
|
||||
test.equal(
|
||||
doc.binary,
|
||||
new Uint8Array(Buffer.from('hello world'))
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Tinytest.add('collection - updating a document with a binary should return a document with a binary',
|
||||
function(test) {
|
||||
if (Meteor.isServer) {
|
||||
const collection = new Mongo.Collection('testBinary2');
|
||||
const _id = Random.id();
|
||||
collection.insert({
|
||||
_id
|
||||
});
|
||||
|
||||
collection.update({ _id }, { $set: { binary: new MongoDB.Binary(Buffer.from('hello world'), 6) } });
|
||||
|
||||
const doc = collection.findOne({ _id });
|
||||
test.ok(
|
||||
doc.binary instanceof MongoDB.Binary
|
||||
);
|
||||
test.equal(
|
||||
doc.binary.buffer,
|
||||
Buffer.from('hello world')
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Tinytest.add('collection - updating a document with a binary (sub type 0) should return a document with a uint8array',
|
||||
function(test) {
|
||||
if (Meteor.isServer) {
|
||||
const collection = new Mongo.Collection('testBinary7');
|
||||
const _id = Random.id();
|
||||
collection.insert({
|
||||
_id
|
||||
});
|
||||
|
||||
collection.update({ _id }, { $set: { binary: new MongoDB.Binary(Buffer.from('hello world'), 0) } });
|
||||
|
||||
const doc = collection.findOne({ _id });
|
||||
test.ok(
|
||||
doc.binary instanceof Uint8Array
|
||||
);
|
||||
test.equal(
|
||||
doc.binary,
|
||||
new Uint8Array(Buffer.from('hello world'))
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Tinytest.add('collection - inserting a document with a uint8array should return a document with a uint8array',
|
||||
function(test) {
|
||||
if (Meteor.isServer) {
|
||||
const collection = new Mongo.Collection('testBinary3');
|
||||
const _id = Random.id();
|
||||
collection.insert({
|
||||
_id,
|
||||
binary: new Uint8Array(Buffer.from('hello world'))
|
||||
});
|
||||
|
||||
const doc = collection.findOne({ _id });
|
||||
test.ok(
|
||||
doc.binary instanceof Uint8Array
|
||||
);
|
||||
test.equal(
|
||||
doc.binary,
|
||||
new Uint8Array(Buffer.from('hello world'))
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Tinytest.add('collection - updating a document with a uint8array should return a document with a uint8array',
|
||||
function(test) {
|
||||
if (Meteor.isServer) {
|
||||
const collection = new Mongo.Collection('testBinary4');
|
||||
const _id = Random.id();
|
||||
collection.insert({
|
||||
_id
|
||||
});
|
||||
|
||||
collection.update(
|
||||
{ _id },
|
||||
{ $set: { binary: new Uint8Array(Buffer.from('hello world')) } }
|
||||
)
|
||||
|
||||
const doc = collection.findOne({ _id });
|
||||
test.ok(
|
||||
doc.binary instanceof Uint8Array
|
||||
);
|
||||
test.equal(
|
||||
doc.binary,
|
||||
new Uint8Array(Buffer.from('hello world'))
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Tinytest.add('collection - finding with a query with a uint8array field should return the correct document',
|
||||
function(test) {
|
||||
if (Meteor.isServer) {
|
||||
const collection = new Mongo.Collection('testBinary5');
|
||||
const _id = Random.id();
|
||||
collection.insert({
|
||||
_id,
|
||||
binary: new Uint8Array(Buffer.from('hello world'))
|
||||
});
|
||||
|
||||
const doc = collection.findOne({ binary: new Uint8Array(Buffer.from('hello world')) });
|
||||
test.equal(
|
||||
doc._id,
|
||||
_id
|
||||
);
|
||||
collection.remove({});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Tinytest.add('collection - finding with a query with a binary field should return the correct document',
|
||||
function(test) {
|
||||
if (Meteor.isServer) {
|
||||
const collection = new Mongo.Collection('testBinary6');
|
||||
const _id = Random.id();
|
||||
collection.insert({
|
||||
_id,
|
||||
binary: new MongoDB.Binary(Buffer.from('hello world'), 6)
|
||||
});
|
||||
|
||||
const doc = collection.findOne({ binary: new MongoDB.Binary(Buffer.from('hello world'), 6) });
|
||||
test.equal(
|
||||
doc._id,
|
||||
_id
|
||||
);
|
||||
collection.remove({});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -69,6 +69,10 @@ var unmakeMongoLegal = function (name) { return name.substr(5); };
|
||||
|
||||
var replaceMongoAtomWithMeteor = function (document) {
|
||||
if (document instanceof MongoDB.Binary) {
|
||||
// for backwards compatibility
|
||||
if (document.sub_type !== 0) {
|
||||
return document;
|
||||
}
|
||||
var buffer = document.value(true);
|
||||
return new Uint8Array(buffer);
|
||||
}
|
||||
@@ -98,6 +102,9 @@ var replaceMeteorAtomWithMongo = function (document) {
|
||||
// serialize it correctly).
|
||||
return new MongoDB.Binary(Buffer.from(document));
|
||||
}
|
||||
if (document instanceof MongoDB.Binary) {
|
||||
return document;
|
||||
}
|
||||
if (document instanceof Mongo.ObjectID) {
|
||||
return new MongoDB.ObjectID(document.toHexString());
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ Package.onTest(function (api) {
|
||||
api.use('mongo');
|
||||
api.use('check');
|
||||
api.use('ecmascript');
|
||||
api.use('npm-mongo', 'server');
|
||||
api.use(['tinytest', 'underscore', 'test-helpers', 'ejson', 'random',
|
||||
'ddp', 'base64']);
|
||||
// XXX test order dependency: the allow_tests "partial allow" test
|
||||
|
||||
395
packages/npm-mongo/.npm/package/npm-shrinkwrap.json
generated
395
packages/npm-mongo/.npm/package/npm-shrinkwrap.json
generated
@@ -1,15 +1,355 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"dependencies": {
|
||||
"@aws-crypto/ie11-detection": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-2.0.2.tgz",
|
||||
"integrity": "sha512-5XDMQY98gMAf/WRTic5G++jfmS/VLM0rwpiOpaainKi4L0nqWMSB1SzsrEG5rjFZGYN6ZAefO+/Yta2dFM0kMw==",
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@aws-crypto/sha256-browser": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-2.0.0.tgz",
|
||||
"integrity": "sha512-rYXOQ8BFOaqMEHJrLHul/25ckWH6GTJtdLSajhlqGMx0PmSueAuvboCuZCTqEKlxR8CQOwRarxYMZZSYlhRA1A==",
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@aws-crypto/sha256-js": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-2.0.0.tgz",
|
||||
"integrity": "sha512-VZY+mCY4Nmrs5WGfitmNqXzaE873fcIZDu54cbaDaaamsaTOP1DBImV9F4pICc3EHjQXujyE8jig+PFCaew9ig==",
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@aws-crypto/supports-web-crypto": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-2.0.2.tgz",
|
||||
"integrity": "sha512-6mbSsLHwZ99CTOOswvCRP3C+VCWnzBf+1SnbWxzzJ9lR0mA0JnY2JEAhp8rqmTE0GPFy88rrM27ffgp62oErMQ==",
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@aws-crypto/util": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-2.0.2.tgz",
|
||||
"integrity": "sha512-Lgu5v/0e/BcrZ5m/IWqzPUf3UYFTy/PpeED+uc9SWUR1iZQL8XXbGQg10UfllwwBryO3hFF5dizK+78aoXC1eA==",
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@aws-sdk/abort-controller": {
|
||||
"version": "3.190.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/abort-controller/-/abort-controller-3.190.0.tgz",
|
||||
"integrity": "sha512-M6qo2exTzEfHT5RuW7K090OgesUojhb2JyWiV4ulu7ngY4DWBUBMKUqac696sHRUZvGE5CDzSi0606DMboM+kA=="
|
||||
},
|
||||
"@aws-sdk/client-cognito-identity": {
|
||||
"version": "3.192.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.192.0.tgz",
|
||||
"integrity": "sha512-nIRmiv5JY8wWGUadhG7yLx8o8aVETj5CAgO8e8UJIwwqfue/Yv9bHi2mvkUphO1pj0TeBatAtvu79neJQtsR5g=="
|
||||
},
|
||||
"@aws-sdk/client-sso": {
|
||||
"version": "3.190.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.190.0.tgz",
|
||||
"integrity": "sha512-joEKRjJEzgvXnEih/x2UDDCPlvXWCO3MAHmqi44yJ36Ph4YsFS299mOjPdVLuzUtpQ+cST1nRO7hXNFrulW2jQ=="
|
||||
},
|
||||
"@aws-sdk/client-sts": {
|
||||
"version": "3.192.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.192.0.tgz",
|
||||
"integrity": "sha512-iv72dmRxbZ1cN5jGn4KIVzzu11eduS2fXHbNgd7JsFd5hLBV5TvJaugQzUdXNmy2gN4HiRJr+qa9WkD5b39lsA=="
|
||||
},
|
||||
"@aws-sdk/config-resolver": {
|
||||
"version": "3.190.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/config-resolver/-/config-resolver-3.190.0.tgz",
|
||||
"integrity": "sha512-K+VnDtjTgjpf7yHEdDB0qgGbHToF0pIL0pQMSnmk2yc8BoB3LGG/gg1T0Ki+wRlrFnDCJ6L+8zUdawY2qDsbyw=="
|
||||
},
|
||||
"@aws-sdk/credential-provider-cognito-identity": {
|
||||
"version": "3.192.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.192.0.tgz",
|
||||
"integrity": "sha512-CWo+KyHCGyYtvjlmDIGtnwBEkdiondergZADiStbFFvie8pPI7IsdTXNVssQQ1VxKIBGGHVebgZGSklHBqthwA=="
|
||||
},
|
||||
"@aws-sdk/credential-provider-env": {
|
||||
"version": "3.190.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.190.0.tgz",
|
||||
"integrity": "sha512-GTY7l3SJhTmRGFpWddbdJOihSqoMN8JMo3CsCtIjk4/h3xirBi02T4GSvbrMyP7FP3Fdl4NAdT+mHJ4q2Bvzxw=="
|
||||
},
|
||||
"@aws-sdk/credential-provider-imds": {
|
||||
"version": "3.190.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.190.0.tgz",
|
||||
"integrity": "sha512-gI5pfBqGYCKdmx8igPvq+jLzyE2kuNn9Q5u73pdM/JZxiq7GeWYpE/MqqCubHxPtPcTFgAwxCxCFoXlUTBh/2g=="
|
||||
},
|
||||
"@aws-sdk/credential-provider-ini": {
|
||||
"version": "3.190.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.190.0.tgz",
|
||||
"integrity": "sha512-Z7NN/evXJk59hBQlfOSWDfHntwmxwryu6uclgv7ECI6SEVtKt1EKIlPuCLUYgQ4lxb9bomyO5lQAl/1WutNT5w=="
|
||||
},
|
||||
"@aws-sdk/credential-provider-node": {
|
||||
"version": "3.190.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.190.0.tgz",
|
||||
"integrity": "sha512-ctCG5+TsIK2gVgvvFiFjinPjc5nGpSypU3nQKCaihtPh83wDN6gCx4D0p9M8+fUrlPa5y+o/Y7yHo94ATepM8w=="
|
||||
},
|
||||
"@aws-sdk/credential-provider-process": {
|
||||
"version": "3.190.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.190.0.tgz",
|
||||
"integrity": "sha512-sIJhICR80n5XY1kW/EFHTh5ZzBHb5X+744QCH3StcbKYI44mOZvNKfFdeRL2fQ7yLgV7npte2HJRZzQPWpZUrw=="
|
||||
},
|
||||
"@aws-sdk/credential-provider-sso": {
|
||||
"version": "3.190.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.190.0.tgz",
|
||||
"integrity": "sha512-uarU9vk471MHHT+GJj3KWFSmaaqLNL5n1KcMer2CCAZfjs+mStAi8+IjZuuKXB4vqVs5DxdH8cy5aLaJcBlXwQ=="
|
||||
},
|
||||
"@aws-sdk/credential-provider-web-identity": {
|
||||
"version": "3.190.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.190.0.tgz",
|
||||
"integrity": "sha512-nlIBeK9hGHKWC874h+ITAfPZ9Eaok+x/ydZQVKsLHiQ9PH3tuQ8AaGqhuCwBSH0hEAHZ/BiKeEx5VyWAE8/x+Q=="
|
||||
},
|
||||
"@aws-sdk/credential-providers": {
|
||||
"version": "3.192.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.192.0.tgz",
|
||||
"integrity": "sha512-iBTrEPkfOHlfgQyk7EeUCmZnhUKXsGcc/hhxBbc6Z/Xc7Y8LqRVLbEmHq9lruXraFuvs26xV9oZi1s1UMXneQA=="
|
||||
},
|
||||
"@aws-sdk/fetch-http-handler": {
|
||||
"version": "3.190.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/fetch-http-handler/-/fetch-http-handler-3.190.0.tgz",
|
||||
"integrity": "sha512-5riRpKydARXAPLesTZm6eP6QKJ4HJGQ3k0Tepi3nvxHVx3UddkRNoX0pLS3rvbajkykWPNC2qdfRGApWlwOYsA=="
|
||||
},
|
||||
"@aws-sdk/hash-node": {
|
||||
"version": "3.190.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/hash-node/-/hash-node-3.190.0.tgz",
|
||||
"integrity": "sha512-DNwVT3O8zc9Jk/bXiXcN0WsD98r+JJWryw9F1/ZZbuzbf6rx2qhI8ZK+nh5X6WMtYPU84luQMcF702fJt/1bzg=="
|
||||
},
|
||||
"@aws-sdk/invalid-dependency": {
|
||||
"version": "3.190.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/invalid-dependency/-/invalid-dependency-3.190.0.tgz",
|
||||
"integrity": "sha512-crCh63e8d/Uw9y3dQlVTPja7+IZiXpNXyH6oSuAadTDQwMq6KK87Av1/SDzVf6bAo2KgAOo41MyO2joaCEk0dQ=="
|
||||
},
|
||||
"@aws-sdk/is-array-buffer": {
|
||||
"version": "3.188.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/is-array-buffer/-/is-array-buffer-3.188.0.tgz",
|
||||
"integrity": "sha512-n69N4zJZCNd87Rf4NzufPzhactUeM877Y0Tp/F3KiHqGeTnVjYUa4Lv1vLBjqtfjYb2HWT3NKlYn5yzrhaEwiQ=="
|
||||
},
|
||||
"@aws-sdk/middleware-content-length": {
|
||||
"version": "3.190.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-content-length/-/middleware-content-length-3.190.0.tgz",
|
||||
"integrity": "sha512-sSU347SuC6I8kWum1jlJlpAqeV23KP7enG+ToWcEcgFrJhm3AvuqB//NJxDbkKb2DNroRvJjBckBvrwNAjQnBQ=="
|
||||
},
|
||||
"@aws-sdk/middleware-host-header": {
|
||||
"version": "3.190.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.190.0.tgz",
|
||||
"integrity": "sha512-cL7Vo/QSpGx/DDmFxjeV0Qlyi1atvHQDPn3MLBBmi1icu+3GKZkCMAJwzsrV3U4+WoVoDYT9FJ9yMQf2HaIjeQ=="
|
||||
},
|
||||
"@aws-sdk/middleware-logger": {
|
||||
"version": "3.190.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.190.0.tgz",
|
||||
"integrity": "sha512-rrfLGYSZCBtiXNrIa8pJ2uwUoUMyj6Q82E8zmduTvqKWviCr6ZKes0lttGIkWhjvhql2m4CbjG5MPBnY7RXL4A=="
|
||||
},
|
||||
"@aws-sdk/middleware-recursion-detection": {
|
||||
"version": "3.190.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.190.0.tgz",
|
||||
"integrity": "sha512-5tc1AIIZe5jDNdyuJW+7vIFmQOxz3q031ZVrEtUEIF7cz2ySho2lkOWziz+v+UGSLhjHGKMz3V26+aN1FLZNxQ=="
|
||||
},
|
||||
"@aws-sdk/middleware-retry": {
|
||||
"version": "3.190.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-retry/-/middleware-retry-3.190.0.tgz",
|
||||
"integrity": "sha512-h1bPopkncf2ue/erJdhqvgR2AEh0bIvkNsIHhx93DckWKotZd/GAVDq0gpKj7/f/7B+teHH8Fg5GDOwOOGyKcg=="
|
||||
},
|
||||
"@aws-sdk/middleware-sdk-sts": {
|
||||
"version": "3.192.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.192.0.tgz",
|
||||
"integrity": "sha512-xzTV7MyG5ipWYTvekWX1tQc5ExsUvCYsDTBCD3LR5hBrP8assUDPo52zGSe+QMcjgnQv7BcYIzeikTkLEG0dUw=="
|
||||
},
|
||||
"@aws-sdk/middleware-serde": {
|
||||
"version": "3.190.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-serde/-/middleware-serde-3.190.0.tgz",
|
||||
"integrity": "sha512-S132hEOK4jwbtZ1bGAgSuQ0DMFG4TiD4ulAwbQRBYooC7tiWZbRiR0Pkt2hV8d7WhOHgUpg7rvqlA7/HXXBAsA=="
|
||||
},
|
||||
"@aws-sdk/middleware-signing": {
|
||||
"version": "3.192.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.192.0.tgz",
|
||||
"integrity": "sha512-qTRIU/TL/dvtTrNj+AkZkgYeTIFslib3Y3XnQNNM6RCm4cMxIgs2K/lnhaUmLdbzHrpOQb4cISkY8yiHo+pNsw=="
|
||||
},
|
||||
"@aws-sdk/middleware-stack": {
|
||||
"version": "3.190.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-stack/-/middleware-stack-3.190.0.tgz",
|
||||
"integrity": "sha512-h1mqiWNJdi1OTSEY8QovpiHgDQEeRG818v8yShpqSYXJKEqdn54MA3Z1D2fg/Wv/8ZJsFrBCiI7waT1JUYOmCg=="
|
||||
},
|
||||
"@aws-sdk/middleware-user-agent": {
|
||||
"version": "3.190.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.190.0.tgz",
|
||||
"integrity": "sha512-y/2cTE1iYHKR0nkb3DvR3G8vt12lcTP95r/iHp8ZO+Uzpc25jM/AyMHWr2ZjqQiHKNlzh8uRw1CmQtgg4sBxXQ=="
|
||||
},
|
||||
"@aws-sdk/node-config-provider": {
|
||||
"version": "3.190.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/node-config-provider/-/node-config-provider-3.190.0.tgz",
|
||||
"integrity": "sha512-TJPUchyeK5KeEXWrwb6oW5/OkY3STCSGR1QIlbPcaTGkbo4kXAVyQmmZsY4KtRPuDM6/HlfUQV17bD716K65rQ=="
|
||||
},
|
||||
"@aws-sdk/node-http-handler": {
|
||||
"version": "3.190.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/node-http-handler/-/node-http-handler-3.190.0.tgz",
|
||||
"integrity": "sha512-3Klkr73TpZkCzcnSP+gmFF0Baluzk3r7BaWclJHqt2LcFUWfIJzYlnbBQNZ4t3EEq7ZlBJX85rIDHBRlS+rUyA=="
|
||||
},
|
||||
"@aws-sdk/property-provider": {
|
||||
"version": "3.190.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/property-provider/-/property-provider-3.190.0.tgz",
|
||||
"integrity": "sha512-uzdKjHE2blbuceTC5zeBgZ0+Uo/hf9pH20CHpJeVNtrrtF3GALtu4Y1Gu5QQVIQBz8gjHnqANx0XhfYzorv69Q=="
|
||||
},
|
||||
"@aws-sdk/protocol-http": {
|
||||
"version": "3.190.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/protocol-http/-/protocol-http-3.190.0.tgz",
|
||||
"integrity": "sha512-s5MVfeONpfZYRzCSbqQ+wJ3GxKED+aSS7+CQoeaYoD6HDTDxaMGNv9aiPxVCzW02sgG7py7f29Q6Vw+5taZXZA=="
|
||||
},
|
||||
"@aws-sdk/querystring-builder": {
|
||||
"version": "3.190.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/querystring-builder/-/querystring-builder-3.190.0.tgz",
|
||||
"integrity": "sha512-w9mTKkCsaLIBC8EA4RAHrqethNGbf60CbpPzN/QM7yCV3ZZJAXkppFfjTVVOMbPaI8GUEOptJtzgqV68CRB7ow=="
|
||||
},
|
||||
"@aws-sdk/querystring-parser": {
|
||||
"version": "3.190.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/querystring-parser/-/querystring-parser-3.190.0.tgz",
|
||||
"integrity": "sha512-vCKP0s33VtS47LSYzEWRRr2aTbi3qNkUuQyIrc5LMqBfS5hsy79P1HL4Q7lCVqZB5fe61N8fKzOxDxWRCF0sXg=="
|
||||
},
|
||||
"@aws-sdk/service-error-classification": {
|
||||
"version": "3.190.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/service-error-classification/-/service-error-classification-3.190.0.tgz",
|
||||
"integrity": "sha512-g+s6xtaMa5fCMA2zJQC4BiFGMP7FN5/L1V/UwxCnKy8skCwaN0K5A1tFffBjjbYiPI7Gu7LVorWD2A0Y4xl01Q=="
|
||||
},
|
||||
"@aws-sdk/shared-ini-file-loader": {
|
||||
"version": "3.190.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/shared-ini-file-loader/-/shared-ini-file-loader-3.190.0.tgz",
|
||||
"integrity": "sha512-CZC/xsGReUEl5w+JgfancrxfkaCbEisyIFy6HALUYrioWQe80WMqLAdUMZSXHWjIaNK9mH0J/qvcSV2MuIoMzQ=="
|
||||
},
|
||||
"@aws-sdk/signature-v4": {
|
||||
"version": "3.190.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4/-/signature-v4-3.190.0.tgz",
|
||||
"integrity": "sha512-L/R/1X2T+/Kg2k/sjoYyDFulVUGrVcRfyEKKVFIUNg0NwUtw5UKa1/gS7geTKcg4q8M2pd/v+OCBrge2X7phUw=="
|
||||
},
|
||||
"@aws-sdk/smithy-client": {
|
||||
"version": "3.190.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/smithy-client/-/smithy-client-3.190.0.tgz",
|
||||
"integrity": "sha512-f5EoCwjBLXMyuN491u1NmEutbolL0cJegaJbtgK9OJw2BLuRHiBknjDF4OEVuK/WqK0kz2JLMGi9xwVPl4BKCA=="
|
||||
},
|
||||
"@aws-sdk/types": {
|
||||
"version": "3.190.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.190.0.tgz",
|
||||
"integrity": "sha512-mkeZ+vJZzElP6OdRXvuLKWHSlDQxZP9u8BjQB9N0Rw0pCXTzYS0vzIhN1pL0uddWp5fMrIE68snto9xNR6BQuA=="
|
||||
},
|
||||
"@aws-sdk/url-parser": {
|
||||
"version": "3.190.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/url-parser/-/url-parser-3.190.0.tgz",
|
||||
"integrity": "sha512-FKFDtxA9pvHmpfWmNVK5BAVRpDgkWMz3u4Sg9UzB+WAFN6UexRypXXUZCFAo8S04FbPKfYOR3O0uVlw7kzmj9g=="
|
||||
},
|
||||
"@aws-sdk/util-base64-browser": {
|
||||
"version": "3.188.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/util-base64-browser/-/util-base64-browser-3.188.0.tgz",
|
||||
"integrity": "sha512-qlH+5NZBLiyKziL335BEPedYxX6j+p7KFRWXvDQox9S+s+gLCayednpK+fteOhBenCcR9fUZOVuAPScy1I8qCg=="
|
||||
},
|
||||
"@aws-sdk/util-base64-node": {
|
||||
"version": "3.188.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/util-base64-node/-/util-base64-node-3.188.0.tgz",
|
||||
"integrity": "sha512-r1dccRsRjKq+OhVRUfqFiW3sGgZBjHbMeHLbrAs9jrOjU2PTQ8PSzAXLvX/9lmp7YjmX17Qvlsg0NCr1tbB9OA=="
|
||||
},
|
||||
"@aws-sdk/util-body-length-browser": {
|
||||
"version": "3.188.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/util-body-length-browser/-/util-body-length-browser-3.188.0.tgz",
|
||||
"integrity": "sha512-8VpnwFWXhnZ/iRSl9mTf+VKOX9wDE8QtN4bj9pBfxwf90H1X7E8T6NkiZD3k+HubYf2J94e7DbeHs7fuCPW5Qg=="
|
||||
},
|
||||
"@aws-sdk/util-body-length-node": {
|
||||
"version": "3.188.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/util-body-length-node/-/util-body-length-node-3.188.0.tgz",
|
||||
"integrity": "sha512-XwqP3vxk60MKp4YDdvDeCD6BPOiG2e+/Ou4AofZOy5/toB6NKz2pFNibQIUg2+jc7mPMnGnvOW3MQEgSJ+gu/Q=="
|
||||
},
|
||||
"@aws-sdk/util-buffer-from": {
|
||||
"version": "3.188.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/util-buffer-from/-/util-buffer-from-3.188.0.tgz",
|
||||
"integrity": "sha512-NX1WXZ8TH20IZb4jPFT2CnLKSqZWddGxtfiWxD9M47YOtq/SSQeR82fhqqVjJn4P8w2F5E28f+Du4ntg/sGcxA=="
|
||||
},
|
||||
"@aws-sdk/util-config-provider": {
|
||||
"version": "3.188.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/util-config-provider/-/util-config-provider-3.188.0.tgz",
|
||||
"integrity": "sha512-LBA7tLbi7v4uvbOJhSnjJrxbcRifKK/1ZVK94JTV2MNSCCyNkFotyEI5UWDl10YKriTIUyf7o5cakpiDZ3O4xg=="
|
||||
},
|
||||
"@aws-sdk/util-defaults-mode-browser": {
|
||||
"version": "3.190.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/util-defaults-mode-browser/-/util-defaults-mode-browser-3.190.0.tgz",
|
||||
"integrity": "sha512-FKxTU4tIbFk2pdUbBNneStF++j+/pB4NYJ1HRSEAb/g4D2+kxikR/WKIv3p0JTVvAkwcuX/ausILYEPUyDZ4HQ=="
|
||||
},
|
||||
"@aws-sdk/util-defaults-mode-node": {
|
||||
"version": "3.190.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/util-defaults-mode-node/-/util-defaults-mode-node-3.190.0.tgz",
|
||||
"integrity": "sha512-qBiIMjNynqAP7p6urG1+ZattYkFaylhyinofVcLEiDvM9a6zGt6GZsxru2Loq0kRAXXGew9E9BWGt45HcDc20g=="
|
||||
},
|
||||
"@aws-sdk/util-hex-encoding": {
|
||||
"version": "3.188.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/util-hex-encoding/-/util-hex-encoding-3.188.0.tgz",
|
||||
"integrity": "sha512-QyWovTtjQ2RYxqVM+STPh65owSqzuXURnfoof778spyX4iQ4z46wOge1YV2ZtwS8w5LWd9eeVvDrLu5POPYOnA=="
|
||||
},
|
||||
"@aws-sdk/util-locate-window": {
|
||||
"version": "3.188.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.188.0.tgz",
|
||||
"integrity": "sha512-SxobBVLZkkLSawTCfeQnhVX3Azm9O+C2dngZVe1+BqtF8+retUbVTs7OfYeWBlawVkULKF2e781lTzEHBBjCzw=="
|
||||
},
|
||||
"@aws-sdk/util-middleware": {
|
||||
"version": "3.190.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/util-middleware/-/util-middleware-3.190.0.tgz",
|
||||
"integrity": "sha512-qzTJ/qhFDzHZS+iXdHydQ/0sWAuNIB5feeLm55Io/I8Utv3l3TKYOhbgGwTsXY+jDk7oD+YnAi7hLN5oEBCwpg=="
|
||||
},
|
||||
"@aws-sdk/util-uri-escape": {
|
||||
"version": "3.188.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/util-uri-escape/-/util-uri-escape-3.188.0.tgz",
|
||||
"integrity": "sha512-4Y6AYZMT483Tiuq8dxz5WHIiPNdSFPGrl6tRTo2Oi2FcwypwmFhqgEGcqxeXDUJktvaCBxeA08DLr/AemVhPCg=="
|
||||
},
|
||||
"@aws-sdk/util-user-agent-browser": {
|
||||
"version": "3.190.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.190.0.tgz",
|
||||
"integrity": "sha512-c074wjsD+/u9vT7DVrBLkwVhn28I+OEHuHaqpTVCvAIjpueZ3oms0e99YJLfpdpEgdLavOroAsNFtAuRrrTZZw=="
|
||||
},
|
||||
"@aws-sdk/util-user-agent-node": {
|
||||
"version": "3.190.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.190.0.tgz",
|
||||
"integrity": "sha512-R36BMvvPX8frqFhU4lAsrOJ/2PJEHH/Jz1WZzO3GWmVSEAQQdHmo8tVPE3KOM7mZWe5Hj1dZudFAIxWHHFYKJA=="
|
||||
},
|
||||
"@aws-sdk/util-utf8-browser": {
|
||||
"version": "3.188.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.188.0.tgz",
|
||||
"integrity": "sha512-jt627x0+jE+Ydr9NwkFstg3cUvgWh56qdaqAMDsqgRlKD21md/6G226z/Qxl7lb1VEW2LlmCx43ai/37Qwcj2Q=="
|
||||
},
|
||||
"@aws-sdk/util-utf8-node": {
|
||||
"version": "3.188.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-node/-/util-utf8-node-3.188.0.tgz",
|
||||
"integrity": "sha512-hCgP4+C0Lekjpjt2zFJ2R/iHes5sBGljXa5bScOFAEkRUc0Qw0VNgTv7LpEbIOAwGmqyxBoCwBW0YHPW1DfmYQ=="
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "18.7.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.13.tgz",
|
||||
"integrity": "sha512-46yIhxSe5xEaJZXWdIBP7GU4HDTG8/eo0qd9atdiL+lFpA03y8KS+lkTN834TWJj5767GbWv4n/P6efyTFt1Dw=="
|
||||
"version": "18.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.2.tgz",
|
||||
"integrity": "sha512-BWN3M23gLO2jVG8g/XHIRFWiiV4/GckeFIqbU/C4V3xpoBBWSMk4OZomouN0wCkfQFPqgZikyLr7DOYDysIkkw=="
|
||||
},
|
||||
"@types/webidl-conversions": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-6.1.1.tgz",
|
||||
"integrity": "sha512-XAahCdThVuCFDQLT7R7Pk/vqeObFNL3YqRyFZg+AqAP/W1/w3xHaIxuW7WszQqTbIBOPRcItYJIou3i/mppu3Q=="
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||
"integrity": "sha512-xTE1E+YF4aWPJJeUzaZI5DRntlkY3+BCVJi0axFptnjGmAoWxkyREIh/XMrfxVLejwQxMCfDXdICo0VLxThrog=="
|
||||
},
|
||||
"@types/whatwg-url": {
|
||||
"version": "8.2.2",
|
||||
@@ -21,6 +361,11 @@
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
|
||||
},
|
||||
"bowser": {
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz",
|
||||
"integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA=="
|
||||
},
|
||||
"bson": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/bson/-/bson-4.7.0.tgz",
|
||||
@@ -36,6 +381,11 @@
|
||||
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="
|
||||
},
|
||||
"fast-xml-parser": {
|
||||
"version": "4.0.11",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.0.11.tgz",
|
||||
"integrity": "sha512-4aUg3aNRR/WjQAcpceODG1C3x3lFANXRo8+1biqfieHmg9pyMt7qB4lQV/Ta6sJCTbA5vfD8fnA8S54JATiFUA=="
|
||||
},
|
||||
"ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
@@ -52,14 +402,14 @@
|
||||
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="
|
||||
},
|
||||
"mongodb": {
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.9.0.tgz",
|
||||
"integrity": "sha512-tJJEFJz7OQTQPZeVHZJIeSOjMRqc5eSyXTt86vSQENEErpkiG7279tM/GT5AVZ7TgXNh9HQxoa2ZkbrANz5GQw=="
|
||||
"version": "4.11.0",
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.11.0.tgz",
|
||||
"integrity": "sha512-9l9n4Nk2BYZzljW3vHah3Z0rfS5npKw6ktnkmFgTcnzaXH1DRm3pDl6VMHu84EVb1lzmSaJC4OzWZqTkB5i2wg=="
|
||||
},
|
||||
"mongodb-connection-string-url": {
|
||||
"version": "2.5.3",
|
||||
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.5.3.tgz",
|
||||
"integrity": "sha512-f+/WsED+xF4B74l3k9V/XkTVj5/fxFH2o5ToKXd8Iyi5UhM+sO9u0Ape17Mvl/GkZaFtM0HQnzAG5OTmhKw+tQ=="
|
||||
"version": "2.5.4",
|
||||
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.5.4.tgz",
|
||||
"integrity": "sha512-SeAxuWs0ez3iI3vvmLk/j2y+zHwigTDKQhtdxTgt5ZCOQQS5+HW4g45/Xw5vzzbn7oQXCNQ24Z40AkJsizEy7w=="
|
||||
},
|
||||
"punycode": {
|
||||
"version": "2.1.1",
|
||||
@@ -77,20 +427,35 @@
|
||||
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="
|
||||
},
|
||||
"socks": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.7.0.tgz",
|
||||
"integrity": "sha512-scnOe9y4VuiNUULJN72GrM26BNOjVsfPXI+j+98PkyEfsIXroa5ofyjT+FzGvn/xHs73U2JtoBYAVx9Hl4quSA=="
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz",
|
||||
"integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ=="
|
||||
},
|
||||
"sparse-bitfield": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
|
||||
"integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ=="
|
||||
},
|
||||
"strnum": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz",
|
||||
"integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA=="
|
||||
},
|
||||
"tr46": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz",
|
||||
"integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA=="
|
||||
},
|
||||
"tslib": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
||||
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
|
||||
},
|
||||
"uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
|
||||
},
|
||||
"webidl-conversions": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
|
||||
Package.describe({
|
||||
summary: "Wrapper around the mongo npm package",
|
||||
version: "4.9.0",
|
||||
version: "4.11.0",
|
||||
documentation: null
|
||||
});
|
||||
|
||||
Npm.depends({
|
||||
mongodb: "4.9.0"
|
||||
mongodb: "4.11.0"
|
||||
});
|
||||
|
||||
Package.onUse(function (api) {
|
||||
|
||||
@@ -136,7 +136,7 @@ OAuth._checkRedirectUrlOrigin = redirectUrl => {
|
||||
);
|
||||
};
|
||||
|
||||
const middleware = (req, res, next) => {
|
||||
const middleware = async (req, res, next) => {
|
||||
let requestData;
|
||||
|
||||
// Make sure to catch any exceptions because otherwise we'd crash
|
||||
@@ -168,7 +168,7 @@ const middleware = (req, res, next) => {
|
||||
requestData = req.body;
|
||||
}
|
||||
|
||||
handler(service, requestData, res);
|
||||
await handler(service, requestData, res);
|
||||
} catch (err) {
|
||||
// if we got thrown an error, save it off, it will get passed to
|
||||
// the appropriate login call (if any) and reported there.
|
||||
@@ -473,3 +473,31 @@ OAuth.openSecrets = (serviceData, userId) => {
|
||||
);
|
||||
return result;
|
||||
};
|
||||
|
||||
OAuth._addValuesToQueryParams = (
|
||||
values = {},
|
||||
queryParams = new URLSearchParams()
|
||||
) => {
|
||||
Object.entries(values).forEach(([key, value]) => {
|
||||
queryParams.set(key, `${value}`);
|
||||
});
|
||||
return queryParams;
|
||||
};
|
||||
|
||||
OAuth._fetch = async (
|
||||
url,
|
||||
method = 'GET',
|
||||
{ headers = {}, queryParams = {}, body, ...options } = {}
|
||||
) => {
|
||||
const urlWithParams = new URL(url);
|
||||
|
||||
OAuth._addValuesToQueryParams(queryParams, urlWithParams.searchParams);
|
||||
|
||||
const requestOptions = {
|
||||
method: method.toUpperCase(),
|
||||
headers,
|
||||
...(body ? { body } : {}),
|
||||
...options,
|
||||
};
|
||||
return fetch(urlWithParams.toString(), requestOptions);
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ Package.onUse(api => {
|
||||
api.use(['reload', 'base64'], 'client');
|
||||
|
||||
api.use('oauth-encryption', 'server', {weak: true});
|
||||
api.use('fetch', 'server');
|
||||
|
||||
|
||||
api.export('OAuth');
|
||||
|
||||
@@ -19,12 +19,12 @@ export class OAuth1Binding {
|
||||
this._urls = urls;
|
||||
}
|
||||
|
||||
prepareRequestToken(callbackUrl) {
|
||||
async prepareRequestToken(callbackUrl) {
|
||||
const headers = this._buildHeader({
|
||||
oauth_callback: callbackUrl
|
||||
});
|
||||
|
||||
const response = this._call('POST', this._urls.requestToken, headers);
|
||||
const response = await this._call({method: 'POST', url: this._urls.requestToken, headers});
|
||||
const tokens = querystring.parse(response.content);
|
||||
|
||||
if (! tokens.oauth_callback_confirmed)
|
||||
@@ -35,7 +35,7 @@ export class OAuth1Binding {
|
||||
this.requestTokenSecret = tokens.oauth_token_secret;
|
||||
}
|
||||
|
||||
prepareAccessToken(query, requestTokenSecret) {
|
||||
async prepareAccessToken(query, requestTokenSecret) {
|
||||
// support implementations that use request token secrets. This is
|
||||
// read by this._call.
|
||||
//
|
||||
@@ -50,7 +50,7 @@ export class OAuth1Binding {
|
||||
oauth_verifier: query.oauth_verifier
|
||||
});
|
||||
|
||||
const response = this._call('POST', this._urls.accessToken, headers);
|
||||
const response = await this._call({ method: 'POST', url: this._urls.accessToken, headers });
|
||||
const tokens = querystring.parse(response.content);
|
||||
|
||||
if (! tokens.oauth_token || ! tokens.oauth_token_secret) {
|
||||
@@ -66,7 +66,7 @@ export class OAuth1Binding {
|
||||
this.accessTokenSecret = tokens.oauth_token_secret;
|
||||
}
|
||||
|
||||
call(method, url, params, callback) {
|
||||
async callAsync(method, url, params, callback) {
|
||||
const headers = this._buildHeader({
|
||||
oauth_token: this.accessToken
|
||||
});
|
||||
@@ -75,14 +75,29 @@ export class OAuth1Binding {
|
||||
params = {};
|
||||
}
|
||||
|
||||
return this._call(method, url, headers, params, callback);
|
||||
return this._call({ method, url, headers, params, callback });
|
||||
}
|
||||
|
||||
async getAsync(url, params, callback) {
|
||||
return this.callAsync('GET', url, params, callback);
|
||||
}
|
||||
|
||||
async postAsync(url, params, callback) {
|
||||
return this.callAsync('POST', url, params, callback);
|
||||
}
|
||||
|
||||
call(method, url, params, callback) {
|
||||
// Require changes when remove Fibers. Exposed to public api.
|
||||
return Promise.await(this.callAsync(method, url, params, callback));
|
||||
}
|
||||
|
||||
get(url, params, callback) {
|
||||
// Require changes when remove Fibers. Exposed to public api.
|
||||
return this.call('GET', url, params, callback);
|
||||
}
|
||||
|
||||
post(url, params, callback) {
|
||||
// Require changes when remove Fibers. Exposed to public api.
|
||||
return this.call('POST', url, params, callback);
|
||||
}
|
||||
|
||||
@@ -118,7 +133,7 @@ export class OAuth1Binding {
|
||||
return crypto.createHmac('SHA1', signingKey).update(signatureBase).digest('base64');
|
||||
};
|
||||
|
||||
_call(method, url, headers = {}, params = {}, callback) {
|
||||
async _call({method, url, headers = {}, params = {}, callback}) {
|
||||
// all URLs to be functions to support parameters/customization
|
||||
if(typeof url === "function") {
|
||||
url = url(this);
|
||||
@@ -141,29 +156,52 @@ export class OAuth1Binding {
|
||||
|
||||
// Make a authorization string according to oauth1 spec
|
||||
const authString = this._getAuthHeaderString(headers);
|
||||
|
||||
// Make signed request
|
||||
try {
|
||||
const response = HTTP.call(method, url, {
|
||||
params,
|
||||
headers: {
|
||||
Authorization: authString
|
||||
return OAuth._fetch(url, method, {
|
||||
headers: {
|
||||
Authorization: authString,
|
||||
...(method.toUpperCase() === 'POST' ? { 'Content-Type': 'application/x-www-form-urlencoded' } : {})
|
||||
},
|
||||
...(method.toUpperCase() === 'POST' ?
|
||||
{ body: OAuth._addValuesToQueryParams(params).toString() }
|
||||
: { queryParams: params })
|
||||
}).then((res) =>
|
||||
res.text().then((content) => {
|
||||
const responseHeaders = Array.from(res.headers.entries()).reduce(
|
||||
(acc, [key, val]) => {
|
||||
return { ...acc, [key.toLowerCase()]: val };
|
||||
},
|
||||
{}
|
||||
);
|
||||
const data = responseHeaders['content-type'].includes('application/json') ?
|
||||
JSON.parse(content) : undefined;
|
||||
return {
|
||||
content: data ? '' : content,
|
||||
data,
|
||||
headers: { ...responseHeaders, nonce: headers.oauth_nonce },
|
||||
redirected: res.redirected,
|
||||
ok: res.ok,
|
||||
statusCode: res.status,
|
||||
};
|
||||
})
|
||||
)
|
||||
.then((response) => {
|
||||
if (callback) {
|
||||
callback(undefined, response);
|
||||
}
|
||||
}, callback && ((error, response) => {
|
||||
if (! error) {
|
||||
response.nonce = headers.oauth_nonce;
|
||||
return response;
|
||||
})
|
||||
.catch((err) => {
|
||||
if (callback) {
|
||||
callback(err);
|
||||
}
|
||||
callback(error, response);
|
||||
}));
|
||||
// We store nonce so that JWTs can be validated
|
||||
if (response)
|
||||
response.nonce = headers.oauth_nonce;
|
||||
return response;
|
||||
} catch (err) {
|
||||
throw Object.assign(new Error(`Failed to send OAuth1 request to ${url}. ${err.message}`),
|
||||
{response: err.response});
|
||||
}
|
||||
};
|
||||
console.log({ err });
|
||||
throw Object.assign(
|
||||
new Error(`Failed to send OAuth1 request to ${url}. ${err.message}`),
|
||||
{ response: err.response }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
_encodeHeader(header) {
|
||||
return Object.keys(header).reduce((memo, key) => {
|
||||
|
||||
@@ -6,7 +6,7 @@ OAuth._queryParamsWithAuthTokenUrl = (authUrl, oauthBinding, params = {}, whitel
|
||||
|
||||
Object.assign(
|
||||
redirectUrlObj.query,
|
||||
whitelistedQueryParams.reduce((prev, param) =>
|
||||
whitelistedQueryParams.reduce((prev, param) =>
|
||||
params.query[param] ? { ...prev, param: params.query[param] } : prev,
|
||||
{}
|
||||
),
|
||||
@@ -25,7 +25,7 @@ OAuth._queryParamsWithAuthTokenUrl = (authUrl, oauthBinding, params = {}, whitel
|
||||
};
|
||||
|
||||
// connect middleware
|
||||
OAuth._requestHandlers['1'] = (service, query, res) => {
|
||||
OAuth._requestHandlers['1'] = async (service, query, res) => {
|
||||
const config = ServiceConfiguration.configurations.findOne({service: service.serviceName});
|
||||
if (! config) {
|
||||
throw new ServiceConfiguration.ConfigError(service.serviceName);
|
||||
@@ -45,7 +45,7 @@ OAuth._requestHandlers['1'] = (service, query, res) => {
|
||||
});
|
||||
|
||||
// Get a request token to start auth process
|
||||
oauthBinding.prepareRequestToken(callbackUrl);
|
||||
await oauthBinding.prepareRequestToken(callbackUrl);
|
||||
|
||||
// Keep track of request token so we can verify it on the next step
|
||||
OAuth._storeRequestToken(
|
||||
@@ -91,10 +91,10 @@ OAuth._requestHandlers['1'] = (service, query, res) => {
|
||||
// subsequent call to the `login` method will be immediate.
|
||||
|
||||
// Get the access token for signing requests
|
||||
oauthBinding.prepareAccessToken(query, requestTokenInfo.requestTokenSecret);
|
||||
await oauthBinding.prepareAccessToken(query, requestTokenInfo.requestTokenSecret);
|
||||
|
||||
// Run service-specific handler.
|
||||
const oauthResult = service.handleOauthRequest(
|
||||
const oauthResult = await service.handleOauthRequest(
|
||||
oauthBinding, { query: query });
|
||||
|
||||
const credentialToken = OAuth._credentialTokenFromQuery(query);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import http from 'http';
|
||||
import { OAuth1Binding } from './oauth1_binding';
|
||||
|
||||
const testPendingCredential = (test, method) => {
|
||||
const testPendingCredential = async (test, method) => {
|
||||
const twitterfooId = Random.id();
|
||||
const twitterfooName = `nickname${Random.id()}`;
|
||||
const twitterfooAccessToken = Random.id();
|
||||
@@ -17,8 +17,8 @@ const testPendingCredential = (test, method) => {
|
||||
authenticate: "https://example.com/oauth/authenticate"
|
||||
};
|
||||
|
||||
OAuth1Binding.prototype.prepareRequestToken = () => {};
|
||||
OAuth1Binding.prototype.prepareAccessToken = function() {
|
||||
OAuth1Binding.prototype.prepareRequestToken = async () => {};
|
||||
OAuth1Binding.prototype.prepareAccessToken = async function() {
|
||||
this.accessToken = twitterfooAccessToken;
|
||||
this.accessTokenSecret = twitterfooAccessTokenSecret;
|
||||
};
|
||||
@@ -27,7 +27,7 @@ const testPendingCredential = (test, method) => {
|
||||
|
||||
try {
|
||||
// register a fake login service
|
||||
OAuth.registerService(serviceName, 1, urls, query => ({
|
||||
OAuth.registerService(serviceName, 1, urls, async query => ({
|
||||
serviceData: {
|
||||
id: twitterfooId,
|
||||
screenName: twitterfooName,
|
||||
@@ -71,7 +71,7 @@ const testPendingCredential = (test, method) => {
|
||||
respData += args[0];
|
||||
return end.apply(this, arguments);
|
||||
};
|
||||
OAuthTest.middleware(req, res);
|
||||
await OAuthTest.middleware(req, res);
|
||||
const credentialSecret = respData;
|
||||
|
||||
// Test that the result for the token is available
|
||||
@@ -94,17 +94,17 @@ const testPendingCredential = (test, method) => {
|
||||
}
|
||||
};
|
||||
|
||||
Tinytest.add("oauth1 - pendingCredential is stored and can be retrieved (without oauth encryption)", test => {
|
||||
Tinytest.addAsync("oauth1 - pendingCredential is stored and can be retrieved (without oauth encryption)", async test => {
|
||||
OAuthEncryption.loadKey(null);
|
||||
testPendingCredential(test, "GET");
|
||||
testPendingCredential(test, "POST");
|
||||
await testPendingCredential(test, "GET");
|
||||
await testPendingCredential(test, "POST");
|
||||
});
|
||||
|
||||
Tinytest.add("oauth1 - pendingCredential is stored and can be retrieved (with oauth encryption)", test => {
|
||||
Tinytest.addAsync("oauth1 - pendingCredential is stored and can be retrieved (with oauth encryption)", async test => {
|
||||
try {
|
||||
OAuthEncryption.loadKey(Buffer.from([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]).toString("base64"));
|
||||
testPendingCredential(test, "GET");
|
||||
testPendingCredential(test, "POST");
|
||||
await testPendingCredential(test, "GET");
|
||||
await testPendingCredential(test, "POST");
|
||||
} finally {
|
||||
OAuthEncryption.loadKey(null);
|
||||
}
|
||||
|
||||
@@ -8,10 +8,7 @@ Package.onUse(api => {
|
||||
api.use('random');
|
||||
api.use('service-configuration', ['client', 'server']);
|
||||
api.use('oauth', ['client', 'server']);
|
||||
api.use([
|
||||
'check',
|
||||
'http@1.4.4 || 2.0.0'
|
||||
], 'server');
|
||||
api.use('check', 'server');
|
||||
|
||||
api.use('mongo');
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// connect middleware
|
||||
OAuth._requestHandlers['2'] = (service, query, res) => {
|
||||
OAuth._requestHandlers['2'] = async (service, query, res) => {
|
||||
let credentialSecret;
|
||||
|
||||
// check if user authorized access
|
||||
@@ -7,7 +7,7 @@ OAuth._requestHandlers['2'] = (service, query, res) => {
|
||||
// Prepare the login results before returning.
|
||||
|
||||
// Run service-specific handler.
|
||||
const oauthResult = service.handleOauthRequest(query);
|
||||
const oauthResult = await service.handleOauthRequest(query);
|
||||
credentialSecret = Random.secret();
|
||||
|
||||
const credentialToken = OAuth._credentialTokenFromQuery(query);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import http from 'http';
|
||||
|
||||
const testPendingCredential = function (test, method) {
|
||||
const testPendingCredential = async function (test, method) {
|
||||
const foobookId = Random.id();
|
||||
const foobookOption1 = Random.id();
|
||||
const credentialToken = Random.id();
|
||||
@@ -51,7 +51,7 @@ const testPendingCredential = function (test, method) {
|
||||
return end.apply(this, args);
|
||||
};
|
||||
|
||||
OAuthTest.middleware(req, res);
|
||||
await OAuthTest.middleware(req, res);
|
||||
const credentialSecret = respData;
|
||||
|
||||
// Test that the result for the token is available
|
||||
@@ -72,17 +72,17 @@ const testPendingCredential = function (test, method) {
|
||||
}
|
||||
};
|
||||
|
||||
Tinytest.add("oauth2 - pendingCredential is stored and can be retrieved (without oauth encryption)", test => {
|
||||
Tinytest.addAsync("oauth2 - pendingCredential is stored and can be retrieved (without oauth encryption)", async test => {
|
||||
OAuthEncryption.loadKey(null);
|
||||
testPendingCredential(test, "GET");
|
||||
testPendingCredential(test, "POST");
|
||||
await testPendingCredential(test, "GET");
|
||||
await testPendingCredential(test, "POST");
|
||||
});
|
||||
|
||||
Tinytest.add("oauth2 - pendingCredential is stored and can be retrieved (with oauth encryption)", test => {
|
||||
Tinytest.addAsync("oauth2 - pendingCredential is stored and can be retrieved (with oauth encryption)", async test => {
|
||||
try {
|
||||
OAuthEncryption.loadKey(Buffer.from([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]).toString("base64"));
|
||||
testPendingCredential(test, "GET");
|
||||
testPendingCredential(test, "POST");
|
||||
await testPendingCredential(test, "GET");
|
||||
await testPendingCredential(test, "POST");
|
||||
} finally {
|
||||
OAuthEncryption.loadKey(null);
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ class CssToolsMinifier {
|
||||
path: 'merged-stylesheets.css'
|
||||
}];
|
||||
} else {
|
||||
const minifiedFiles = CssTools.minifyCss(merged.code);
|
||||
const minifiedFiles = await CssTools.minifyCssAsync(merged.code);
|
||||
|
||||
result = minifiedFiles.map(minified => ({
|
||||
data: minified
|
||||
|
||||
@@ -142,8 +142,13 @@ testAsyncMulti = function (name, funcs, { isOnly = false } = {}) {
|
||||
test.extraDetails.asyncBlock = i++;
|
||||
|
||||
new Promise(resolve => {
|
||||
resolve(func.apply(context, [test, _.bind(em.expect, em)]));
|
||||
}).then(result => {
|
||||
const result = func.apply(context, [test, _.bind(em.expect, em)]);
|
||||
if (result && typeof result.then === "function") {
|
||||
return result.then((r) => resolve(r))
|
||||
}
|
||||
|
||||
return resolve(result);
|
||||
}).then(() => {
|
||||
em.done();
|
||||
}, exception => {
|
||||
if (em.cancel()) {
|
||||
@@ -191,3 +196,24 @@ pollUntil = function (expect, f, timeout, step, noFail) {
|
||||
step
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper that is used on the async tests.
|
||||
* Just run the function and assert if we have an error or not.
|
||||
* @param fn
|
||||
* @param test
|
||||
* @param shouldErrorOut
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
runAndThrowIfNeeded = async (fn, test, shouldErrorOut) => {
|
||||
let err, result;
|
||||
try {
|
||||
result = await fn();
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
test[shouldErrorOut ? "isTrue" : "isFalse"](err);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Package.describe({
|
||||
summary: "Utility functions for tests",
|
||||
version: '1.3.0'
|
||||
version: '1.3.1'
|
||||
});
|
||||
|
||||
Package.onUse(function (api) {
|
||||
@@ -28,7 +28,8 @@ Package.onUse(function (api) {
|
||||
'SeededRandom', 'clickElement', 'blurElement',
|
||||
'focusElement', 'simulateEvent', 'getStyleProperty', 'canonicalizeHtml',
|
||||
'renderToDiv', 'clickIt',
|
||||
'withCallbackLogger', 'testAsyncMulti', 'simplePoll',
|
||||
'withCallbackLogger', 'testAsyncMulti',
|
||||
'simplePoll', 'runAndThrowIfNeeded',
|
||||
'makeTestConnection', 'DomUtils']);
|
||||
|
||||
api.addFiles('try_all_permutations.js');
|
||||
|
||||
@@ -88,7 +88,7 @@ var reportResults = function(results) {
|
||||
}
|
||||
|
||||
// Now process the current report
|
||||
if (_.isArray(results.events)) {
|
||||
if (Array.isArray(results.events)) {
|
||||
// append events, if present
|
||||
Array.prototype.push.apply((test.events || (test.events = [])),
|
||||
results.events);
|
||||
@@ -97,7 +97,7 @@ var reportResults = function(results) {
|
||||
return a.sequence - b.sequence;
|
||||
});
|
||||
var out = [];
|
||||
_.each(test.events, function (e) {
|
||||
test.events.forEach(function (e) {
|
||||
if (out.length === 0 || out[out.length - 1].sequence !== e.sequence)
|
||||
out.push(e);
|
||||
});
|
||||
@@ -110,7 +110,7 @@ var reportResults = function(results) {
|
||||
// test name yet).
|
||||
if (test.expanded === undefined)
|
||||
test.expanded = true;
|
||||
if (!_.contains(failedTests, test.fullName))
|
||||
if (!failedTests.includes(test.fullName))
|
||||
failedTests.push(test.fullName);
|
||||
|
||||
countDep.changed();
|
||||
@@ -147,16 +147,16 @@ var forgetEvents = function (results) {
|
||||
// possibly 'events'.
|
||||
var _findTestForResults = function (results) {
|
||||
var groupPath = results.groupPath; // array
|
||||
if ((! _.isArray(groupPath)) || (groupPath.length < 1)) {
|
||||
if ((! Array.isArray(groupPath)) || (groupPath.length < 1)) {
|
||||
throw new Error("Test must be part of a group");
|
||||
}
|
||||
|
||||
var group;
|
||||
var i = 0;
|
||||
_.each(groupPath, function(gname) {
|
||||
groupPath.forEach(function(gname) {
|
||||
var array = (group ? (group.groups || (group.groups = []))
|
||||
: resultTree);
|
||||
var newGroup = _.find(array, function(g) { return g.name === gname; });
|
||||
var newGroup = array.find(function(g) { return g.name === gname; });
|
||||
if (! newGroup) {
|
||||
newGroup = {
|
||||
name: gname,
|
||||
@@ -177,12 +177,12 @@ var _findTestForResults = function (results) {
|
||||
|
||||
var testName = results.test;
|
||||
var server = !!results.server;
|
||||
var test = _.find(group.tests || (group.tests = []),
|
||||
var test = (group.tests || (group.tests = [])).find(
|
||||
function(t) { return t.name === testName &&
|
||||
t.server === server; });
|
||||
if (! test) {
|
||||
// create test
|
||||
var nameParts = _.clone(groupPath);
|
||||
var nameParts = [...groupPath];
|
||||
nameParts.push(testName);
|
||||
var fullName = nameParts.join(' - ');
|
||||
test = {
|
||||
@@ -209,7 +209,7 @@ var _findTestForResults = function (results) {
|
||||
|
||||
var _testTime = function(t) {
|
||||
if (t.events && t.events.length > 0) {
|
||||
var lastEvent = _.last(t.events);
|
||||
var lastEvent = t.events[t.events.length - 1];
|
||||
if (lastEvent.type === "finish") {
|
||||
if ((typeof lastEvent.timeMs) === "number") {
|
||||
return lastEvent.timeMs;
|
||||
@@ -221,15 +221,15 @@ var _testTime = function(t) {
|
||||
|
||||
var _testStatus = function(t) {
|
||||
var events = t.events || [];
|
||||
if (_.find(events, function(x) { return x.type === "exception"; })) {
|
||||
if (events.find(function(x) { return x.type === "exception"; })) {
|
||||
// "exception" should be last event, except race conditions on the
|
||||
// server can make this not the case. Technically we can't tell
|
||||
// if the test is still running at this point, but it can only
|
||||
// result in FAIL.
|
||||
return "failed";
|
||||
} else if (events.length == 0 || (_.last(events).type != "finish")) {
|
||||
} else if (events.length == 0 || (events[events.length - 1].type != "finish")) {
|
||||
return "running";
|
||||
} else if (_.any(events, function(e) {
|
||||
} else if (events.some(function(e) {
|
||||
return e.type == "fail" || e.type == "exception"; })) {
|
||||
return "failed";
|
||||
} else {
|
||||
@@ -261,8 +261,8 @@ Template.navBar.helpers({
|
||||
var walk = function (groups) {
|
||||
var total = 0;
|
||||
|
||||
_.each(groups || [], function (group) {
|
||||
_.each(group.tests || [], function (t) {
|
||||
(groups || []).forEach(function (group) {
|
||||
(group.tests || []).forEach(function (t) {
|
||||
total += _testTime(t);
|
||||
});
|
||||
|
||||
@@ -450,14 +450,14 @@ Template.test.helpers({
|
||||
},
|
||||
|
||||
eventsArray: function() {
|
||||
var events = _.filter(this.events, function(e) {
|
||||
return e.type != "finish";
|
||||
var events = this.events.filter(function(e) {
|
||||
return e[type] != "finish";
|
||||
});
|
||||
|
||||
var partitionBy = function(seq, func) {
|
||||
var result = [];
|
||||
var lastValue = {};
|
||||
_.each(seq, function(x) {
|
||||
seq.forEach(function(x) {
|
||||
var newValue = func(x);
|
||||
if (newValue === lastValue) {
|
||||
result[result.length-1].push(x);
|
||||
@@ -470,17 +470,17 @@ Template.test.helpers({
|
||||
};
|
||||
|
||||
var dupLists = partitionBy(
|
||||
_.map(events, function(e) {
|
||||
events.map(function(e) {
|
||||
// XXX XXX We need something better than stringify!
|
||||
// stringify([undefined]) === "[null]"
|
||||
e = _.clone(e);
|
||||
e = Object.assign({}, e);
|
||||
delete e.sequence;
|
||||
return {obj: e, str: JSON.stringify(e)};
|
||||
}), function(x) { return x.str; });
|
||||
|
||||
return _.map(dupLists, function(L) {
|
||||
return dupLists.map(function(L) {
|
||||
var obj = L[0].obj;
|
||||
return (L.length > 1) ? _.extend({times: L.length}, obj) : obj;
|
||||
return (L.length > 1) ? Object.assign({times: L.length}, obj) : obj;
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -525,7 +525,7 @@ Template.event.helpers({
|
||||
var type = details.type;
|
||||
var stack = details.stack;
|
||||
|
||||
details = _.clone(details);
|
||||
details = Array.isArray(details) && [...details] || Object.assign({}, details);
|
||||
delete details.type;
|
||||
delete details.stack;
|
||||
|
||||
@@ -535,14 +535,14 @@ Template.event.helpers({
|
||||
details.expected);
|
||||
}
|
||||
|
||||
return _.compact(_.map(details, function(val, key) {
|
||||
return Object.entries(details).map(function([key, val]) {
|
||||
|
||||
// make test._stringEqual results print nicely,
|
||||
// in particular for multiline strings
|
||||
if (type === 'string_equal' &&
|
||||
(key === 'actual' || key === 'expected')) {
|
||||
var html = '<pre class="string_equal string_equal_'+key+'">';
|
||||
_.each(diff, function (piece) {
|
||||
diff.forEach(function (piece) {
|
||||
var which = piece[0];
|
||||
var text = piece[1];
|
||||
if (which === 0 ||
|
||||
@@ -561,7 +561,7 @@ Template.event.helpers({
|
||||
// You can end up with a an undefined value, e.g. using
|
||||
// isNull without providing a message attribute: isNull(1).
|
||||
// No need to display those.
|
||||
if (!_.isUndefined(val)) {
|
||||
if (typeof val !== 'undefined') {
|
||||
return {
|
||||
key: key,
|
||||
val: val
|
||||
@@ -569,7 +569,7 @@ Template.event.helpers({
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}));
|
||||
}).filter(Boolean);
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -583,4 +583,4 @@ Template.event.helpers({
|
||||
is_debuggable: function() {
|
||||
return !!this.cookie;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -13,7 +13,6 @@ Package.onUse(function (api) {
|
||||
// XXX this should go away, and there should be a clean interface
|
||||
// that tinytest and the driver both implement?
|
||||
api.use('tinytest');
|
||||
api.use('underscore');
|
||||
|
||||
api.use('session');
|
||||
api.use('reload');
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
const Future = Meteor.isServer && require('fibers/future');
|
||||
|
||||
/******************************************************************************/
|
||||
/* TestCaseResults */
|
||||
/******************************************************************************/
|
||||
@@ -186,6 +184,43 @@ export class TestCaseResults {
|
||||
this.ok();
|
||||
}
|
||||
|
||||
_assertActual(actual, predicate, message) {
|
||||
if (actual && predicate(actual))
|
||||
this.ok();
|
||||
else
|
||||
this.fail({
|
||||
type: "throws",
|
||||
message: (actual ?
|
||||
"wrong error thrown: " + actual.message :
|
||||
"did not throw an error as expected") + (message ? ": " + message : ""),
|
||||
});
|
||||
}
|
||||
|
||||
_guessPredicate(expected) {
|
||||
let predicate;
|
||||
|
||||
if (expected === undefined) {
|
||||
predicate = function () {
|
||||
return true;
|
||||
};
|
||||
} else if (typeof expected === "string") {
|
||||
predicate = function (actual) {
|
||||
return typeof actual.message === "string" &&
|
||||
actual.message.indexOf(expected) !== -1;
|
||||
};
|
||||
} else if (expected instanceof RegExp) {
|
||||
predicate = function (actual) {
|
||||
return expected.test(actual.message);
|
||||
};
|
||||
} else if (typeof expected === 'function') {
|
||||
predicate = expected;
|
||||
} else {
|
||||
throw new Error('expected should be a string, regexp, or predicate function');
|
||||
}
|
||||
|
||||
return predicate;
|
||||
}
|
||||
|
||||
// expected can be:
|
||||
// undefined: accept any exception.
|
||||
// string: pass if the string is a substring of the exception message.
|
||||
@@ -204,26 +239,8 @@ export class TestCaseResults {
|
||||
// particular class, use a predicate function.
|
||||
//
|
||||
throws(f, expected, message) {
|
||||
var actual, predicate;
|
||||
|
||||
if (expected === undefined) {
|
||||
predicate = function (actual) {
|
||||
return true;
|
||||
};
|
||||
} else if (typeof expected === "string") {
|
||||
predicate = function (actual) {
|
||||
return typeof actual.message === "string" &&
|
||||
actual.message.indexOf(expected) !== -1;
|
||||
};
|
||||
} else if (expected instanceof RegExp) {
|
||||
predicate = function (actual) {
|
||||
return expected.test(actual.message);
|
||||
};
|
||||
} else if (typeof expected === 'function') {
|
||||
predicate = expected;
|
||||
} else {
|
||||
throw new Error('expected should be a string, regexp, or predicate function');
|
||||
}
|
||||
let actual;
|
||||
const predicate = this._guessPredicate(expected);
|
||||
|
||||
try {
|
||||
f();
|
||||
@@ -231,15 +248,27 @@ export class TestCaseResults {
|
||||
actual = exception;
|
||||
}
|
||||
|
||||
if (actual && predicate(actual))
|
||||
this.ok();
|
||||
else
|
||||
this.fail({
|
||||
type: "throws",
|
||||
message: (actual ?
|
||||
"wrong error thrown: " + actual.message :
|
||||
"did not throw an error as expected") + (message ? ": " + message : ""),
|
||||
});
|
||||
this._assertActual(actual, predicate, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as throw, but accepts an async function as a parameter.
|
||||
* @param f
|
||||
* @param expected
|
||||
* @param message
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async throwsAsync(f, expected, message) {
|
||||
let actual;
|
||||
const predicate = this._guessPredicate(expected);
|
||||
|
||||
try {
|
||||
await f();
|
||||
} catch (exception) {
|
||||
actual = exception;
|
||||
}
|
||||
|
||||
this._assertActual(actual, predicate, message);
|
||||
}
|
||||
|
||||
isTrue(v, msg) {
|
||||
@@ -309,7 +338,7 @@ export class TestCaseResults {
|
||||
pass = true;
|
||||
}
|
||||
} else {
|
||||
/* fail -- not something that contains other things */;
|
||||
/* fail -- not something that contains other things */
|
||||
}
|
||||
|
||||
if (pass === ! not) {
|
||||
@@ -546,37 +575,37 @@ export class TestRun {
|
||||
}
|
||||
|
||||
if (Meteor.isServer) {
|
||||
// On the server, ensure that only one test runs at a time, even
|
||||
// with multiple clients.
|
||||
this.manager.testQueue.queueTask(() => {
|
||||
// The future resolves when the test completes or times out.
|
||||
var future = new Future();
|
||||
Meteor.setTimeout(
|
||||
() => {
|
||||
if (future.isResolved())
|
||||
// If the future has resolved the test has completed.
|
||||
return;
|
||||
test.timedOut = true;
|
||||
this._report(test, {
|
||||
type: "exception",
|
||||
details: {
|
||||
message: "test timed out"
|
||||
}
|
||||
});
|
||||
future['return']();
|
||||
},
|
||||
3 * 60 * 1000 // 3 minutes
|
||||
);
|
||||
this._runTest(test, () => {
|
||||
// The test can complete after it has timed out (it might
|
||||
// just be slow), so only resolve the future if the test
|
||||
// hasn't timed out.
|
||||
if (! future.isResolved())
|
||||
future['return']();
|
||||
}, stop_at_offset);
|
||||
// Wait for the test to complete or time out.
|
||||
future.wait();
|
||||
onComplete && onComplete();
|
||||
// On the server, ensure that only one test runs at a time, even
|
||||
// with multiple clients.
|
||||
let hasRan = false;
|
||||
const timeoutPromise = new Promise((resolve) => {
|
||||
Meteor.setTimeout(() => {
|
||||
if (!hasRan) {
|
||||
test.timedOut = true;
|
||||
this._report(test, {
|
||||
type: "exception",
|
||||
details: {
|
||||
message: "test timed out"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
resolve();
|
||||
}, 3 * 60 * 1000);
|
||||
});
|
||||
const runnerPromise = new Promise((resolve) => {
|
||||
this._runTest(test, () => {
|
||||
if (!hasRan) {
|
||||
hasRan = true;
|
||||
}
|
||||
resolve();
|
||||
}, stop_at_offset);
|
||||
});
|
||||
|
||||
Promise.race([runnerPromise, timeoutPromise]).finally(() => {
|
||||
onComplete && onComplete();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// client
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
|
||||
export { Tinytest };
|
||||
|
||||
const Fiber = require('fibers');
|
||||
const Fiber = Meteor._isFibersEnabled && require('fibers');
|
||||
const handlesForRun = new Map;
|
||||
const reportsForRun = new Map;
|
||||
|
||||
@@ -58,7 +58,7 @@ Meteor.methods({
|
||||
}
|
||||
|
||||
function onReport(report) {
|
||||
if (! Fiber.current) {
|
||||
if (Fiber && !Fiber.current) {
|
||||
Meteor._debug("Trying to report a test not in a fiber! "+
|
||||
"You probably forgot to wrap a callback in bindEnvironment.");
|
||||
console.trace();
|
||||
|
||||
@@ -7,7 +7,6 @@ Package.onUse(function(api) {
|
||||
api.use('oauth1', ['client', 'server']);
|
||||
api.use('oauth', ['client', 'server']);
|
||||
api.use('random', 'client');
|
||||
api.use('underscore', 'server');
|
||||
api.use('service-configuration', ['client', 'server']);
|
||||
|
||||
api.addFiles('twitter_common.js', ['server', 'client']);
|
||||
|
||||
@@ -15,9 +15,9 @@ var urls = {
|
||||
// https://dev.twitter.com/docs/api/1.1/get/account/verify_credentials
|
||||
Twitter.whitelistedFields = ['profile_image_url', 'profile_image_url_https', 'lang', 'email'];
|
||||
|
||||
OAuth.registerService('twitter', 1, urls, function(oauthBinding) {
|
||||
var identity = oauthBinding.get('https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true').data;
|
||||
|
||||
OAuth.registerService('twitter', 1, urls, async function(oauthBinding) {
|
||||
const response = await oauthBinding.getAsync('https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true');
|
||||
const { data: identity } = response;
|
||||
var serviceData = {
|
||||
id: identity.id_str,
|
||||
screenName: identity.screen_name,
|
||||
@@ -26,8 +26,8 @@ OAuth.registerService('twitter', 1, urls, function(oauthBinding) {
|
||||
};
|
||||
|
||||
// include helpful fields from twitter
|
||||
var fields = _.pick(identity, Twitter.whitelistedFields);
|
||||
_.extend(serviceData, fields);
|
||||
const { identity: fields } = Twitter.whitelistedFields;
|
||||
Object.assign(serviceData, fields);
|
||||
|
||||
return {
|
||||
serviceData: serviceData,
|
||||
|
||||
@@ -5,7 +5,6 @@ Package.describe({
|
||||
|
||||
Package.onUse(function(api) {
|
||||
api.use('ecmascript');
|
||||
api.use('underscore', 'server');
|
||||
api.addFiles('webapp-hashing.js', 'server');
|
||||
api.export('WebAppHashing');
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
var crypto = Npm.require("crypto");
|
||||
import { createHash } from "crypto";
|
||||
|
||||
WebAppHashing = {};
|
||||
|
||||
@@ -13,13 +13,11 @@ WebAppHashing = {};
|
||||
|
||||
WebAppHashing.calculateClientHash =
|
||||
function (manifest, includeFilter, runtimeConfigOverride) {
|
||||
var hash = crypto.createHash('sha1');
|
||||
var hash = createHash('sha1');
|
||||
|
||||
// Omit the old hashed client values in the new hash. These may be
|
||||
// modified in the new boilerplate.
|
||||
var runtimeCfg = _.omit(__meteor_runtime_config__,
|
||||
['autoupdateVersion', 'autoupdateVersionRefreshable',
|
||||
'autoupdateVersionCordova']);
|
||||
var { autoupdateVersion, autoupdateVersionRefreshable, autoupdateVersionCordova, ...runtimeCfg } = __meteor_runtime_config__;
|
||||
|
||||
if (runtimeConfigOverride) {
|
||||
runtimeCfg = runtimeConfigOverride;
|
||||
@@ -27,7 +25,7 @@ WebAppHashing.calculateClientHash =
|
||||
|
||||
hash.update(JSON.stringify(runtimeCfg, 'utf8'));
|
||||
|
||||
_.each(manifest, function (resource) {
|
||||
manifest.forEach(function (resource) {
|
||||
if ((! includeFilter || includeFilter(resource.type, resource.replaceable)) &&
|
||||
(resource.where === 'client' || resource.where === 'internal')) {
|
||||
hash.update(resource.path);
|
||||
@@ -39,7 +37,7 @@ WebAppHashing.calculateClientHash =
|
||||
|
||||
WebAppHashing.calculateCordovaCompatibilityHash =
|
||||
function(platformVersion, pluginVersions) {
|
||||
const hash = crypto.createHash('sha1');
|
||||
const hash = createHash('sha1');
|
||||
|
||||
hash.update(platformVersion);
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ Package.onUse(api => {
|
||||
api.use('oauth1', ['client', 'server']);
|
||||
api.use('oauth', ['client', 'server']);
|
||||
api.use('random', 'client');
|
||||
api.use('http@1.4.4 || 2.0.0', 'server');
|
||||
api.use(['service-configuration', 'ecmascript'], ['client', 'server']);
|
||||
|
||||
api.addFiles('weibo_client.js', 'client');
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
Weibo = {};
|
||||
|
||||
OAuth.registerService('weibo', 2, null, query => {
|
||||
OAuth.registerService('weibo', 2, null, async query => {
|
||||
|
||||
const response = getTokenResponse(query);
|
||||
const response = await getTokenResponse(query);
|
||||
const uid = parseInt(response.uid, 10);
|
||||
|
||||
// different parts of weibo's api seem to expect numbers, or strings
|
||||
@@ -11,7 +11,7 @@ OAuth.registerService('weibo', 2, null, query => {
|
||||
throw new Error(`Expected 'uid' to parse to an integer: ${JSON.stringify(response)}`);
|
||||
}
|
||||
|
||||
const identity = getIdentity(response.access_token, uid);
|
||||
const identity = await getIdentity(response.access_token, uid);
|
||||
|
||||
return {
|
||||
serviceData: {
|
||||
@@ -31,46 +31,48 @@ OAuth.registerService('weibo', 2, null, query => {
|
||||
// - uid
|
||||
// - access_token
|
||||
// - expires_in: lifetime of this token in seconds (5 years(!) right now)
|
||||
const getTokenResponse = query => {
|
||||
const config = ServiceConfiguration.configurations.findOne({service: 'weibo'});
|
||||
if (!config)
|
||||
throw new ServiceConfiguration.ConfigError();
|
||||
const getTokenResponse = async (query) => {
|
||||
const config = ServiceConfiguration.configurations.findOne({
|
||||
service: 'weibo',
|
||||
});
|
||||
if (!config) throw new ServiceConfiguration.ConfigError();
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = HTTP.post(
|
||||
"https://api.weibo.com/oauth2/access_token", {params: {
|
||||
code: query.code,
|
||||
client_id: config.clientId,
|
||||
client_secret: OAuth.openSecret(config.secret),
|
||||
redirect_uri: OAuth._redirectUri('weibo', config, null, {replaceLocalhost: true}),
|
||||
grant_type: 'authorization_code'
|
||||
}});
|
||||
} catch (err) {
|
||||
throw Object.assign(new Error(`Failed to complete OAuth handshake with Weibo. ${err.message}`),
|
||||
{response: err.response});
|
||||
}
|
||||
|
||||
// result.headers["content-type"] is 'text/plain;charset=UTF-8', so
|
||||
// the http package doesn't automatically populate result.data
|
||||
response.data = JSON.parse(response.content);
|
||||
|
||||
if (response.data.error) { // if the http response was a json object with an error attribute
|
||||
throw new Error(`Failed to complete OAuth handshake with Weibo. ${response.data.error}`);
|
||||
} else {
|
||||
return response.data;
|
||||
}
|
||||
return OAuth._fetch('https://api.weibo.com/oauth2/access_token', 'POST', {
|
||||
queryParams: {
|
||||
code: query.code,
|
||||
client_id: config.clientId,
|
||||
client_secret: OAuth.openSecret(config.secret),
|
||||
redirect_uri: OAuth._redirectUri('weibo', config, null, {
|
||||
replaceLocalhost: true,
|
||||
}),
|
||||
grant_type: 'authorization_code',
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.catch((err) => {
|
||||
throw Object.assign(
|
||||
new Error(
|
||||
`Failed to complete OAuth handshake with Weibo. ${err.message}`
|
||||
),
|
||||
{ response: err.response }
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const getIdentity = (accessToken, userId) => {
|
||||
try {
|
||||
return HTTP.get(
|
||||
"https://api.weibo.com/2/users/show.json",
|
||||
{params: {access_token: accessToken, uid: userId}}).data;
|
||||
} catch (err) {
|
||||
throw Object.assign(new Error("Failed to fetch identity from Weibo. " + err.message),
|
||||
{response: err.response});
|
||||
}
|
||||
const getIdentity = async (accessToken, userId) => {
|
||||
return OAuth._fetch('https://api.weibo.com/2/users/show.json', 'GET', {
|
||||
queryParams: {
|
||||
access_token: accessToken,
|
||||
uid: userId,
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.catch((err) => {
|
||||
throw Object.assign(
|
||||
new Error('Failed to fetch identity from Weibo. ' + err.message),
|
||||
{ response: err.response }
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
Weibo.retrieveCredential = (credentialToken, credentialSecret) =>
|
||||
|
||||
@@ -14,7 +14,7 @@ const Selenium = require('./run-selenium.js').Selenium;
|
||||
const AppRunner = require('./run-app.js').AppRunner;
|
||||
const MongoRunner = require('./run-mongo.js').MongoRunner;
|
||||
const HMRServer = require('./run-hmr').HMRServer;
|
||||
const Updater = require('./run-updater.js').Updater;
|
||||
const Updater = require('./run-updater').Updater;
|
||||
|
||||
class Runner {
|
||||
constructor({
|
||||
@@ -123,7 +123,7 @@ class Runner {
|
||||
hmrPath: HMRPath,
|
||||
secret: hmrSecret,
|
||||
projectContext: self.projectContext,
|
||||
cordovaServerPort
|
||||
cordovaServerPort
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -947,7 +947,7 @@ Object.assign(AppRunner.prototype, {
|
||||
var runResult = self._runOnce({
|
||||
onListen: function () {
|
||||
if (! self.noRestartBanner && ! firstRun) {
|
||||
runLog.logRestart();
|
||||
runLog.logRestart(self);
|
||||
Console.enableProgressDisplay(false);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -145,7 +145,7 @@ Object.assign(RunLog.prototype, {
|
||||
self.temporaryMessageLength = msg.length;
|
||||
},
|
||||
|
||||
logRestart: function () {
|
||||
logRestart: function (options) {
|
||||
var self = this;
|
||||
|
||||
if (self.consecutiveRestartMessages) {
|
||||
@@ -159,7 +159,7 @@ Object.assign(RunLog.prototype, {
|
||||
self.consecutiveRestartMessages = 1;
|
||||
}
|
||||
|
||||
var message = "=> Meteor server restarted";
|
||||
var message = "=> Meteor server restarted at: " + options.rootUrl;
|
||||
if (self.consecutiveRestartMessages > 1) {
|
||||
message += " (x" + self.consecutiveRestartMessages + ")";
|
||||
}
|
||||
|
||||
@@ -1,60 +1,52 @@
|
||||
var Console = require('../console/console.js').Console;
|
||||
import { Console } from '../console/console';
|
||||
|
||||
var Updater = function () {
|
||||
var self = this;
|
||||
self.timer = null;
|
||||
};
|
||||
const CHECK_UPDATE_INTERVAL = 3 * 60 * 60 * 1000; // every 3 hours
|
||||
|
||||
// XXX make it take a runLog?
|
||||
// XXX need to deal with updater writing messages (bypassing old
|
||||
// stdout interception.. maybe it should be global after all..)
|
||||
Object.assign(Updater.prototype, {
|
||||
start: function () {
|
||||
var self = this;
|
||||
export class Updater {
|
||||
constructor() {
|
||||
this.timer = null;
|
||||
}
|
||||
|
||||
if (self.timer) {
|
||||
throw new Error("already running?");
|
||||
start() {
|
||||
if (this.timer) {
|
||||
throw new Error('already running?');
|
||||
}
|
||||
|
||||
const self = this;
|
||||
// Check every 3 hours. (Should not share buildmessage state with
|
||||
// the main fiber.)
|
||||
async function check() {
|
||||
self._check();
|
||||
}
|
||||
|
||||
self.timer = setInterval(check, 3 * 60 * 60 * 1000);
|
||||
this.timer = setInterval(check, CHECK_UPDATE_INTERVAL);
|
||||
|
||||
// Also start a check now, but don't block on it. (This should
|
||||
// not share buildmessage state with the main fiber.)
|
||||
check();
|
||||
},
|
||||
}
|
||||
|
||||
_check: function () {
|
||||
var self = this;
|
||||
var updater = require('../packaging/updater.js');
|
||||
_check() {
|
||||
const updater = require('../packaging/updater');
|
||||
try {
|
||||
updater.tryToDownloadUpdate({showBanner: true});
|
||||
updater.tryToDownloadUpdate({ showBanner: true });
|
||||
} catch (e) {
|
||||
// oh well, this was the background. Only show errors if we are in debug
|
||||
// mode.
|
||||
Console.debug("Error inside updater.");
|
||||
Console.debug('Error inside updater.');
|
||||
Console.debug(e.stack);
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
// Returns immediately. However if an update check is currently
|
||||
// running it will complete in the background. Idempotent.
|
||||
stop: function () {
|
||||
var self = this;
|
||||
|
||||
if (self.timer) {
|
||||
return;
|
||||
}
|
||||
clearInterval(self.timer);
|
||||
self.timer = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Returns immediately. However, if an update check is currently
|
||||
// running it will complete in the background. Idempotent.
|
||||
stop() {
|
||||
if (!this.timer) return;
|
||||
|
||||
exports.Updater = Updater;
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
25
tools/tests/server-restart-port.js
Normal file
25
tools/tests/server-restart-port.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import * as selftest from '../tool-testing/selftest';
|
||||
|
||||
selftest.define("server outputs port number on restarting", () => testHelper({
|
||||
path: "server/main.js",
|
||||
id: "server/main.js"
|
||||
}));
|
||||
|
||||
function testHelper(server) {
|
||||
const s = new selftest.Sandbox();
|
||||
s.createApp("myapp", "client-refresh");
|
||||
s.cd("myapp");
|
||||
|
||||
let run = s.run("--port", "21000");
|
||||
run.match("Started proxy");
|
||||
run.waitSecs(15);
|
||||
|
||||
run.match(server.id + " 0");
|
||||
|
||||
s.write(server.path, s.read(server.path).replace(
|
||||
/module.id, (\d+)/,
|
||||
(match, n) => `module.id, ${ ++n }`,
|
||||
));
|
||||
|
||||
run.match("Meteor server restarted at: http://localhost:21000/");
|
||||
}
|
||||
Reference in New Issue
Block a user