revert(api): decouple api from curriculum
This reverts commit c077ffe4b9
via PR #40703
This commit is contained in:
committed by
Mrugesh Mohapatra
parent
454b267138
commit
8f0e441644
@@ -1,214 +0,0 @@
|
||||
/* eslint-disable camelcase */
|
||||
export const mockCancellationHook = {
|
||||
headers: {
|
||||
host: 'a47fb0f4.ngrok.io',
|
||||
accept: '*/*',
|
||||
'paypal-transmission-id': '2e24bc40-61d1-11ea-8ac4-7d4e2605c70c',
|
||||
'paypal-transmission-time': '2020-03-09T06:42:43Z',
|
||||
'paypal-transmission-sig': 'ODCa4gXmfnxkNga1t9p2HTIWFjlTj68P7MhueQd',
|
||||
'paypal-auth-version': 'v2',
|
||||
'paypal-cert-url': 'https://api.sandbox.paypal.com/v1/notifications/certs',
|
||||
'paypal-auth-algo': 'SHA256withRSA',
|
||||
'content-type': 'application/json',
|
||||
'user-agent': 'PayPal/AUHD-214.0-54280748',
|
||||
'correlation-id': 'c3823d4c07ce5',
|
||||
cal_poolstack: 'amqunphttpdeliveryd:UNPHTTPDELIVERY',
|
||||
client_pid: '23853',
|
||||
'content-length': '1706',
|
||||
'x-forwarded-proto': 'https',
|
||||
'x-forwarded-for': '173.0.82.126'
|
||||
},
|
||||
body: {
|
||||
id: 'WH-1VF24938EU372274X-83540367M0110254R',
|
||||
event_version: '1.0',
|
||||
create_time: '2020-03-06T15:34:50.000Z',
|
||||
resource_type: 'subscription',
|
||||
resource_version: '2.0',
|
||||
event_type: 'BILLING.SUBSCRIPTION.CANCELLED',
|
||||
summary: 'Subscription cancelled',
|
||||
resource: {
|
||||
shipping_amount: { currency_code: 'USD', value: '0.0' },
|
||||
start_time: '2020-03-05T08:00:00Z',
|
||||
update_time: '2020-03-09T06:42:09Z',
|
||||
quantity: '1',
|
||||
subscriber: {
|
||||
name: [Object],
|
||||
email_address: 'sb-zdry81054163@personal.example.com',
|
||||
payer_id: '82PVXVLDAU3E8',
|
||||
shipping_address: [Object]
|
||||
},
|
||||
billing_info: {
|
||||
outstanding_balance: [Object],
|
||||
cycle_executions: [Array],
|
||||
last_payment: [Object],
|
||||
next_billing_time: '2020-04-05T10:00:00Z',
|
||||
failed_payments_count: 0
|
||||
},
|
||||
create_time: '2020-03-06T07:34:50Z',
|
||||
links: [[Object]],
|
||||
id: 'I-BA1ATBNF8T3P',
|
||||
plan_id: 'P-6VP46874PR423771HLZDKFBA',
|
||||
status: 'CANCELLED',
|
||||
status_update_time: '2020-03-09T06:42:09Z'
|
||||
},
|
||||
links: [
|
||||
{
|
||||
href:
|
||||
'https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-1VF24938EU372274X-83540367M0110254R',
|
||||
rel: 'self',
|
||||
method: 'GET'
|
||||
},
|
||||
{
|
||||
href:
|
||||
'https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-1VF24938EU372274X-83540367M0110254R/resend',
|
||||
rel: 'resend',
|
||||
method: 'POST'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
export const mockActivationHook = {
|
||||
headers: {
|
||||
host: 'a47fb0f4.ngrok.io',
|
||||
accept: '*/*',
|
||||
'paypal-transmission-id': '22103660-5f7d-11ea-8ac4-7d4e2605c70c',
|
||||
'paypal-transmission-time': '2020-03-06T07:36:03Z',
|
||||
'paypal-transmission-sig':
|
||||
'a;sldfn;lqwjhepjtn12l3n5123mnpu1i-sc-_+++dsflqenwpk1n234uthmsqwr123',
|
||||
'paypal-auth-version': 'v2',
|
||||
'paypal-cert-url':
|
||||
'https://api.sandbox.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-1d93a270',
|
||||
'paypal-auth-algo': 'SHASHASHA',
|
||||
'content-type': 'application/json',
|
||||
'user-agent': 'PayPal/AUHD-214.0-54280748',
|
||||
'correlation-id': 'e0b25772e11af',
|
||||
client_pid: '14973',
|
||||
'content-length': '2201',
|
||||
'x-forwarded-proto': 'https',
|
||||
'x-forwarded-for': '173.0.82.126'
|
||||
},
|
||||
body: {
|
||||
id: 'WH-77687562XN25889J8-8Y6T55435R66168T6',
|
||||
create_time: '2018-19-12T22:20:32.000Z',
|
||||
resource_type: 'subscription',
|
||||
event_type: 'BILLING.SUBSCRIPTION.ACTIVATED',
|
||||
summary: 'A billing agreement was activated.',
|
||||
resource: {
|
||||
quantity: '20',
|
||||
subscriber: {
|
||||
name: {
|
||||
given_name: 'John',
|
||||
surname: 'Doe'
|
||||
},
|
||||
email_address: 'donor@freecodecamp.com',
|
||||
shipping_address: {
|
||||
name: {
|
||||
full_name: 'John Doe'
|
||||
},
|
||||
address: {
|
||||
address_line_1: '2211 N First Street',
|
||||
address_line_2: 'Building 17',
|
||||
admin_area_2: 'San Jose',
|
||||
admin_area_1: 'CA',
|
||||
postal_code: '95131',
|
||||
country_code: 'US'
|
||||
}
|
||||
}
|
||||
},
|
||||
create_time: '2018-12-10T21:20:49Z',
|
||||
shipping_amount: {
|
||||
currency_code: 'USD',
|
||||
value: '10.00'
|
||||
},
|
||||
start_time: '2018-11-01T00:00:00Z',
|
||||
update_time: '2018-12-10T21:20:49Z',
|
||||
billing_info: {
|
||||
outstanding_balance: {
|
||||
currency_code: 'USD',
|
||||
value: '10.00'
|
||||
},
|
||||
cycle_executions: [
|
||||
{
|
||||
tenure_type: 'TRIAL',
|
||||
sequence: 1,
|
||||
cycles_completed: 1,
|
||||
cycles_remaining: 0,
|
||||
current_pricing_scheme_version: 1
|
||||
},
|
||||
{
|
||||
tenure_type: 'REGULAR',
|
||||
sequence: 2,
|
||||
cycles_completed: 1,
|
||||
cycles_remaining: 0,
|
||||
current_pricing_scheme_version: 2
|
||||
}
|
||||
],
|
||||
last_payment: {
|
||||
amount: {
|
||||
currency_code: 'USD',
|
||||
value: '500.00'
|
||||
},
|
||||
time: '2018-12-01T01:20:49Z'
|
||||
},
|
||||
next_billing_time: '2019-01-01T00:20:49Z',
|
||||
final_payment_time: '2020-01-01T00:20:49Z',
|
||||
failed_payments_count: 2
|
||||
},
|
||||
links: [
|
||||
{
|
||||
href:
|
||||
'https://api.paypal.com/v1/billing/subscriptions/I-BW452GLLEP1G',
|
||||
rel: 'self',
|
||||
method: 'GET'
|
||||
},
|
||||
{
|
||||
href:
|
||||
'https://api.paypal.com/v1/billing/subscriptions/I-BW452GLLEP1G',
|
||||
rel: 'edit',
|
||||
method: 'PATCH'
|
||||
},
|
||||
{
|
||||
href:
|
||||
'https://api.paypal.com/v1/billing/subscriptions/I-BW452GLLEP1G/suspend',
|
||||
rel: 'suspend',
|
||||
method: 'POST'
|
||||
},
|
||||
{
|
||||
href:
|
||||
'https://api.paypal.com/v1/billing/subscriptions/I-BW452GLLEP1G/cancel',
|
||||
rel: 'cancel',
|
||||
method: 'POST'
|
||||
},
|
||||
{
|
||||
href:
|
||||
'https://api.paypal.com/v1/billing/subscriptions/I-BW452GLLEP1G/capture',
|
||||
rel: 'capture',
|
||||
method: 'POST'
|
||||
}
|
||||
],
|
||||
id: 'I-BW452GLLEP1G',
|
||||
plan_id: 'P-5ML4271244454362WXNWU5NQ',
|
||||
auto_renewal: true,
|
||||
status: 'ACTIVE',
|
||||
status_update_time: '2018-12-10T21:20:49Z'
|
||||
},
|
||||
links: [
|
||||
{
|
||||
href:
|
||||
'https://api.paypal.com/v1/notifications/webhooks-events/WH-77687562XN25889J8-8Y6T55435R66168T6',
|
||||
rel: 'self',
|
||||
method: 'GET',
|
||||
encType: 'application/json'
|
||||
},
|
||||
{
|
||||
href:
|
||||
'https://api.paypal.com/v1/notifications/webhooks-events/WH-77687562XN25889J8-8Y6T55435R66168T6/resend',
|
||||
rel: 'resend',
|
||||
method: 'POST',
|
||||
encType: 'application/json'
|
||||
}
|
||||
],
|
||||
event_version: '1.0',
|
||||
resource_version: '2.0'
|
||||
}
|
||||
};
|
@@ -1,93 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import debug from 'debug';
|
||||
import dedent from 'dedent';
|
||||
import fs from 'fs';
|
||||
import { google } from 'googleapis';
|
||||
import { Observable } from 'rx';
|
||||
|
||||
import { timeCache, observeMethod } from './rx';
|
||||
|
||||
// one million!
|
||||
const upperBound = 1000 * 1000;
|
||||
const scope = 'https://www.googleapis.com/auth/analytics.readonly';
|
||||
const pathToCred = process.env.GOOGLE_APPLICATION_CREDENTIALS;
|
||||
|
||||
const log = debug('fcc:server:utils:about');
|
||||
const analytics = google.analytics('v3');
|
||||
const makeRequest = observeMethod(analytics.data.realtime, 'get');
|
||||
export const toBoundInt = _.flow(
|
||||
// first convert string to integer
|
||||
_.toInteger,
|
||||
// then we bound the integer to prevent weird things like Infinity
|
||||
// and negative numbers
|
||||
// can't wait to the day we need to update this!
|
||||
_.partialRight(_.clamp, 0, upperBound)
|
||||
);
|
||||
|
||||
export function createActiveUsers() {
|
||||
const zero = Observable.of(0);
|
||||
let credentials;
|
||||
if (!pathToCred) {
|
||||
// if no path to credentials set to zero;
|
||||
log(dedent`
|
||||
no google applications credentials environmental variable found
|
||||
'GOOGLE_APPLICATION_CREDENTIALS'
|
||||
'activeUser' api will always return 0
|
||||
this can safely be ignored during development
|
||||
`);
|
||||
return zero;
|
||||
}
|
||||
try {
|
||||
credentials = require(fs.realpathSync(pathToCred));
|
||||
} catch (err) {
|
||||
log('google applications credentials file failed to require');
|
||||
console.error(err);
|
||||
// if we can't require credentials set to zero;
|
||||
return zero;
|
||||
}
|
||||
if (
|
||||
!credentials.private_key ||
|
||||
!credentials.client_email ||
|
||||
!credentials.viewId
|
||||
) {
|
||||
log(dedent`
|
||||
google applications credentials json should have a
|
||||
* private_key
|
||||
* client_email
|
||||
* viewId
|
||||
but none were found
|
||||
`);
|
||||
return zero;
|
||||
}
|
||||
|
||||
const client = new google.auth.JWT(
|
||||
credentials['client_email'],
|
||||
null,
|
||||
credentials['private_key'],
|
||||
[scope]
|
||||
);
|
||||
const authorize = observeMethod(client, 'authorize');
|
||||
const options = {
|
||||
ids: `ga:${credentials.viewId}`,
|
||||
auth: client,
|
||||
metrics: 'rt:activeUsers'
|
||||
};
|
||||
return Observable.defer(
|
||||
// we wait for authorize to complete before attempting to make request
|
||||
// this ensures our token is initialized and valid
|
||||
// we defer here to make sure the actual request is done per subscription
|
||||
// instead of once at startup
|
||||
() => authorize().flatMap(() => makeRequest(options))
|
||||
)
|
||||
// data: Array[body|Object, request: Request]
|
||||
.map(data => data[0])
|
||||
.map(
|
||||
({ totalsForAllResults } = {}) => totalsForAllResults['rt:activeUsers']
|
||||
)
|
||||
.map(toBoundInt)
|
||||
// print errors to error log for logging, duh
|
||||
.do(null, err => console.error(err))
|
||||
// always send a number down
|
||||
.catch(() => Observable.of(0))
|
||||
::timeCache(2, 'seconds');
|
||||
}
|
@@ -1,50 +0,0 @@
|
||||
const githubRegex = /github/i;
|
||||
const providerHash = {
|
||||
facebook: ({ id }) => id,
|
||||
github: ({ username }) => username,
|
||||
twitter: ({ username }) => username,
|
||||
linkedin({ _json }) {
|
||||
return (_json && _json.publicProfileUrl) || null;
|
||||
},
|
||||
google: ({ id }) => id
|
||||
};
|
||||
|
||||
export function getUsernameFromProvider(provider, profile) {
|
||||
return typeof providerHash[provider] === 'function'
|
||||
? providerHash[provider](profile)
|
||||
: null;
|
||||
}
|
||||
|
||||
// createProfileAttributes(provider: String, profile: {}) => Object
|
||||
export function createUserUpdatesFromProfile(provider, profile) {
|
||||
if (githubRegex.test(provider)) {
|
||||
return createProfileAttributesFromGithub(profile);
|
||||
}
|
||||
return {
|
||||
[getSocialProvider(provider)]: getUsernameFromProvider(
|
||||
getSocialProvider(provider),
|
||||
profile
|
||||
)
|
||||
};
|
||||
}
|
||||
// createProfileAttributes(profile) => profileUpdate
|
||||
function createProfileAttributesFromGithub(profile) {
|
||||
const {
|
||||
profileUrl: githubProfile,
|
||||
username,
|
||||
_json: { avatar_url: picture, blog: website, location, bio, name } = {}
|
||||
} = profile;
|
||||
return {
|
||||
name,
|
||||
username: username.toLowerCase(),
|
||||
location,
|
||||
bio,
|
||||
website,
|
||||
picture,
|
||||
githubProfile
|
||||
};
|
||||
}
|
||||
|
||||
export function getSocialProvider(provider) {
|
||||
return provider.split('-')[0];
|
||||
}
|
@@ -1,19 +0,0 @@
|
||||
export default {
|
||||
bg9997c9c79feddfaeb9bdef: '56bbb991ad1ed5201cd392ca',
|
||||
bg9995c9c69feddfaeb9bdef: '56bbb991ad1ed5201cd392cb',
|
||||
bg9994c9c69feddfaeb9bdef: '56bbb991ad1ed5201cd392cc',
|
||||
bg9996c9c69feddfaeb9bdef: '56bbb991ad1ed5201cd392cd',
|
||||
bg9997c9c69feddfaeb9bdef: '56bbb991ad1ed5201cd392ce',
|
||||
bg9997c9c89feddfaeb9bdef: '56bbb991ad1ed5201cd392cf',
|
||||
bg9998c9c99feddfaeb9bdef: '56bbb991ad1ed5201cd392d0',
|
||||
bg9999c9c99feddfaeb9bdef: '56bbb991ad1ed5201cd392d1',
|
||||
bg9999c9c99feedfaeb9bdef: '56bbb991ad1ed5201cd392d2',
|
||||
bg9999c9c99fdddfaeb9bdef: '56bbb991ad1ed5201cd392d3',
|
||||
bb000000000000000000001: '56bbb991ad1ed5201cd392d4',
|
||||
bc000000000000000000001: '56bbb991ad1ed5201cd392d5',
|
||||
bb000000000000000000002: '56bbb991ad1ed5201cd392d6',
|
||||
bb000000000000000000003: '56bbb991ad1ed5201cd392d7',
|
||||
bb000000000000000000004: '56bbb991ad1ed5201cd392d8',
|
||||
bb000000000000000000005: '56bbb991ad1ed5201cd392d9',
|
||||
bb000000000000000000006: '56bbb991ad1ed5201cd392da'
|
||||
};
|
@@ -1,11 +0,0 @@
|
||||
import { Observable, helpers } from 'rx';
|
||||
|
||||
export default function castToObservable(maybe) {
|
||||
if (Observable.isObservable(maybe)) {
|
||||
return maybe;
|
||||
}
|
||||
if (helpers.isPromise(maybe)) {
|
||||
return Observable.fromPromise(maybe);
|
||||
}
|
||||
return Observable.of(maybe);
|
||||
}
|
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"frontEnd": "isFrontEndCert",
|
||||
"backEnd": "isBackEndCert",
|
||||
"dataVis": "isDataVisCert",
|
||||
"respWebDesign": "isRespWebDesignCert",
|
||||
"frontEndLibs": "isFrontEndLibsCert",
|
||||
"dataVis2018": "is2018DataVisCert",
|
||||
"jsAlgoDataStruct": "isJsAlgoDataStructCert",
|
||||
"apisMicroservices": "isApisMicroservicesCert",
|
||||
"infosecQa": "isInfosecQaCert",
|
||||
"qaV7": "isQaCertV7",
|
||||
"infosecV7": "isInfosecCertV7",
|
||||
"sciCompPyV7": "isSciCompPyCertV7",
|
||||
"dataAnalysisPyV7": "isDataAnalysisPyCertV7",
|
||||
"machineLearningPyV7": "isMachineLearningPyCertV7",
|
||||
"fullStack": "isFullStackCert"
|
||||
}
|
@@ -1,651 +0,0 @@
|
||||
let alphabet = '';
|
||||
|
||||
for (let i = 0; i < 26; i++) {
|
||||
alphabet = alphabet.concat(String.fromCharCode(97 + i));
|
||||
}
|
||||
|
||||
let blocklist = [
|
||||
...alphabet.split(''),
|
||||
'about',
|
||||
'academic-honesty',
|
||||
'account',
|
||||
'agile',
|
||||
'all-stories',
|
||||
'api',
|
||||
'backend-challenge-completed',
|
||||
'bonfire',
|
||||
'cats.json',
|
||||
'challenge-completed',
|
||||
'challenge',
|
||||
'challenges',
|
||||
'chat',
|
||||
'code-of-conduct',
|
||||
'coding-bootcamp-cost-calculator',
|
||||
'completed-bonfire',
|
||||
'completed-challenge',
|
||||
'completed-field-guide',
|
||||
'completed-zipline-or-basejump',
|
||||
'copyright-policy',
|
||||
'copyright',
|
||||
'deprecated-signin',
|
||||
'donate',
|
||||
'email-signin',
|
||||
'events',
|
||||
'explorer',
|
||||
'external',
|
||||
'field-guide',
|
||||
'forgot',
|
||||
'forum',
|
||||
'get-help',
|
||||
'get-pai',
|
||||
'guide',
|
||||
'how-nonprofit-projects-work',
|
||||
'internal',
|
||||
'jobs-form',
|
||||
'jobs',
|
||||
'learn-to-code',
|
||||
'learn',
|
||||
'login',
|
||||
'logout',
|
||||
'map',
|
||||
'modern-challenge-completed',
|
||||
'news',
|
||||
'nonprofit-project-instructions',
|
||||
'nonprofits-form',
|
||||
'nonprofits',
|
||||
'open-api',
|
||||
'passwordless-change',
|
||||
'pmi-acp-agile-project-managers-form',
|
||||
'pmi-acp-agile-project-managers',
|
||||
'privacy-policy',
|
||||
'privacy',
|
||||
'profile',
|
||||
'project-completed',
|
||||
'reset',
|
||||
'services',
|
||||
'shop',
|
||||
'signin',
|
||||
'signout',
|
||||
'signup',
|
||||
'sitemap.xml',
|
||||
'software-resources-for-nonprofits',
|
||||
'sponsors',
|
||||
'stories',
|
||||
'support',
|
||||
'terms-of-service',
|
||||
'terms',
|
||||
'the-fastest-web-page-on-the-internet',
|
||||
'twitch',
|
||||
'unsubscribe',
|
||||
'unsubscribed',
|
||||
'update-my-portfolio',
|
||||
'update-my-profile-ui',
|
||||
'update-my-projects',
|
||||
'update-my-theme',
|
||||
'update-my-username',
|
||||
'user',
|
||||
'username',
|
||||
'wiki',
|
||||
|
||||
// reserved paths for localizations
|
||||
'afrikaans',
|
||||
'arabic',
|
||||
'bengali',
|
||||
'catalan',
|
||||
'chinese',
|
||||
'czech',
|
||||
'danish',
|
||||
'dutch',
|
||||
'espanol',
|
||||
'finnish',
|
||||
'french',
|
||||
'german',
|
||||
'greek',
|
||||
'hebrew',
|
||||
'hindi',
|
||||
'hungarian',
|
||||
'italian',
|
||||
'japanese',
|
||||
'korean',
|
||||
'norwegian',
|
||||
'polish',
|
||||
'portuguese',
|
||||
'romanian',
|
||||
'russian',
|
||||
'serbian',
|
||||
'spanish',
|
||||
'swahili',
|
||||
'swedish',
|
||||
'turkish',
|
||||
'ukrainian',
|
||||
'vietnamese',
|
||||
|
||||
// some more names from https://github.com/marteinn/The-Big-Username-Blacklist-JS/blob/master/src/list.js
|
||||
'.htaccess',
|
||||
'.htpasswd',
|
||||
'.well-known',
|
||||
'400',
|
||||
'401',
|
||||
'403',
|
||||
'404',
|
||||
'405',
|
||||
'406',
|
||||
'407',
|
||||
'408',
|
||||
'409',
|
||||
'410',
|
||||
'411',
|
||||
'412',
|
||||
'413',
|
||||
'414',
|
||||
'415',
|
||||
'416',
|
||||
'417',
|
||||
'421',
|
||||
'422',
|
||||
'423',
|
||||
'424',
|
||||
'426',
|
||||
'428',
|
||||
'429',
|
||||
'431',
|
||||
'500',
|
||||
'501',
|
||||
'502',
|
||||
'503',
|
||||
'504',
|
||||
'505',
|
||||
'506',
|
||||
'507',
|
||||
'508',
|
||||
'509',
|
||||
'510',
|
||||
'511',
|
||||
'about',
|
||||
'about-us',
|
||||
'abuse',
|
||||
'access',
|
||||
'account',
|
||||
'accounts',
|
||||
'ad',
|
||||
'add',
|
||||
'admin',
|
||||
'administration',
|
||||
'administrator',
|
||||
'ads',
|
||||
'advertise',
|
||||
'advertising',
|
||||
'aes128-ctr',
|
||||
'aes128-gcm',
|
||||
'aes192-ctr',
|
||||
'aes256-ctr',
|
||||
'aes256-gcm',
|
||||
'affiliate',
|
||||
'affiliates',
|
||||
'ajax',
|
||||
'alert',
|
||||
'alerts',
|
||||
'alpha',
|
||||
'amp',
|
||||
'analytics',
|
||||
'api',
|
||||
'app',
|
||||
'apps',
|
||||
'asc',
|
||||
'assets',
|
||||
'atom',
|
||||
'auth',
|
||||
'authentication',
|
||||
'authorize',
|
||||
'autoconfig',
|
||||
'autodiscover',
|
||||
'avatar',
|
||||
'backup',
|
||||
'banner',
|
||||
'banners',
|
||||
'beta',
|
||||
'billing',
|
||||
'billings',
|
||||
'blog',
|
||||
'blogs',
|
||||
'board',
|
||||
'bookmark',
|
||||
'bookmarks',
|
||||
'broadcasthost',
|
||||
'business',
|
||||
'buy',
|
||||
'cache',
|
||||
'calendar',
|
||||
'campaign',
|
||||
'captcha',
|
||||
'careers',
|
||||
'cart',
|
||||
'cas',
|
||||
'categories',
|
||||
'category',
|
||||
'cdn',
|
||||
'cgi',
|
||||
'cgi-bin',
|
||||
'chacha20-poly1305',
|
||||
'change',
|
||||
'channel',
|
||||
'channels',
|
||||
'chart',
|
||||
'chat',
|
||||
'checkout',
|
||||
'clear',
|
||||
'client',
|
||||
'close',
|
||||
'cms',
|
||||
'com',
|
||||
'comment',
|
||||
'comments',
|
||||
'community',
|
||||
'compare',
|
||||
'compose',
|
||||
'config',
|
||||
'connect',
|
||||
'contact',
|
||||
'contest',
|
||||
'cookies',
|
||||
'copy',
|
||||
'copyright',
|
||||
'count',
|
||||
'create',
|
||||
'crossdomain.xml',
|
||||
'css',
|
||||
'curve25519-sha256',
|
||||
'customer',
|
||||
'customers',
|
||||
'customize',
|
||||
'dashboard',
|
||||
'db',
|
||||
'deals',
|
||||
'debug',
|
||||
'delete',
|
||||
'desc',
|
||||
'destroy',
|
||||
'dev',
|
||||
'developer',
|
||||
'developers',
|
||||
'diffie-hellman-group-exchange-sha256',
|
||||
'diffie-hellman-group14-sha1',
|
||||
'disconnect',
|
||||
'discuss',
|
||||
'dns',
|
||||
'dns0',
|
||||
'dns1',
|
||||
'dns2',
|
||||
'dns3',
|
||||
'dns4',
|
||||
'docs',
|
||||
'documentation',
|
||||
'domain',
|
||||
'download',
|
||||
'downloads',
|
||||
'downvote',
|
||||
'draft',
|
||||
'drop',
|
||||
'ecdh-sha2-nistp256',
|
||||
'ecdh-sha2-nistp384',
|
||||
'ecdh-sha2-nistp521',
|
||||
'edit',
|
||||
'editor',
|
||||
'email',
|
||||
'enterprise',
|
||||
'error',
|
||||
'errors',
|
||||
'event',
|
||||
'events',
|
||||
'example',
|
||||
'exception',
|
||||
'exit',
|
||||
'explore',
|
||||
'export',
|
||||
'extensions',
|
||||
'false',
|
||||
'family',
|
||||
'faq',
|
||||
'faqs',
|
||||
'favicon.ico',
|
||||
'features',
|
||||
'feed',
|
||||
'feedback',
|
||||
'feeds',
|
||||
'file',
|
||||
'files',
|
||||
'filter',
|
||||
'follow',
|
||||
'follower',
|
||||
'followers',
|
||||
'following',
|
||||
'fonts',
|
||||
'forgot',
|
||||
'forgot-password',
|
||||
'forgotpassword',
|
||||
'form',
|
||||
'forms',
|
||||
'forum',
|
||||
'forums',
|
||||
'friend',
|
||||
'friends',
|
||||
'ftp',
|
||||
'get',
|
||||
'git',
|
||||
'go',
|
||||
'group',
|
||||
'groups',
|
||||
'guest',
|
||||
'guidelines',
|
||||
'guides',
|
||||
'head',
|
||||
'header',
|
||||
'help',
|
||||
'hide',
|
||||
'hmac-sha',
|
||||
'hmac-sha1',
|
||||
'hmac-sha1-etm',
|
||||
'hmac-sha2-256',
|
||||
'hmac-sha2-256-etm',
|
||||
'hmac-sha2-512',
|
||||
'hmac-sha2-512-etm',
|
||||
'home',
|
||||
'host',
|
||||
'hosting',
|
||||
'hostmaster',
|
||||
'htpasswd',
|
||||
'http',
|
||||
'httpd',
|
||||
'https',
|
||||
'humans.txt',
|
||||
'icons',
|
||||
'images',
|
||||
'imap',
|
||||
'img',
|
||||
'import',
|
||||
'index',
|
||||
'info',
|
||||
'insert',
|
||||
'investors',
|
||||
'invitations',
|
||||
'invite',
|
||||
'invites',
|
||||
'invoice',
|
||||
'is',
|
||||
'isatap',
|
||||
'issues',
|
||||
'it',
|
||||
'jobs',
|
||||
'join',
|
||||
'js',
|
||||
'json',
|
||||
'keybase.txt',
|
||||
'learn',
|
||||
'legal',
|
||||
'license',
|
||||
'licensing',
|
||||
'like',
|
||||
'limit',
|
||||
'live',
|
||||
'load',
|
||||
'local',
|
||||
'localdomain',
|
||||
'localhost',
|
||||
'lock',
|
||||
'login',
|
||||
'logout',
|
||||
'lost-password',
|
||||
'mail',
|
||||
'mail0',
|
||||
'mail1',
|
||||
'mail2',
|
||||
'mail3',
|
||||
'mail4',
|
||||
'mail5',
|
||||
'mail6',
|
||||
'mail7',
|
||||
'mail8',
|
||||
'mail9',
|
||||
'mailer-daemon',
|
||||
'mailerdaemon',
|
||||
'map',
|
||||
'marketing',
|
||||
'marketplace',
|
||||
'master',
|
||||
'me',
|
||||
'media',
|
||||
'member',
|
||||
'members',
|
||||
'message',
|
||||
'messages',
|
||||
'metrics',
|
||||
'mis',
|
||||
'mobile',
|
||||
'moderator',
|
||||
'modify',
|
||||
'more',
|
||||
'mx',
|
||||
'my',
|
||||
'net',
|
||||
'network',
|
||||
'new',
|
||||
'news',
|
||||
'newsletter',
|
||||
'newsletters',
|
||||
'next',
|
||||
'nil',
|
||||
'no-reply',
|
||||
'nobody',
|
||||
'noc',
|
||||
'none',
|
||||
'noreply',
|
||||
'notification',
|
||||
'notifications',
|
||||
'ns',
|
||||
'ns0',
|
||||
'ns1',
|
||||
'ns2',
|
||||
'ns3',
|
||||
'ns4',
|
||||
'ns5',
|
||||
'ns6',
|
||||
'ns7',
|
||||
'ns8',
|
||||
'ns9',
|
||||
'null',
|
||||
'oauth',
|
||||
'oauth2',
|
||||
'offer',
|
||||
'offers',
|
||||
'online',
|
||||
'openid',
|
||||
'order',
|
||||
'orders',
|
||||
'overview',
|
||||
'owner',
|
||||
'page',
|
||||
'pages',
|
||||
'partners',
|
||||
'passwd',
|
||||
'password',
|
||||
'pay',
|
||||
'payment',
|
||||
'payments',
|
||||
'photo',
|
||||
'photos',
|
||||
'pixel',
|
||||
'plans',
|
||||
'plugins',
|
||||
'policies',
|
||||
'policy',
|
||||
'pop',
|
||||
'pop3',
|
||||
'popular',
|
||||
'portfolio',
|
||||
'post',
|
||||
'postfix',
|
||||
'postmaster',
|
||||
'poweruser',
|
||||
'preferences',
|
||||
'premium',
|
||||
'press',
|
||||
'previous',
|
||||
'pricing',
|
||||
'print',
|
||||
'privacy',
|
||||
'privacy-policy',
|
||||
'private',
|
||||
'prod',
|
||||
'product',
|
||||
'production',
|
||||
'profile',
|
||||
'profiles',
|
||||
'project',
|
||||
'projects',
|
||||
'public',
|
||||
'purchase',
|
||||
'put',
|
||||
'quota',
|
||||
'redirect',
|
||||
'reduce',
|
||||
'refund',
|
||||
'refunds',
|
||||
'register',
|
||||
'registration',
|
||||
'remove',
|
||||
'replies',
|
||||
'reply',
|
||||
'report',
|
||||
'request',
|
||||
'request-password',
|
||||
'reset',
|
||||
'reset-password',
|
||||
'response',
|
||||
'return',
|
||||
'returns',
|
||||
'review',
|
||||
'reviews',
|
||||
'robots.txt',
|
||||
'root',
|
||||
'rootuser',
|
||||
'rsa-sha2-2',
|
||||
'rsa-sha2-512',
|
||||
'rss',
|
||||
'rules',
|
||||
'sales',
|
||||
'save',
|
||||
'script',
|
||||
'sdk',
|
||||
'search',
|
||||
'secure',
|
||||
'security',
|
||||
'select',
|
||||
'services',
|
||||
'session',
|
||||
'sessions',
|
||||
'settings',
|
||||
'setup',
|
||||
'share',
|
||||
'shift',
|
||||
'shop',
|
||||
'signin',
|
||||
'signup',
|
||||
'site',
|
||||
'sitemap',
|
||||
'sites',
|
||||
'smtp',
|
||||
'sort',
|
||||
'source',
|
||||
'sql',
|
||||
'ssh',
|
||||
'ssh-rsa',
|
||||
'ssl',
|
||||
'ssladmin',
|
||||
'ssladministrator',
|
||||
'sslwebmaster',
|
||||
'stage',
|
||||
'staging',
|
||||
'stat',
|
||||
'static',
|
||||
'statistics',
|
||||
'stats',
|
||||
'status',
|
||||
'store',
|
||||
'style',
|
||||
'styles',
|
||||
'stylesheet',
|
||||
'stylesheets',
|
||||
'subdomain',
|
||||
'subscribe',
|
||||
'sudo',
|
||||
'super',
|
||||
'superuser',
|
||||
'support',
|
||||
'survey',
|
||||
'sync',
|
||||
'sysadmin',
|
||||
'system',
|
||||
'tablet',
|
||||
'tag',
|
||||
'tags',
|
||||
'team',
|
||||
'telnet',
|
||||
'terms',
|
||||
'terms-of-use',
|
||||
'test',
|
||||
'testimonials',
|
||||
'theme',
|
||||
'themes',
|
||||
'today',
|
||||
'tools',
|
||||
'topic',
|
||||
'topics',
|
||||
'tour',
|
||||
'training',
|
||||
'translate',
|
||||
'translations',
|
||||
'trending',
|
||||
'trial',
|
||||
'true',
|
||||
'umac-128',
|
||||
'umac-128-etm',
|
||||
'umac-64',
|
||||
'umac-64-etm',
|
||||
'undefined',
|
||||
'unfollow',
|
||||
'unlike',
|
||||
'unsubscribe',
|
||||
'update',
|
||||
'upgrade',
|
||||
'usenet',
|
||||
'user',
|
||||
'username',
|
||||
'users',
|
||||
'uucp',
|
||||
'var',
|
||||
'verify',
|
||||
'video',
|
||||
'view',
|
||||
'void',
|
||||
'vote',
|
||||
'webmail',
|
||||
'webmaster',
|
||||
'website',
|
||||
'widget',
|
||||
'widgets',
|
||||
'wiki',
|
||||
'wpad',
|
||||
'write',
|
||||
'www',
|
||||
'www-data',
|
||||
'www1',
|
||||
'www2',
|
||||
'www3',
|
||||
'www4',
|
||||
'you',
|
||||
'yourname',
|
||||
'yourusername',
|
||||
'zlib'
|
||||
];
|
||||
|
||||
export const blocklistedUsernames = [...new Set(blocklist)];
|
@@ -1,6 +0,0 @@
|
||||
export function createCookieConfig(req) {
|
||||
return {
|
||||
signed: !!req.signedCookies,
|
||||
domain: process.env.COOKIE_DOMAIN || 'localhost'
|
||||
};
|
||||
}
|
@@ -1,27 +0,0 @@
|
||||
const _handledError = Symbol('handledError');
|
||||
|
||||
export function isHandledError(err) {
|
||||
return !!err[_handledError];
|
||||
}
|
||||
|
||||
export function unwrapHandledError(err) {
|
||||
return err[_handledError] || {};
|
||||
}
|
||||
|
||||
export function wrapHandledError(
|
||||
err,
|
||||
{ type, message, redirectTo, status = 200 }
|
||||
) {
|
||||
err[_handledError] = { type, message, redirectTo, status };
|
||||
return err;
|
||||
}
|
||||
|
||||
// for use with express-validator error formatter
|
||||
export const createValidatorErrorFormatter = (type, redirectTo) => ({ msg }) =>
|
||||
wrapHandledError(new Error(msg), {
|
||||
type,
|
||||
message: msg,
|
||||
redirectTo,
|
||||
// we default to 400 as these are malformed requests
|
||||
status: 400
|
||||
});
|
@@ -1,18 +0,0 @@
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
// day count between two epochs (inclusive)
|
||||
export function dayCount([head, tail], timezone = 'UTC') {
|
||||
return Math.ceil(
|
||||
moment(
|
||||
moment(head)
|
||||
.tz(timezone)
|
||||
.endOf('day')
|
||||
).diff(
|
||||
moment(tail)
|
||||
.tz(timezone)
|
||||
.startOf('day'),
|
||||
'days',
|
||||
true
|
||||
)
|
||||
);
|
||||
}
|
@@ -1,88 +0,0 @@
|
||||
/* global describe expect it */
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
import { dayCount } from './date-utils';
|
||||
|
||||
const PST = 'America/Los_Angeles';
|
||||
|
||||
describe('date utils', () => {
|
||||
describe('dayCount', () => {
|
||||
it('should return 1 day given epochs of the same day', () => {
|
||||
expect(
|
||||
dayCount([
|
||||
moment.utc('8/3/2015 3:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf()
|
||||
])
|
||||
).toEqual(1);
|
||||
});
|
||||
|
||||
it('should return 1 day given same epochs', () => {
|
||||
expect(
|
||||
dayCount([
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf()
|
||||
])
|
||||
).toEqual(1);
|
||||
});
|
||||
|
||||
it('should return 2 days when there is a 24 hours difference', () => {
|
||||
expect(
|
||||
dayCount([
|
||||
moment.utc('8/4/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf()
|
||||
])
|
||||
).toEqual(2);
|
||||
});
|
||||
|
||||
it(
|
||||
'should return 2 days when the diff is less than 24h but ' +
|
||||
'different in UTC',
|
||||
() => {
|
||||
expect(
|
||||
dayCount([
|
||||
moment.utc('8/4/2015 1:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('8/3/2015 23:00', 'M/D/YYYY H:mm').valueOf()
|
||||
])
|
||||
).toEqual(2);
|
||||
}
|
||||
);
|
||||
|
||||
it(
|
||||
'should return 1 day when the diff is less than 24h ' +
|
||||
'and days are different in UTC, but given PST',
|
||||
() => {
|
||||
expect(
|
||||
dayCount(
|
||||
[
|
||||
moment.utc('8/4/2015 1:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('8/3/2015 23:00', 'M/D/YYYY H:mm').valueOf()
|
||||
],
|
||||
PST
|
||||
)
|
||||
).toEqual(1);
|
||||
}
|
||||
);
|
||||
|
||||
it('should return correct count when there is very big period', () => {
|
||||
expect(
|
||||
dayCount([
|
||||
moment.utc('10/27/2015 1:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('5/12/1982 1:00', 'M/D/YYYY H:mm').valueOf()
|
||||
])
|
||||
).toEqual(12222);
|
||||
});
|
||||
|
||||
it(
|
||||
'should return 2 days when there is a 24 hours difference ' +
|
||||
'between dates given `undefined` timezone',
|
||||
() => {
|
||||
expect(
|
||||
dayCount([
|
||||
moment.utc('8/4/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf()
|
||||
])
|
||||
).toEqual(2);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
@@ -1,173 +0,0 @@
|
||||
/* eslint-disable camelcase */
|
||||
import axios from 'axios';
|
||||
import debug from 'debug';
|
||||
import keys from '../../../../config/secrets';
|
||||
|
||||
const log = debug('fcc:boot:donate');
|
||||
|
||||
const paypalverifyWebhookURL =
|
||||
keys.paypal.verifyWebhookURL ||
|
||||
`https://api.sandbox.paypal.com/v1/notifications/verify-webhook-signature`;
|
||||
const paypalTokenURL =
|
||||
keys.paypal.tokenUrl || `https://api.sandbox.paypal.com/v1/oauth2/token`;
|
||||
|
||||
export async function getAsyncPaypalToken() {
|
||||
const res = await axios.post(paypalTokenURL, null, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
auth: {
|
||||
username: keys.paypal.client,
|
||||
password: keys.paypal.secret
|
||||
},
|
||||
params: {
|
||||
grant_type: 'client_credentials'
|
||||
}
|
||||
});
|
||||
return res.data.access_token;
|
||||
}
|
||||
|
||||
export function capitalizeKeys(object) {
|
||||
Object.keys(object).forEach(function(key) {
|
||||
object[key.toUpperCase()] = object[key];
|
||||
});
|
||||
}
|
||||
|
||||
export async function verifyWebHook(headers, body, token, webhookId) {
|
||||
var webhookEventBody = typeof body === 'string' ? JSON.parse(body) : body;
|
||||
|
||||
capitalizeKeys(headers);
|
||||
|
||||
const payload = {
|
||||
auth_algo: headers['PAYPAL-AUTH-ALGO'],
|
||||
cert_url: headers['PAYPAL-CERT-URL'],
|
||||
transmission_id: headers['PAYPAL-TRANSMISSION-ID'],
|
||||
transmission_sig: headers['PAYPAL-TRANSMISSION-SIG'],
|
||||
transmission_time: headers['PAYPAL-TRANSMISSION-TIME'],
|
||||
webhook_id: webhookId,
|
||||
webhook_event: webhookEventBody
|
||||
};
|
||||
|
||||
const response = await axios.post(paypalverifyWebhookURL, payload, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data.verification_status === 'SUCCESS') {
|
||||
return body;
|
||||
} else {
|
||||
throw {
|
||||
message: `Failed token verification.`,
|
||||
type: 'FailedPaypalTokenVerificationError'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function verifyWebHookType(req) {
|
||||
// check if webhook type for creation
|
||||
const {
|
||||
body: { event_type }
|
||||
} = req;
|
||||
|
||||
if (
|
||||
event_type === 'BILLING.SUBSCRIPTION.ACTIVATED' ||
|
||||
event_type === 'BILLING.SUBSCRIPTION.CANCELLED'
|
||||
)
|
||||
return req;
|
||||
else
|
||||
throw {
|
||||
message: 'Webhook type is not supported',
|
||||
type: 'UnsupportedWebhookType'
|
||||
};
|
||||
}
|
||||
|
||||
export const createAsyncUserDonation = (user, donation) => {
|
||||
log(`Creating donation:${donation.subscriptionId}`);
|
||||
user
|
||||
.createDonation(donation)
|
||||
.toPromise()
|
||||
.catch(err => {
|
||||
throw new Error(err);
|
||||
});
|
||||
};
|
||||
|
||||
export function createDonationObj(body) {
|
||||
const {
|
||||
resource: {
|
||||
id,
|
||||
status_update_time,
|
||||
subscriber: { email_address } = {
|
||||
email_address: null
|
||||
}
|
||||
}
|
||||
} = body;
|
||||
|
||||
let donation = {
|
||||
email: email_address,
|
||||
amount: 500,
|
||||
duration: 'month',
|
||||
provider: 'paypal',
|
||||
subscriptionId: id,
|
||||
customerId: email_address,
|
||||
startDate: new Date(status_update_time).toISOString()
|
||||
};
|
||||
return donation;
|
||||
}
|
||||
|
||||
export function createDonation(body, app) {
|
||||
const { User } = app.models;
|
||||
const {
|
||||
resource: {
|
||||
subscriber: { email_address } = {
|
||||
email_address: null
|
||||
}
|
||||
}
|
||||
} = body;
|
||||
|
||||
let donation = createDonationObj(body);
|
||||
|
||||
let email = email_address;
|
||||
return User.findOne({ where: { email } }, (err, user) => {
|
||||
if (err) throw new Error(err);
|
||||
if (!user) {
|
||||
log(`Creating new user:${email}`);
|
||||
return User.create({ email })
|
||||
.then(user => {
|
||||
createAsyncUserDonation(user, donation);
|
||||
})
|
||||
.catch(err => {
|
||||
throw new Error(err);
|
||||
});
|
||||
}
|
||||
return createAsyncUserDonation(user, donation);
|
||||
});
|
||||
}
|
||||
|
||||
export async function cancelDonation(body, app) {
|
||||
const {
|
||||
resource: { id, status_update_time = new Date(Date.now()).toISOString() }
|
||||
} = body;
|
||||
const { Donation } = app.models;
|
||||
Donation.findOne({ where: { subscriptionId: id } }, (err, donation) => {
|
||||
if (err || !donation) throw Error(err);
|
||||
log(`Updating donation record: ${donation.subscriptionId}`);
|
||||
donation.updateAttributes({
|
||||
endDate: new Date(status_update_time).toISOString()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateUser(body, app) {
|
||||
const { event_type } = body;
|
||||
if (event_type === 'BILLING.SUBSCRIPTION.ACTIVATED') {
|
||||
createDonation(body, app);
|
||||
} else if (event_type === 'BILLING.SUBSCRIPTION.CANCELLED') {
|
||||
cancelDonation(body, app);
|
||||
} else
|
||||
throw {
|
||||
message: 'Webhook type is not supported',
|
||||
type: 'UnsupportedWebhookType'
|
||||
};
|
||||
}
|
@@ -1,163 +0,0 @@
|
||||
/* eslint-disable camelcase */
|
||||
/* global describe it expect */
|
||||
/* global jest*/
|
||||
|
||||
import axios from 'axios';
|
||||
import keys from '../../../../config/secrets';
|
||||
import {
|
||||
getAsyncPaypalToken,
|
||||
verifyWebHook,
|
||||
updateUser,
|
||||
capitalizeKeys,
|
||||
createDonationObj
|
||||
} from './donation';
|
||||
import { mockActivationHook, mockCancellationHook } from './__mocks__/donation';
|
||||
import {
|
||||
mockApp,
|
||||
createDonationMockFn,
|
||||
createUserMockFn,
|
||||
updateDonationAttr,
|
||||
updateUserAttr
|
||||
} from '../boot_tests/fixtures';
|
||||
|
||||
jest.mock('axios');
|
||||
|
||||
const verificationUrl = `https://api.sandbox.paypal.com/v1/notifications/verify-webhook-signature`;
|
||||
const tokenUrl = `https://api.sandbox.paypal.com/v1/oauth2/token`;
|
||||
const {
|
||||
body: activationHookBody,
|
||||
headers: activationHookHeaders
|
||||
} = mockActivationHook;
|
||||
|
||||
describe('donation', () => {
|
||||
describe('getAsyncPaypalToken', () => {
|
||||
it('call paypal api for token ', async () => {
|
||||
const res = {
|
||||
data: {
|
||||
access_token: 'token'
|
||||
}
|
||||
};
|
||||
|
||||
axios.post.mockImplementationOnce(() => Promise.resolve(res));
|
||||
|
||||
await expect(getAsyncPaypalToken()).resolves.toEqual(
|
||||
res.data.access_token
|
||||
);
|
||||
|
||||
expect(axios.post).toHaveBeenCalledTimes(1);
|
||||
expect(axios.post).toHaveBeenCalledWith(tokenUrl, null, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
auth: {
|
||||
username: keys.paypal.client,
|
||||
password: keys.paypal.secret
|
||||
},
|
||||
params: {
|
||||
grant_type: 'client_credentials'
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('verifyWebHook', () => {
|
||||
// normalize headers
|
||||
capitalizeKeys(activationHookHeaders);
|
||||
const mockWebhookId = 'qwdfq;3w12341dfa4';
|
||||
const mockAccessToken = '241231223$!@$#1243';
|
||||
const mockPayLoad = {
|
||||
auth_algo: activationHookHeaders['PAYPAL-AUTH-ALGO'],
|
||||
cert_url: activationHookHeaders['PAYPAL-CERT-URL'],
|
||||
transmission_id: activationHookHeaders['PAYPAL-TRANSMISSION-ID'],
|
||||
transmission_sig: activationHookHeaders['PAYPAL-TRANSMISSION-SIG'],
|
||||
transmission_time: activationHookHeaders['PAYPAL-TRANSMISSION-TIME'],
|
||||
webhook_id: mockWebhookId,
|
||||
webhook_event: activationHookBody
|
||||
};
|
||||
const failedVerificationErr = {
|
||||
message: `Failed token verification.`,
|
||||
type: 'FailedPaypalTokenVerificationError'
|
||||
};
|
||||
const axiosOptions = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${mockAccessToken}`
|
||||
}
|
||||
};
|
||||
const successVerificationResponce = {
|
||||
data: {
|
||||
verification_status: 'SUCCESS'
|
||||
}
|
||||
};
|
||||
const failedVerificationResponce = {
|
||||
data: {
|
||||
verification_status: 'FAILED'
|
||||
}
|
||||
};
|
||||
|
||||
it('calls paypal for Webhook verification', async () => {
|
||||
axios.post.mockImplementationOnce(() =>
|
||||
Promise.resolve(successVerificationResponce)
|
||||
);
|
||||
|
||||
await expect(
|
||||
verifyWebHook(
|
||||
activationHookHeaders,
|
||||
activationHookBody,
|
||||
mockAccessToken,
|
||||
mockWebhookId
|
||||
)
|
||||
).resolves.toEqual(activationHookBody);
|
||||
|
||||
expect(axios.post).toHaveBeenCalledWith(
|
||||
verificationUrl,
|
||||
mockPayLoad,
|
||||
axiosOptions
|
||||
);
|
||||
});
|
||||
it('throws error if verification not successful', async () => {
|
||||
axios.post.mockImplementationOnce(() =>
|
||||
Promise.resolve(failedVerificationResponce)
|
||||
);
|
||||
|
||||
await expect(
|
||||
verifyWebHook(
|
||||
activationHookHeaders,
|
||||
activationHookBody,
|
||||
mockAccessToken,
|
||||
mockWebhookId
|
||||
)
|
||||
).rejects.toEqual(failedVerificationErr);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateUser', () => {
|
||||
it('created a donation when a machting user found', () => {
|
||||
updateUser(activationHookBody, mockApp);
|
||||
expect(createDonationMockFn).toHaveBeenCalledTimes(1);
|
||||
expect(createDonationMockFn).toHaveBeenCalledWith(
|
||||
createDonationObj(activationHookBody)
|
||||
);
|
||||
});
|
||||
it('create a user and donation when no machting user found', () => {
|
||||
let newActivationHookBody = activationHookBody;
|
||||
newActivationHookBody.resource.subscriber.email_address =
|
||||
'new@freecodecamp.org';
|
||||
updateUser(newActivationHookBody, mockApp);
|
||||
expect(createUserMockFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('modify user and donation records on cancellation', () => {
|
||||
const { body: cancellationHookBody } = mockCancellationHook;
|
||||
const {
|
||||
resource: { status_update_time = new Date(Date.now()).toISOString() }
|
||||
} = cancellationHookBody;
|
||||
|
||||
updateUser(cancellationHookBody, mockApp);
|
||||
expect(updateDonationAttr).toHaveBeenCalledWith({
|
||||
endDate: new Date(status_update_time).toISOString()
|
||||
});
|
||||
expect(updateUserAttr).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,20 +0,0 @@
|
||||
import { flatten } from 'lodash';
|
||||
|
||||
// TODO: keeping curriculum in memory is handy if we want to field requests that
|
||||
// need to 'query' the curriculum, but if we force the client to handle
|
||||
// redirectToCurrentChallenge and, instead, only report the current challenge id
|
||||
// via the user object, then we should *not* store this so it can be garbage
|
||||
// collected.
|
||||
|
||||
import curriculum from '../../../../config/curriculum.json';
|
||||
|
||||
export function getChallenges() {
|
||||
return Object.keys(curriculum)
|
||||
.map(key => curriculum[key].blocks)
|
||||
.reduce((challengeArray, superBlock) => {
|
||||
const challengesForBlock = Object.keys(superBlock).map(
|
||||
key => superBlock[key].challenges
|
||||
);
|
||||
return [...challengeArray, ...flatten(challengesForBlock)];
|
||||
}, []);
|
||||
}
|
@@ -1,59 +0,0 @@
|
||||
function getCompletedCertCount(user) {
|
||||
return [
|
||||
'isApisMicroservicesCert',
|
||||
'is2018DataVisCert',
|
||||
'isFrontEndLibsCert',
|
||||
'isQaCertV7',
|
||||
'isInfosecCertV7',
|
||||
'isJsAlgoDataStructCert',
|
||||
'isRespWebDesignCert',
|
||||
'isSciCompPyCertV7',
|
||||
'isDataAnalysisPyCertV7',
|
||||
'isMachineLearningPyCertV7'
|
||||
].reduce((sum, key) => (user[key] ? sum + 1 : sum), 0);
|
||||
}
|
||||
|
||||
function getLegacyCertCount(user) {
|
||||
return [
|
||||
'isFrontEndCert',
|
||||
'isBackEndCert',
|
||||
'isDataVisCert',
|
||||
'isInfosecQaCert'
|
||||
].reduce((sum, key) => (user[key] ? sum + 1 : sum), 0);
|
||||
}
|
||||
|
||||
export default function populateUser(db, user) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let populatedUser = { ...user };
|
||||
db.collection('user')
|
||||
.aggregate([
|
||||
{ $match: { _id: user.id } },
|
||||
{ $project: { points: { $size: '$progressTimestamps' } } }
|
||||
])
|
||||
.get(function(err, [{ points = 1 } = {}]) {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
user.points = points;
|
||||
let completedChallengeCount = 0;
|
||||
let completedProjectCount = 0;
|
||||
if ('completedChallenges' in user) {
|
||||
completedChallengeCount = user.completedChallenges.length;
|
||||
user.completedChallenges.forEach(item => {
|
||||
if (
|
||||
'challengeType' in item &&
|
||||
(item.challengeType === 3 || item.challengeType === 4)
|
||||
) {
|
||||
completedProjectCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
populatedUser.completedChallengeCount = completedChallengeCount;
|
||||
populatedUser.completedProjectCount = completedProjectCount;
|
||||
populatedUser.completedCertCount = getCompletedCertCount(user);
|
||||
populatedUser.completedLegacyCertCount = getLegacyCertCount(user);
|
||||
populatedUser.completedChallenges = [];
|
||||
return resolve(populatedUser);
|
||||
});
|
||||
});
|
||||
}
|
@@ -1,74 +0,0 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { isBefore } from 'date-fns';
|
||||
|
||||
import { jwtSecret as _jwtSecret } from '../../../../config/secrets';
|
||||
|
||||
export const authHeaderNS = 'X-fcc-access-token';
|
||||
export const jwtCookieNS = 'jwt_access_token';
|
||||
|
||||
export function createCookieConfig(req) {
|
||||
return {
|
||||
signed: !!req.signedCookies,
|
||||
domain: process.env.COOKIE_DOMAIN || 'localhost'
|
||||
};
|
||||
}
|
||||
|
||||
export function setAccessTokenToResponse(
|
||||
{ accessToken },
|
||||
req,
|
||||
res,
|
||||
jwtSecret = _jwtSecret
|
||||
) {
|
||||
const cookieConfig = {
|
||||
...createCookieConfig(req),
|
||||
maxAge: accessToken.ttl || 77760000000
|
||||
};
|
||||
const jwtAccess = jwt.sign({ accessToken }, jwtSecret);
|
||||
res.cookie(jwtCookieNS, jwtAccess, cookieConfig);
|
||||
return;
|
||||
}
|
||||
|
||||
export function getAccessTokenFromRequest(req, jwtSecret = _jwtSecret) {
|
||||
const maybeToken =
|
||||
(req.headers && req.headers[authHeaderNS]) ||
|
||||
(req.signedCookies && req.signedCookies[jwtCookieNS]) ||
|
||||
(req.cookie && req.cookie[jwtCookieNS]);
|
||||
if (!maybeToken) {
|
||||
return {
|
||||
accessToken: null,
|
||||
error: errorTypes.noTokenFound
|
||||
};
|
||||
}
|
||||
let token;
|
||||
try {
|
||||
token = jwt.verify(maybeToken, jwtSecret);
|
||||
} catch (err) {
|
||||
return { accessToken: null, error: errorTypes.invalidToken };
|
||||
}
|
||||
|
||||
const { accessToken } = token;
|
||||
const { created, ttl } = accessToken;
|
||||
const valid = isBefore(Date.now(), Date.parse(created) + ttl);
|
||||
if (!valid) {
|
||||
return {
|
||||
accessToken: null,
|
||||
error: errorTypes.expiredToken
|
||||
};
|
||||
}
|
||||
return { accessToken, error: '', jwt: maybeToken };
|
||||
}
|
||||
|
||||
export function removeCookies(req, res) {
|
||||
const config = createCookieConfig(req);
|
||||
res.clearCookie(jwtCookieNS, config);
|
||||
res.clearCookie('access_token', config);
|
||||
res.clearCookie('userId', config);
|
||||
res.clearCookie('_csrf', config);
|
||||
return;
|
||||
}
|
||||
|
||||
export const errorTypes = {
|
||||
noTokenFound: 'No token found',
|
||||
invalidToken: 'Invalid token',
|
||||
expiredToken: 'Token timed out'
|
||||
};
|
@@ -1,167 +0,0 @@
|
||||
/* global describe it expect */
|
||||
import {
|
||||
getAccessTokenFromRequest,
|
||||
errorTypes,
|
||||
setAccessTokenToResponse,
|
||||
removeCookies
|
||||
} from './getSetAccessToken';
|
||||
import { mockReq, mockRes } from 'sinon-express-mock';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
describe('getSetAccessToken', () => {
|
||||
const validJWTSecret = 'this is a super secret string';
|
||||
const invalidJWTSecret = 'This is not correct secret';
|
||||
const now = new Date(Date.now());
|
||||
const theBeginningOfTime = new Date(0);
|
||||
const domain = process.env.COOKIE_DOMAIN || 'localhost';
|
||||
const accessToken = {
|
||||
id: '123abc',
|
||||
userId: '456def',
|
||||
ttl: 60000,
|
||||
created: now
|
||||
};
|
||||
|
||||
describe('getAccessTokenFromRequest', () => {
|
||||
it('return `no token` error if no token is found', () => {
|
||||
const req = mockReq({ headers: {}, cookie: {} });
|
||||
const result = getAccessTokenFromRequest(req, validJWTSecret);
|
||||
expect(result.error).toEqual(errorTypes.noTokenFound);
|
||||
});
|
||||
|
||||
describe('cookies', () => {
|
||||
it('returns `invalid token` error for malformed tokens', () => {
|
||||
const invalidJWT = jwt.sign({ accessToken }, invalidJWTSecret);
|
||||
// eslint-disable-next-line camelcase
|
||||
const req = mockReq({ cookie: { jwt_access_token: invalidJWT } });
|
||||
const result = getAccessTokenFromRequest(req, validJWTSecret);
|
||||
|
||||
expect(result.error).toEqual(errorTypes.invalidToken);
|
||||
});
|
||||
|
||||
it('returns `expired token` error for expired tokens', () => {
|
||||
const invalidJWT = jwt.sign(
|
||||
{ accessToken: { ...accessToken, created: theBeginningOfTime } },
|
||||
validJWTSecret
|
||||
);
|
||||
// eslint-disable-next-line camelcase
|
||||
const req = mockReq({ cookie: { jwt_access_token: invalidJWT } });
|
||||
const result = getAccessTokenFromRequest(req, validJWTSecret);
|
||||
|
||||
expect(result.error).toEqual(errorTypes.expiredToken);
|
||||
});
|
||||
|
||||
it('returns a valid access token with no errors ', () => {
|
||||
expect.assertions(2);
|
||||
const validJWT = jwt.sign({ accessToken }, validJWTSecret);
|
||||
// eslint-disable-next-line camelcase
|
||||
const req = mockReq({ cookie: { jwt_access_token: validJWT } });
|
||||
const result = getAccessTokenFromRequest(req, validJWTSecret);
|
||||
|
||||
expect(result.error).toBeFalsy();
|
||||
expect(result.accessToken).toEqual({
|
||||
...accessToken,
|
||||
created: accessToken.created.toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the signed jwt if found', () => {
|
||||
const validJWT = jwt.sign({ accessToken }, validJWTSecret);
|
||||
// eslint-disable-next-line camelcase
|
||||
const req = mockReq({ cookie: { jwt_access_token: validJWT } });
|
||||
const result = getAccessTokenFromRequest(req, validJWTSecret);
|
||||
|
||||
expect(result.jwt).toEqual(validJWT);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Auth headers', () => {
|
||||
it('returns `invalid token` error for malformed tokens', () => {
|
||||
const invalidJWT = jwt.sign({ accessToken }, invalidJWTSecret);
|
||||
// eslint-disable-next-line camelcase
|
||||
const req = mockReq({ headers: { 'X-fcc-access-token': invalidJWT } });
|
||||
const result = getAccessTokenFromRequest(req, validJWTSecret);
|
||||
|
||||
expect(result.error).toEqual(errorTypes.invalidToken);
|
||||
});
|
||||
|
||||
it('returns `expired token` error for expired tokens', () => {
|
||||
const invalidJWT = jwt.sign(
|
||||
{ accessToken: { ...accessToken, created: theBeginningOfTime } },
|
||||
validJWTSecret
|
||||
);
|
||||
// eslint-disable-next-line camelcase
|
||||
const req = mockReq({ headers: { 'X-fcc-access-token': invalidJWT } });
|
||||
const result = getAccessTokenFromRequest(req, validJWTSecret);
|
||||
|
||||
expect(result.error).toEqual(errorTypes.expiredToken);
|
||||
});
|
||||
|
||||
it('returns a valid access token with no errors ', () => {
|
||||
expect.assertions(2);
|
||||
const validJWT = jwt.sign({ accessToken }, validJWTSecret);
|
||||
// eslint-disable-next-line camelcase
|
||||
const req = mockReq({ headers: { 'X-fcc-access-token': validJWT } });
|
||||
const result = getAccessTokenFromRequest(req, validJWTSecret);
|
||||
|
||||
expect(result.error).toBeFalsy();
|
||||
expect(result.accessToken).toEqual({
|
||||
...accessToken,
|
||||
created: accessToken.created.toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the signed jwt if found', () => {
|
||||
const validJWT = jwt.sign({ accessToken }, validJWTSecret);
|
||||
// eslint-disable-next-line camelcase
|
||||
const req = mockReq({ headers: { 'X-fcc-access-token': validJWT } });
|
||||
const result = getAccessTokenFromRequest(req, validJWTSecret);
|
||||
|
||||
expect(result.jwt).toEqual(validJWT);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setAccessTokenToResponse', () => {
|
||||
it('sets a jwt access token cookie in the response', () => {
|
||||
const req = mockReq();
|
||||
const res = mockRes();
|
||||
|
||||
const expectedJWT = jwt.sign({ accessToken }, validJWTSecret);
|
||||
|
||||
setAccessTokenToResponse({ accessToken }, req, res, validJWTSecret);
|
||||
|
||||
expect(res.cookie.getCall(0).args).toEqual([
|
||||
'jwt_access_token',
|
||||
expectedJWT,
|
||||
{
|
||||
signed: false,
|
||||
domain,
|
||||
maxAge: accessToken.ttl
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeCookies', () => {
|
||||
// eslint-disable-next-line max-len
|
||||
it('removes four cookies set in the lifetime of an authenticated session', () => {
|
||||
// expect.assertions(4);
|
||||
const req = mockReq();
|
||||
const res = mockRes();
|
||||
const jwtOptions = { signed: false, domain };
|
||||
|
||||
removeCookies(req, res);
|
||||
|
||||
expect(res.clearCookie.getCall(0).args).toEqual([
|
||||
'jwt_access_token',
|
||||
jwtOptions
|
||||
]);
|
||||
expect(res.clearCookie.getCall(1).args).toEqual([
|
||||
'access_token',
|
||||
jwtOptions
|
||||
]);
|
||||
expect(res.clearCookie.getCall(2).args).toEqual(['userId', jwtOptions]);
|
||||
expect(res.clearCookie.getCall(3).args).toEqual(['_csrf', jwtOptions]);
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,43 +0,0 @@
|
||||
function isPromiseLike(thing) {
|
||||
return !!thing && typeof thing.then === 'function';
|
||||
}
|
||||
|
||||
function InMemoryCache(initialValue, reportError) {
|
||||
if (typeof reportError !== 'function') {
|
||||
throw new Error(
|
||||
'No reportError function specified for this in-memory-cache'
|
||||
);
|
||||
}
|
||||
const cacheKey = Symbol('cacheKey');
|
||||
const cache = new Map();
|
||||
cache.set(cacheKey, initialValue);
|
||||
|
||||
return {
|
||||
get() {
|
||||
const value = cache.get(cacheKey);
|
||||
return typeof value !== 'undefined' ? value : null;
|
||||
},
|
||||
|
||||
update(fn) {
|
||||
try {
|
||||
const value = fn();
|
||||
if (isPromiseLike(value)) {
|
||||
return value.then(value => cache.set(cacheKey, value));
|
||||
} else {
|
||||
cache.set(cacheKey, value);
|
||||
}
|
||||
} catch (e) {
|
||||
const errMsg = `InMemoryCache > update > caught: ${e.message}`;
|
||||
e.message = errMsg;
|
||||
reportError(e);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
clear() {
|
||||
return cache.delete(cacheKey);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default InMemoryCache;
|
@@ -1,68 +0,0 @@
|
||||
/* global describe beforeEach expect it */
|
||||
import inMemoryCache from './in-memory-cache';
|
||||
import sinon from 'sinon';
|
||||
|
||||
describe('InMemoryCache', () => {
|
||||
let reportErrorStub;
|
||||
const theAnswer = 42;
|
||||
const before = 'before';
|
||||
const after = 'after';
|
||||
const emptyCacheValue = null;
|
||||
|
||||
beforeEach(() => {
|
||||
reportErrorStub = sinon.spy();
|
||||
});
|
||||
|
||||
it('throws if no report function is passed as a second argument', () => {
|
||||
expect(() => inMemoryCache(null)).toThrowError(
|
||||
'No reportError function specified for this in-memory-cache'
|
||||
);
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('returns an initial value', () => {
|
||||
const cache = inMemoryCache(theAnswer, reportErrorStub);
|
||||
expect(cache.get()).toBe(theAnswer);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('updates the cached value', () => {
|
||||
const cache = inMemoryCache(before, reportErrorStub);
|
||||
cache.update(() => after);
|
||||
expect(cache.get()).toBe(after);
|
||||
});
|
||||
|
||||
it('can handle promises correctly', done => {
|
||||
const cache = inMemoryCache(before, reportErrorStub);
|
||||
const promisedUpdate = () => new Promise(resolve => resolve(after));
|
||||
cache.update(promisedUpdate).then(() => {
|
||||
expect(cache.get()).toBe(after);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('reports errors thrown from the update function', () => {
|
||||
const reportErrorStub = sinon.spy();
|
||||
const cache = inMemoryCache(before, reportErrorStub);
|
||||
|
||||
const updateError = new Error('An update error');
|
||||
const updateThatThrows = () => {
|
||||
throw updateError;
|
||||
};
|
||||
|
||||
cache.update(updateThatThrows);
|
||||
expect(reportErrorStub.calledWith(updateError)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clear', () => {
|
||||
it('clears the cache', () => {
|
||||
expect.assertions(2);
|
||||
const cache = inMemoryCache(theAnswer, reportErrorStub);
|
||||
expect(cache.get()).toBe(theAnswer);
|
||||
cache.clear();
|
||||
expect(cache.get()).toBe(emptyCacheValue);
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,3 +0,0 @@
|
||||
exports.addPlaceholderImage = function addPlaceholderImage(name) {
|
||||
return `https://example.com/${name}.png`;
|
||||
};
|
@@ -1,4 +0,0 @@
|
||||
export default ['auth', 'services', 'link'].reduce((throughs, route) => {
|
||||
throughs[route] = true;
|
||||
return throughs;
|
||||
}, {});
|
@@ -1,92 +0,0 @@
|
||||
import dedent from 'dedent';
|
||||
import { validationResult } from 'express-validator';
|
||||
|
||||
import { createValidatorErrorFormatter } from './create-handled-error.js';
|
||||
|
||||
import {
|
||||
getAccessTokenFromRequest,
|
||||
removeCookies
|
||||
} from './getSetAccessToken.js';
|
||||
import { getRedirectParams } from './redirection';
|
||||
|
||||
export function ifNoUserRedirectHome(message, type = 'errors') {
|
||||
return function(req, res, next) {
|
||||
const { path } = req;
|
||||
if (req.user) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const { origin } = getRedirectParams(req);
|
||||
req.flash(type, message || `You must be signed in to access ${path}`);
|
||||
|
||||
return res.redirect(origin);
|
||||
};
|
||||
}
|
||||
|
||||
export function ifNoUserSend(sendThis) {
|
||||
return function(req, res, next) {
|
||||
if (req.user) {
|
||||
return next();
|
||||
}
|
||||
return res.status(200).send(sendThis);
|
||||
};
|
||||
}
|
||||
|
||||
export function ifNoUser401(req, res, next) {
|
||||
if (req.user) {
|
||||
return next();
|
||||
}
|
||||
return res.status(401).end();
|
||||
}
|
||||
|
||||
export function ifNotVerifiedRedirectToUpdateEmail(req, res, next) {
|
||||
const { user } = req;
|
||||
if (!user) {
|
||||
return next();
|
||||
}
|
||||
if (!user.emailVerified) {
|
||||
req.flash(
|
||||
'danger',
|
||||
dedent`
|
||||
We do not have your verified email address on record,
|
||||
please add it in the settings to continue with your request.
|
||||
`
|
||||
);
|
||||
return res.redirect('/settings');
|
||||
}
|
||||
return next();
|
||||
}
|
||||
|
||||
export function ifUserRedirectTo(status) {
|
||||
status = status === 301 ? 301 : 302;
|
||||
return (req, res, next) => {
|
||||
const { accessToken } = getAccessTokenFromRequest(req);
|
||||
const { returnTo } = getRedirectParams(req);
|
||||
if (req.user && accessToken) {
|
||||
return res.status(status).redirect(returnTo);
|
||||
}
|
||||
if (req.user && !accessToken) {
|
||||
// This request has an active auth session
|
||||
// but there is no accessToken attached to the request
|
||||
// perhaps the user cleared cookies?
|
||||
// we need to remove the zombie auth session
|
||||
removeCookies(req, res);
|
||||
delete req.session.passport;
|
||||
}
|
||||
return next();
|
||||
};
|
||||
}
|
||||
|
||||
// for use with express-validator error formatter
|
||||
export const createValidatorErrorHandler = (...args) => (req, res, next) => {
|
||||
const validation = validationResult(req).formatWith(
|
||||
createValidatorErrorFormatter(...args)
|
||||
);
|
||||
|
||||
if (!validation.isEmpty()) {
|
||||
const errors = validation.array();
|
||||
return next(errors.pop());
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
@@ -1,87 +0,0 @@
|
||||
import { isURL } from 'validator';
|
||||
|
||||
import { addPlaceholderImage } from './';
|
||||
import {
|
||||
prepUniqueDaysByHours,
|
||||
calcCurrentStreak,
|
||||
calcLongestStreak
|
||||
} from '../utils/user-stats';
|
||||
|
||||
export const publicUserProps = [
|
||||
'about',
|
||||
'calendar',
|
||||
'completedChallenges',
|
||||
'githubProfile',
|
||||
'isApisMicroservicesCert',
|
||||
'isBackEndCert',
|
||||
'isCheater',
|
||||
'isDonating',
|
||||
'is2018DataVisCert',
|
||||
'isDataVisCert',
|
||||
'isFrontEndCert',
|
||||
'isFullStackCert',
|
||||
'isFrontEndLibsCert',
|
||||
'isHonest',
|
||||
'isInfosecQaCert',
|
||||
'isQaCertV7',
|
||||
'isInfosecCertV7',
|
||||
'isJsAlgoDataStructCert',
|
||||
'isRespWebDesignCert',
|
||||
'isSciCompPyCertV7',
|
||||
'isDataAnalysisPyCertV7',
|
||||
'isMachineLearningPyCertV7',
|
||||
'linkedin',
|
||||
'location',
|
||||
'name',
|
||||
'points',
|
||||
'portfolio',
|
||||
'profileUI',
|
||||
'projects',
|
||||
'streak',
|
||||
'twitter',
|
||||
'username',
|
||||
'website',
|
||||
'yearsTopContributor'
|
||||
];
|
||||
|
||||
export const userPropsForSession = [
|
||||
...publicUserProps,
|
||||
'currentChallengeId',
|
||||
'email',
|
||||
'emailVerified',
|
||||
'id',
|
||||
'sendQuincyEmail',
|
||||
'theme',
|
||||
'completedChallengeCount',
|
||||
'completedProjectCount',
|
||||
'completedCertCount',
|
||||
'completedLegacyCertCount',
|
||||
'acceptedPrivacyTerms',
|
||||
'donationEmails'
|
||||
];
|
||||
|
||||
export function normaliseUserFields(user) {
|
||||
const about = user.bio && !user.about ? user.bio : user.about;
|
||||
const picture = user.picture || addPlaceholderImage(user.username);
|
||||
const twitter =
|
||||
user.twitter && isURL(user.twitter)
|
||||
? user.twitter
|
||||
: user.twitter &&
|
||||
`https://www.twitter.com/${user.twitter.replace(/^@/, '')}`;
|
||||
return { about, picture, twitter };
|
||||
}
|
||||
|
||||
export function getProgress(progressTimestamps, timezone = 'EST') {
|
||||
const calendar = progressTimestamps
|
||||
.filter(Boolean)
|
||||
.reduce((data, timestamp) => {
|
||||
data[Math.floor(timestamp / 1000)] = 1;
|
||||
return data;
|
||||
}, {});
|
||||
const uniqueHours = prepUniqueDaysByHours(progressTimestamps, timezone);
|
||||
const streak = {
|
||||
longest: calcLongestStreak(uniqueHours, timezone),
|
||||
current: calcCurrentStreak(uniqueHours, timezone)
|
||||
};
|
||||
return { calendar, streak };
|
||||
}
|
6
api-server/src/server/utils/react.js
vendored
6
api-server/src/server/utils/react.js
vendored
@@ -1,6 +0,0 @@
|
||||
export const errorThrowerMiddleware = () => next => action => {
|
||||
if (action.error) {
|
||||
throw action.payload;
|
||||
}
|
||||
return next(action);
|
||||
};
|
@@ -1,83 +0,0 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { availableLangs } = require('../../../../config/i18n/all-langs');
|
||||
const { allowedOrigins } = require('../../../../config/cors-settings');
|
||||
// homeLocation is being used as a fallback here. If the one provided by the
|
||||
// client is invalid we default to this.
|
||||
const { homeLocation } = require('../../../../config/env');
|
||||
|
||||
function getReturnTo(encryptedParams, secret, _homeLocation = homeLocation) {
|
||||
let params;
|
||||
try {
|
||||
params = jwt.verify(encryptedParams, secret);
|
||||
} catch (e) {
|
||||
// TODO: report to Sentry? Probably not. Remove entirely?
|
||||
console.log(e);
|
||||
// something went wrong, use default params
|
||||
params = {
|
||||
returnTo: `${_homeLocation}/learn`,
|
||||
origin: _homeLocation,
|
||||
pathPrefix: ''
|
||||
};
|
||||
}
|
||||
|
||||
return normalizeParams(params, _homeLocation);
|
||||
}
|
||||
|
||||
function normalizeParams(
|
||||
{ returnTo, origin, pathPrefix },
|
||||
_homeLocation = homeLocation
|
||||
) {
|
||||
// coerce to strings, just in case something weird and nefarious is happening
|
||||
returnTo = '' + returnTo;
|
||||
origin = '' + origin;
|
||||
pathPrefix = '' + pathPrefix;
|
||||
// we add the '/' to prevent returns to
|
||||
// www.freecodecamp.org.somewhere.else.com
|
||||
if (
|
||||
!returnTo ||
|
||||
!allowedOrigins.some(allowed => returnTo.startsWith(allowed + '/'))
|
||||
) {
|
||||
returnTo = `${_homeLocation}/learn`;
|
||||
origin = _homeLocation;
|
||||
pathPrefix = '';
|
||||
}
|
||||
if (!origin || !allowedOrigins.includes(origin)) {
|
||||
returnTo = `${_homeLocation}/learn`;
|
||||
origin = _homeLocation;
|
||||
pathPrefix = '';
|
||||
}
|
||||
pathPrefix = availableLangs.client.includes(pathPrefix) ? pathPrefix : '';
|
||||
return { returnTo, origin, pathPrefix };
|
||||
}
|
||||
|
||||
// TODO: tests!
|
||||
// TODO: ensure origin and pathPrefix validation happens first
|
||||
// (it needs a dedicated function that can be called from here and getReturnTo)
|
||||
function getRedirectBase(origin, pathPrefix) {
|
||||
const redirectPathSegment = pathPrefix ? `/${pathPrefix}` : '';
|
||||
return `${origin}${redirectPathSegment}`;
|
||||
}
|
||||
|
||||
function getRedirectParams(req, _normalizeParams = normalizeParams) {
|
||||
const url = req.header('Referer');
|
||||
// since we do not always redirect the user back to the page they were on
|
||||
// we need client locale and origin to construct the redirect url.
|
||||
const returnUrl = new URL(url ? url : homeLocation);
|
||||
const origin = returnUrl.origin;
|
||||
// if this is not one of the client languages, validation will convert
|
||||
// this to '' before it is used.
|
||||
const pathPrefix = returnUrl.pathname.split('/')[0];
|
||||
return _normalizeParams({ returnTo: returnUrl.href, origin, pathPrefix });
|
||||
}
|
||||
|
||||
function isRootPath(redirectBase, returnUrl) {
|
||||
const base = new URL(redirectBase);
|
||||
const url = new URL(returnUrl);
|
||||
return base.pathname === url.pathname;
|
||||
}
|
||||
|
||||
module.exports.getReturnTo = getReturnTo;
|
||||
module.exports.getRedirectBase = getRedirectBase;
|
||||
module.exports.normalizeParams = normalizeParams;
|
||||
module.exports.getRedirectParams = getRedirectParams;
|
||||
module.exports.isRootPath = isRootPath;
|
@@ -1,120 +0,0 @@
|
||||
/* global describe expect it */
|
||||
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
const { getReturnTo, normalizeParams } = require('./redirection');
|
||||
|
||||
const validJWTSecret = 'this is a super secret string';
|
||||
const invalidJWTSecret = 'This is not correct secret';
|
||||
const validReturnTo = 'https://www.freecodecamp.org/settings';
|
||||
const invalidReturnTo = 'https://www.freecodecamp.org.fake/settings';
|
||||
const defaultReturnTo = 'https://www.freecodecamp.org/learn';
|
||||
const defaultOrigin = 'https://www.freecodecamp.org';
|
||||
const defaultPrefix = '';
|
||||
|
||||
const defaultObject = {
|
||||
returnTo: defaultReturnTo,
|
||||
origin: defaultOrigin,
|
||||
pathPrefix: defaultPrefix
|
||||
};
|
||||
|
||||
describe('redirection', () => {
|
||||
describe('getReturnTo', () => {
|
||||
it('should extract returnTo from a jwt', () => {
|
||||
expect.assertions(1);
|
||||
|
||||
const encryptedReturnTo = jwt.sign(
|
||||
{ returnTo: validReturnTo, origin: defaultOrigin },
|
||||
validJWTSecret
|
||||
);
|
||||
expect(
|
||||
getReturnTo(encryptedReturnTo, validJWTSecret, defaultOrigin)
|
||||
).toStrictEqual({
|
||||
...defaultObject,
|
||||
returnTo: validReturnTo
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a default url if the secrets do not match', () => {
|
||||
expect.assertions(1);
|
||||
|
||||
const encryptedReturnTo = jwt.sign(
|
||||
{ returnTo: validReturnTo },
|
||||
invalidJWTSecret
|
||||
);
|
||||
expect(
|
||||
getReturnTo(encryptedReturnTo, validJWTSecret, defaultOrigin)
|
||||
).toStrictEqual(defaultObject);
|
||||
});
|
||||
|
||||
it('should return a default url for unknown origins', () => {
|
||||
expect.assertions(1);
|
||||
const encryptedReturnTo = jwt.sign(
|
||||
{ returnTo: invalidReturnTo },
|
||||
validJWTSecret
|
||||
);
|
||||
expect(
|
||||
getReturnTo(encryptedReturnTo, validJWTSecret, defaultOrigin)
|
||||
).toStrictEqual(defaultObject);
|
||||
});
|
||||
});
|
||||
describe('normalizeParams', () => {
|
||||
it('should return a {returnTo, origin, pathPrefix} object', () => {
|
||||
expect.assertions(2);
|
||||
const keys = Object.keys(normalizeParams({}));
|
||||
const expectedKeys = ['returnTo', 'origin', 'pathPrefix'];
|
||||
expect(keys.length).toBe(3);
|
||||
expect(keys).toEqual(expect.arrayContaining(expectedKeys));
|
||||
});
|
||||
it('should default to homeLocation', () => {
|
||||
expect.assertions(1);
|
||||
expect(normalizeParams({}, defaultOrigin)).toEqual(defaultObject);
|
||||
});
|
||||
it('should convert an unknown pathPrefix to ""', () => {
|
||||
expect.assertions(1);
|
||||
const brokenPrefix = {
|
||||
...defaultObject,
|
||||
pathPrefix: 'not-really-a-name'
|
||||
};
|
||||
expect(normalizeParams(brokenPrefix, defaultOrigin)).toEqual(
|
||||
defaultObject
|
||||
);
|
||||
});
|
||||
it('should not change a known pathPrefix', () => {
|
||||
expect.assertions(1);
|
||||
const spanishPrefix = {
|
||||
...defaultObject,
|
||||
pathPrefix: 'espanol'
|
||||
};
|
||||
expect(normalizeParams(spanishPrefix, defaultOrigin)).toEqual(
|
||||
spanishPrefix
|
||||
);
|
||||
});
|
||||
// we *could*, in principle, grab the path and send them to
|
||||
// homeLocation/path, but if the origin is wrong something unexpected is
|
||||
// going on. In that case it's probably best to just send them to
|
||||
// homeLocation/learn.
|
||||
it('should return default parameters if the origin is unknown', () => {
|
||||
expect.assertions(1);
|
||||
const exampleOrigin = {
|
||||
...defaultObject,
|
||||
origin: 'http://example.com',
|
||||
pathPrefix: 'espanol'
|
||||
};
|
||||
expect(normalizeParams(exampleOrigin, defaultOrigin)).toEqual(
|
||||
defaultObject
|
||||
);
|
||||
});
|
||||
it('should return default parameters if the returnTo is unknown', () => {
|
||||
expect.assertions(1);
|
||||
const exampleReturnTo = {
|
||||
...defaultObject,
|
||||
returnTo: 'http://example.com/path',
|
||||
pathPrefix: 'espanol'
|
||||
};
|
||||
expect(normalizeParams(exampleReturnTo, defaultOrigin)).toEqual(
|
||||
defaultObject
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,60 +0,0 @@
|
||||
import Rx, { AsyncSubject, Observable } from 'rx';
|
||||
import moment from 'moment';
|
||||
import debugFactory from 'debug';
|
||||
|
||||
const debug = debugFactory('fcc:rxUtils');
|
||||
|
||||
export function saveInstance(instance) {
|
||||
return new Rx.Observable.create(function(observer) {
|
||||
if (!instance || typeof instance.save !== 'function') {
|
||||
debug('no instance or save method');
|
||||
observer.onNext();
|
||||
return observer.onCompleted();
|
||||
}
|
||||
return instance.save(function(err, savedInstance) {
|
||||
if (err) {
|
||||
return observer.onError(err);
|
||||
}
|
||||
debug('instance saved');
|
||||
observer.onNext(savedInstance);
|
||||
return observer.onCompleted();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// alias saveInstance
|
||||
export const saveUser = saveInstance;
|
||||
|
||||
// observeQuery(Model: Object, methodName: String, query: Any) => Observable
|
||||
export function observeQuery(Model, methodName, query) {
|
||||
return Rx.Observable.fromNodeCallback(Model[methodName], Model)(query);
|
||||
}
|
||||
|
||||
// observeMethod(
|
||||
// context: Object, methodName: String
|
||||
// ) => (query: Any) => Observable
|
||||
export function observeMethod(context, methodName) {
|
||||
return Rx.Observable.fromNodeCallback(context[methodName], context);
|
||||
}
|
||||
|
||||
// must be bound to an observable instance
|
||||
// timeCache(amount: Number, unit: String) => Observable
|
||||
export function timeCache(time, unit) {
|
||||
const source = this;
|
||||
let cache;
|
||||
let expireCacheAt;
|
||||
return Observable.create(observable => {
|
||||
// if there is no expire time set
|
||||
// or if expireCacheAt is smaller than now,
|
||||
// set new expire time in MS and create new subscription to source
|
||||
if (!expireCacheAt || expireCacheAt < Date.now()) {
|
||||
// set expire in ms;
|
||||
expireCacheAt = moment()
|
||||
.add(time, unit)
|
||||
.valueOf();
|
||||
cache = new AsyncSubject();
|
||||
source.subscribe(cache);
|
||||
}
|
||||
return cache.subscribe(observable);
|
||||
});
|
||||
}
|
@@ -1,3 +0,0 @@
|
||||
export function getEmailSender() {
|
||||
return process.env.SES_MAIL_FROM || 'team@freecodecamp.org';
|
||||
}
|
@@ -1,162 +0,0 @@
|
||||
import loopback from 'loopback';
|
||||
import compose from 'lodash/fp/compose';
|
||||
import map from 'lodash/fp/map';
|
||||
import sortBy from 'lodash/fp/sortBy';
|
||||
import trans from 'lodash/fp/transform';
|
||||
import last from 'lodash/fp/last';
|
||||
import forEachRight from 'lodash/fp/forEachRight';
|
||||
import { isEmpty } from 'lodash';
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
import { dayCount } from '../utils/date-utils';
|
||||
|
||||
const transform = trans.convert({ cap: false });
|
||||
|
||||
const hoursBetween = 24;
|
||||
const hoursDay = 24;
|
||||
|
||||
export function prepUniqueDaysByHours(cals, tz = 'UTC') {
|
||||
let prev = null;
|
||||
|
||||
// compose goes bottom to top (map > sortBy > transform)
|
||||
return compose(
|
||||
transform((data, cur, i) => {
|
||||
if (i < 1) {
|
||||
data.push(cur);
|
||||
prev = cur;
|
||||
} else if (
|
||||
moment(cur)
|
||||
.tz(tz)
|
||||
.diff(
|
||||
moment(prev)
|
||||
.tz(tz)
|
||||
.startOf('day'),
|
||||
'hours'
|
||||
) >= hoursDay
|
||||
) {
|
||||
data.push(cur);
|
||||
prev = cur;
|
||||
}
|
||||
}, []),
|
||||
sortBy(e => e),
|
||||
map(ts =>
|
||||
moment(ts)
|
||||
.tz(tz)
|
||||
.startOf('hours')
|
||||
.valueOf()
|
||||
)
|
||||
)(cals);
|
||||
}
|
||||
|
||||
export function calcCurrentStreak(cals, tz = 'UTC') {
|
||||
let prev = last(cals);
|
||||
if (
|
||||
moment()
|
||||
.tz(tz)
|
||||
.startOf('day')
|
||||
.diff(moment(prev).tz(tz), 'hours') > hoursBetween
|
||||
) {
|
||||
return 0;
|
||||
}
|
||||
let currentStreak = 0;
|
||||
let streakContinues = true;
|
||||
forEachRight(cur => {
|
||||
if (
|
||||
moment(prev)
|
||||
.tz(tz)
|
||||
.startOf('day')
|
||||
.diff(moment(cur).tz(tz), 'hours') <= hoursBetween
|
||||
) {
|
||||
prev = cur;
|
||||
currentStreak++;
|
||||
} else {
|
||||
// current streak found
|
||||
streakContinues = false;
|
||||
}
|
||||
return streakContinues;
|
||||
})(cals);
|
||||
|
||||
return currentStreak;
|
||||
}
|
||||
|
||||
export function calcLongestStreak(cals, tz = 'UTC') {
|
||||
let tail = cals[0];
|
||||
const longest = cals.reduce(
|
||||
(longest, head, index) => {
|
||||
const last = cals[index === 0 ? 0 : index - 1];
|
||||
// is streak broken
|
||||
if (
|
||||
moment(head)
|
||||
.tz(tz)
|
||||
.startOf('day')
|
||||
.diff(moment(last).tz(tz), 'hours') > hoursBetween
|
||||
) {
|
||||
tail = head;
|
||||
}
|
||||
if (dayCount(longest, tz) < dayCount([head, tail], tz)) {
|
||||
return [head, tail];
|
||||
}
|
||||
return longest;
|
||||
},
|
||||
[cals[0], cals[0]]
|
||||
);
|
||||
|
||||
return dayCount(longest, tz);
|
||||
}
|
||||
|
||||
export function getUserById(id, User = loopback.getModelByType('User')) {
|
||||
return new Promise((resolve, reject) =>
|
||||
User.findById(id, (err, instance) => {
|
||||
if (err || isEmpty(instance)) {
|
||||
return reject(err || 'No user instance found');
|
||||
}
|
||||
|
||||
let completedChallengeCount = 0;
|
||||
let completedProjectCount = 0;
|
||||
if ('completedChallenges' in instance) {
|
||||
completedChallengeCount = instance.completedChallenges.length;
|
||||
instance.completedChallenges.forEach(item => {
|
||||
if (
|
||||
'challengeType' in item &&
|
||||
(item.challengeType === 3 || item.challengeType === 4)
|
||||
) {
|
||||
completedProjectCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
instance.completedChallengeCount = completedChallengeCount;
|
||||
instance.completedProjectCount = completedProjectCount;
|
||||
instance.completedCertCount = getCompletedCertCount(instance);
|
||||
instance.completedLegacyCertCount = getLegacyCertCount(instance);
|
||||
instance.points =
|
||||
(instance.progressTimestamps && instance.progressTimestamps.length) ||
|
||||
1;
|
||||
return resolve(instance);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function getCompletedCertCount(user) {
|
||||
return [
|
||||
'isApisMicroservicesCert',
|
||||
'is2018DataVisCert',
|
||||
'isFrontEndLibsCert',
|
||||
'isQaCertV7',
|
||||
'isInfosecCertV7',
|
||||
'isJsAlgoDataStructCert',
|
||||
'isRespWebDesignCert',
|
||||
'isSciCompPyCertV7',
|
||||
'isDataAnalysisPyCertV7',
|
||||
'isMachineLearningPyCertV7'
|
||||
].reduce((sum, key) => (user[key] ? sum + 1 : sum), 0);
|
||||
}
|
||||
|
||||
function getLegacyCertCount(user) {
|
||||
return [
|
||||
'isFrontEndCert',
|
||||
'isBackEndCert',
|
||||
'isDataVisCert',
|
||||
'isInfosecQaCert'
|
||||
].reduce((sum, key) => (user[key] ? sum + 1 : sum), 0);
|
||||
}
|
@@ -1,628 +0,0 @@
|
||||
/* global describe it expect afterAll */
|
||||
import moment from 'moment-timezone';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import {
|
||||
prepUniqueDaysByHours,
|
||||
calcCurrentStreak,
|
||||
calcLongestStreak,
|
||||
getUserById
|
||||
} from './user-stats';
|
||||
import { mockUserID, mockApp, mockUser } from '../boot_tests/fixtures';
|
||||
|
||||
// setting now to 2016-02-03T11:00:00 (PST)
|
||||
const clock = sinon.useFakeTimers(1454526000000);
|
||||
const PST = 'America/Los_Angeles';
|
||||
|
||||
describe('user stats', () => {
|
||||
afterAll(() => {
|
||||
clock.restore();
|
||||
});
|
||||
|
||||
describe('prepUniqueDaysByHours', () => {
|
||||
it(
|
||||
'should return correct epoch when all entries fall into ' +
|
||||
'one day in UTC',
|
||||
() => {
|
||||
expect(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('8/3/2015 14:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('8/3/2015 20:00', 'M/D/YYYY H:mm').valueOf()
|
||||
])
|
||||
).toEqual([1438567200000]);
|
||||
}
|
||||
);
|
||||
|
||||
it('should return correct epoch when given two identical dates', () => {
|
||||
expect(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf()
|
||||
])
|
||||
).toEqual([1438567200000]);
|
||||
});
|
||||
|
||||
it('should return 2 epochs when dates fall into two days in PST', () => {
|
||||
expect(
|
||||
prepUniqueDaysByHours(
|
||||
[
|
||||
// 8/2/2015 in America/Los_Angeles
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('8/3/2015 14:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('8/3/2015 20:00', 'M/D/YYYY H:mm').valueOf()
|
||||
],
|
||||
PST
|
||||
)
|
||||
).toEqual([1438567200000, 1438610400000]);
|
||||
});
|
||||
|
||||
it('should return 3 epochs when dates fall into three days', () => {
|
||||
expect(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc('8/1/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('3/3/2015 14:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/30/2014 20:00', 'M/D/YYYY H:mm').valueOf()
|
||||
])
|
||||
).toEqual([1412107200000, 1425391200000, 1438394400000]);
|
||||
});
|
||||
|
||||
it(
|
||||
'should return same but sorted array if all input dates are ' +
|
||||
'start of day',
|
||||
() => {
|
||||
expect(
|
||||
prepUniqueDaysByHours([1438387200000, 1425340800000, 1412035200000])
|
||||
).toEqual([1412035200000, 1425340800000, 1438387200000]);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('calcCurrentStreak', function() {
|
||||
it('should return 1 day when today one challenge was completed', () => {
|
||||
expect(
|
||||
calcCurrentStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc(moment.utc().subtract(1, 'hours')).valueOf()
|
||||
])
|
||||
)
|
||||
).toEqual(1);
|
||||
});
|
||||
|
||||
it(
|
||||
'should return 1 day when today more than one challenge ' +
|
||||
'was completed',
|
||||
() => {
|
||||
expect(
|
||||
calcCurrentStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc(moment.utc().subtract(1, 'hours')).valueOf(),
|
||||
moment.utc(moment.utc().subtract(1, 'hours')).valueOf()
|
||||
])
|
||||
)
|
||||
).toEqual(1);
|
||||
}
|
||||
);
|
||||
|
||||
it('should return 0 days when today 0 challenges were completed', () => {
|
||||
expect(
|
||||
calcCurrentStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc('9/11/2015 4:00', 'M/D/YYYY H:mm').valueOf()
|
||||
])
|
||||
)
|
||||
).toEqual(0);
|
||||
});
|
||||
|
||||
it(
|
||||
'should return 2 days when today and yesterday challenges were ' +
|
||||
'completed',
|
||||
() => {
|
||||
expect(
|
||||
calcCurrentStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc(moment.utc().subtract(1, 'days')).valueOf(),
|
||||
moment.utc(moment.utc().subtract(1, 'hours')).valueOf()
|
||||
])
|
||||
)
|
||||
).toEqual(2);
|
||||
}
|
||||
);
|
||||
|
||||
it(
|
||||
'should return 3 when today and for two days before user was ' + 'active',
|
||||
() => {
|
||||
expect(
|
||||
calcCurrentStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/11/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 1:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 1:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 3:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/13/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/14/2015 5:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc(moment.utc().subtract(2, 'days')).valueOf(),
|
||||
moment.utc(moment.utc().subtract(1, 'days')).valueOf(),
|
||||
moment.utc(moment.utc().subtract(1, 'hours')).valueOf()
|
||||
])
|
||||
)
|
||||
).toEqual(3);
|
||||
}
|
||||
);
|
||||
|
||||
it(
|
||||
'should return 1 when there is a 1.5 day long break and ' +
|
||||
'dates fall into two days separated by third',
|
||||
() => {
|
||||
expect(
|
||||
calcCurrentStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc(moment.utc().subtract(47, 'hours')).valueOf(),
|
||||
moment.utc(moment.utc().subtract(11, 'hours')).valueOf()
|
||||
])
|
||||
)
|
||||
).toEqual(1);
|
||||
}
|
||||
);
|
||||
|
||||
it(
|
||||
'should return 2 when the break is more than 1.5 days ' +
|
||||
'but dates fall into two consecutive days',
|
||||
() => {
|
||||
expect(
|
||||
calcCurrentStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc(moment.utc().subtract(40, 'hours')).valueOf(),
|
||||
moment.utc(moment.utc().subtract(1, 'hours')).valueOf()
|
||||
])
|
||||
)
|
||||
).toEqual(2);
|
||||
}
|
||||
);
|
||||
|
||||
it(
|
||||
'should return correct count in default timezone UTC ' +
|
||||
'given `undefined` timezone',
|
||||
() => {
|
||||
expect(
|
||||
calcCurrentStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc(moment.utc().subtract(1, 'hours')).valueOf(),
|
||||
moment.utc(moment.utc().subtract(1, 'hours')).valueOf()
|
||||
])
|
||||
)
|
||||
).toEqual(1);
|
||||
}
|
||||
);
|
||||
|
||||
it(
|
||||
'should return 2 days when today and yesterday ' +
|
||||
'challenges were completed given PST',
|
||||
() => {
|
||||
expect(
|
||||
calcCurrentStreak(
|
||||
prepUniqueDaysByHours(
|
||||
[
|
||||
moment.utc(moment.utc().subtract(1, 'days')).valueOf(),
|
||||
moment.utc(moment.utc().subtract(1, 'hours')).valueOf()
|
||||
],
|
||||
PST
|
||||
),
|
||||
PST
|
||||
)
|
||||
).toEqual(2);
|
||||
}
|
||||
);
|
||||
|
||||
it(
|
||||
'should return 17 when there is no break in given timezone ' +
|
||||
'(but would be the break if in UTC)',
|
||||
() => {
|
||||
expect(
|
||||
calcCurrentStreak(
|
||||
prepUniqueDaysByHours(
|
||||
[
|
||||
1453174506164,
|
||||
1453175436725,
|
||||
1453252466853,
|
||||
1453294968225,
|
||||
1453383782844,
|
||||
1453431903117,
|
||||
1453471373080,
|
||||
1453594733026,
|
||||
1453645014058,
|
||||
1453746762747,
|
||||
1453747659197,
|
||||
1453748029416,
|
||||
1453818029213,
|
||||
1453951796007,
|
||||
1453988570615,
|
||||
1454069704441,
|
||||
1454203673979,
|
||||
1454294055498,
|
||||
1454333545125,
|
||||
1454415163903,
|
||||
1454519128123,
|
||||
moment.tz(PST).valueOf()
|
||||
],
|
||||
PST
|
||||
),
|
||||
PST
|
||||
)
|
||||
).toEqual(17);
|
||||
}
|
||||
);
|
||||
|
||||
it(
|
||||
'should return 4 when there is a break in UTC ' +
|
||||
'(but would be no break in PST)',
|
||||
() => {
|
||||
expect(
|
||||
calcCurrentStreak(
|
||||
prepUniqueDaysByHours([
|
||||
1453174506164,
|
||||
1453175436725,
|
||||
1453252466853,
|
||||
1453294968225,
|
||||
1453383782844,
|
||||
1453431903117,
|
||||
1453471373080,
|
||||
1453594733026,
|
||||
1453645014058,
|
||||
1453746762747,
|
||||
1453747659197,
|
||||
1453748029416,
|
||||
1453818029213,
|
||||
1453951796007,
|
||||
1453988570615,
|
||||
1454069704441,
|
||||
1454203673979,
|
||||
1454294055498,
|
||||
1454333545125,
|
||||
1454415163903,
|
||||
1454519128123,
|
||||
moment.utc().valueOf()
|
||||
])
|
||||
)
|
||||
).toEqual(4);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('calcLongestStreak', function() {
|
||||
it(
|
||||
'should return 1 when there is the only one one-day-long ' +
|
||||
'streak available',
|
||||
() => {
|
||||
expect(
|
||||
calcLongestStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc('9/12/2015 4:00', 'M/D/YYYY H:mm').valueOf()
|
||||
])
|
||||
)
|
||||
).toEqual(1);
|
||||
}
|
||||
);
|
||||
|
||||
it(
|
||||
'should return 4 when there is the only one ' +
|
||||
'more-than-one-days-long streak available',
|
||||
() => {
|
||||
expect(
|
||||
calcLongestStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc('9/11/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/13/2015 3:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/14/2015 1:00', 'M/D/YYYY H:mm').valueOf()
|
||||
])
|
||||
)
|
||||
).toEqual(4);
|
||||
}
|
||||
);
|
||||
|
||||
it(
|
||||
'should return 1 when there is only one one-day-long streak ' +
|
||||
'and it is today',
|
||||
() => {
|
||||
expect(
|
||||
calcLongestStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc(moment.utc().subtract(1, 'hours')).valueOf()
|
||||
])
|
||||
)
|
||||
).toEqual(1);
|
||||
}
|
||||
);
|
||||
|
||||
it('should return 2 when yesterday and today makes longest streak', () => {
|
||||
expect(
|
||||
calcLongestStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc(moment.utc().subtract(1, 'days')).valueOf(),
|
||||
moment.utc(moment.utc().subtract(1, 'hours')).valueOf()
|
||||
])
|
||||
)
|
||||
).toEqual(2);
|
||||
});
|
||||
|
||||
it('should return 4 when there is a month long break', () => {
|
||||
expect(
|
||||
calcLongestStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/11/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('10/4/2015 1:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('10/5/2015 5:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('10/6/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('10/7/2015 5:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('11/3/2015 2:00', 'M/D/YYYY H:mm').valueOf()
|
||||
])
|
||||
)
|
||||
).toEqual(4);
|
||||
});
|
||||
|
||||
it(
|
||||
'should return 2 when there is a more than 1.5 days ' +
|
||||
'long break of (36 hours)',
|
||||
() => {
|
||||
expect(
|
||||
calcLongestStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/11/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 15:30', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment
|
||||
.utc(
|
||||
moment
|
||||
.utc('9/12/2015 15:30', 'M/D/YYYY H:mm')
|
||||
.add(37, 'hours')
|
||||
)
|
||||
.valueOf(),
|
||||
moment.utc('9/14/2015 22:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/15/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('10/3/2015 2:00', 'M/D/YYYY H:mm').valueOf()
|
||||
])
|
||||
)
|
||||
).toEqual(2);
|
||||
}
|
||||
);
|
||||
|
||||
it(
|
||||
'should return 4 when the longest streak consist of ' +
|
||||
'several same day timestamps',
|
||||
() => {
|
||||
expect(
|
||||
calcLongestStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/11/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 1:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 1:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 3:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/13/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/14/2015 5:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc(moment.utc().subtract(2, 'days')).valueOf(),
|
||||
moment.utc(moment.utc().subtract(1, 'days')).valueOf(),
|
||||
moment.utc().valueOf()
|
||||
])
|
||||
)
|
||||
).toEqual(4);
|
||||
}
|
||||
);
|
||||
|
||||
it(
|
||||
'should return 4 when there are several longest streaks ' +
|
||||
'(same length)',
|
||||
() => {
|
||||
expect(
|
||||
calcLongestStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/11/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 1:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/13/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/14/2015 5:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('10/11/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('10/12/2015 1:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('10/13/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('10/14/2015 5:00', 'M/D/YYYY H:mm').valueOf()
|
||||
])
|
||||
)
|
||||
).toEqual(4);
|
||||
}
|
||||
);
|
||||
|
||||
it(
|
||||
'should return correct longest streak when there is a very ' +
|
||||
'long period',
|
||||
() => {
|
||||
let cals = [];
|
||||
const n = 100;
|
||||
for (let i = 0; i < n; i++) {
|
||||
cals.push(moment.utc(moment.utc().subtract(i, 'days')).valueOf());
|
||||
}
|
||||
|
||||
expect(calcLongestStreak(prepUniqueDaysByHours(cals))).toEqual(n);
|
||||
}
|
||||
);
|
||||
|
||||
it(
|
||||
'should return correct longest streak in default timezone ' +
|
||||
'UTC given `undefined` timezone',
|
||||
() => {
|
||||
expect(
|
||||
calcLongestStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc(moment.utc().subtract(1, 'days')).valueOf(),
|
||||
moment.utc(moment.utc().subtract(1, 'hours')).valueOf()
|
||||
])
|
||||
)
|
||||
).toEqual(2);
|
||||
}
|
||||
);
|
||||
|
||||
it(
|
||||
'should return 4 when there is the only one more-than-one-days-long ' +
|
||||
'streak available given PST',
|
||||
() => {
|
||||
expect(
|
||||
calcLongestStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc('9/11/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/13/2015 3:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/14/2015 1:00', 'M/D/YYYY H:mm').valueOf()
|
||||
]),
|
||||
PST
|
||||
)
|
||||
).toEqual(4);
|
||||
}
|
||||
);
|
||||
|
||||
it(
|
||||
'should return 3 when longest streak is 3 in PST ' +
|
||||
'(but would be different in default timezone UTC)',
|
||||
() => {
|
||||
expect(
|
||||
calcLongestStreak(
|
||||
prepUniqueDaysByHours(
|
||||
[
|
||||
moment.utc('9/11/2015 23:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/13/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/14/2015 6:00', 'M/D/YYYY H:mm').valueOf()
|
||||
],
|
||||
PST
|
||||
),
|
||||
PST
|
||||
)
|
||||
).toEqual(3);
|
||||
}
|
||||
);
|
||||
|
||||
it(
|
||||
'should return 17 when there is no break in PST ' +
|
||||
'(but would be break in UTC) and it is current',
|
||||
() => {
|
||||
expect(
|
||||
calcLongestStreak(
|
||||
prepUniqueDaysByHours(
|
||||
[
|
||||
1453174506164,
|
||||
1453175436725,
|
||||
1453252466853,
|
||||
1453294968225,
|
||||
1453383782844,
|
||||
1453431903117,
|
||||
1453471373080,
|
||||
1453594733026,
|
||||
1453645014058,
|
||||
1453746762747,
|
||||
1453747659197,
|
||||
1453748029416,
|
||||
1453818029213,
|
||||
1453951796007,
|
||||
1453988570615,
|
||||
1454069704441,
|
||||
1454203673979,
|
||||
1454294055498,
|
||||
1454333545125,
|
||||
1454415163903,
|
||||
1454519128123,
|
||||
moment.tz(PST).valueOf()
|
||||
],
|
||||
PST
|
||||
),
|
||||
PST
|
||||
)
|
||||
).toEqual(17);
|
||||
}
|
||||
);
|
||||
|
||||
it(
|
||||
'should return a streak of 4 when there is a break in UTC ' +
|
||||
'(but no break in PST)',
|
||||
() => {
|
||||
expect(
|
||||
calcLongestStreak(
|
||||
prepUniqueDaysByHours([
|
||||
1453174506164,
|
||||
1453175436725,
|
||||
1453252466853,
|
||||
1453294968225,
|
||||
1453383782844,
|
||||
1453431903117,
|
||||
1453471373080,
|
||||
1453594733026,
|
||||
1453645014058,
|
||||
1453746762747,
|
||||
1453747659197,
|
||||
1453748029416,
|
||||
1453818029213,
|
||||
1453951796007,
|
||||
1453988570615,
|
||||
1454069704441,
|
||||
1454203673979,
|
||||
1454294055498,
|
||||
1454333545125,
|
||||
1454415163903,
|
||||
1454519128123,
|
||||
moment.utc().valueOf()
|
||||
])
|
||||
)
|
||||
).toEqual(4);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('getUserById', () => {
|
||||
const stubUser = {
|
||||
findById(id, cb) {
|
||||
cb(null, { id: 123 });
|
||||
}
|
||||
};
|
||||
it('returns a promise', () => {
|
||||
expect.assertions(3);
|
||||
expect(typeof getUserById('123', stubUser).then).toEqual('function');
|
||||
expect(typeof getUserById('123', stubUser).catch).toEqual('function');
|
||||
expect(typeof getUserById('123', stubUser).finally).toEqual('function');
|
||||
});
|
||||
|
||||
it('resolves a user for a given id', done => {
|
||||
expect.assertions(7);
|
||||
return getUserById(mockUserID, mockApp.models.User)
|
||||
.then(user => {
|
||||
expect(user).toEqual(mockUser);
|
||||
|
||||
expect(user).toHaveProperty('progressTimestamps');
|
||||
expect(user).toHaveProperty('completedChallengeCount');
|
||||
expect(user).toHaveProperty('completedProjectCount');
|
||||
expect(user).toHaveProperty('completedCertCount');
|
||||
expect(user).toHaveProperty('completedLegacyCertCount');
|
||||
expect(user).toHaveProperty('completedChallenges');
|
||||
})
|
||||
.then(done)
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('throws when no user is found', done => {
|
||||
const noUserError = new Error('No user found');
|
||||
const throwyUserModel = {
|
||||
findById(_, cb) {
|
||||
cb(noUserError);
|
||||
}
|
||||
};
|
||||
expect(
|
||||
getUserById('not-a-real-id', throwyUserModel).catch(error => {
|
||||
expect(error).toEqual(noUserError);
|
||||
done();
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,30 +0,0 @@
|
||||
// Refer : http://stackoverflow.com/a/430240/1932901
|
||||
function trimTags(value) {
|
||||
const tagBody = '(?:[^"\'>]|"[^"]*"|\'[^\']*\')*';
|
||||
const tagOrComment = new RegExp(
|
||||
'<(?:' +
|
||||
// Comment body.
|
||||
'!--(?:(?:-*[^->])*--+|-?)' +
|
||||
// Special "raw text" elements whose content should be elided.
|
||||
'|script\\b' +
|
||||
tagBody +
|
||||
'>[\\s\\S]*?</script\\s*' +
|
||||
'|style\\b' +
|
||||
tagBody +
|
||||
'>[\\s\\S]*?</style\\s*' +
|
||||
// Regular name
|
||||
'|/?[a-z]' +
|
||||
tagBody +
|
||||
')>',
|
||||
'gi'
|
||||
);
|
||||
let rawValue;
|
||||
do {
|
||||
rawValue = value;
|
||||
value = value.replace(tagOrComment, '');
|
||||
} while (value !== rawValue);
|
||||
|
||||
return value.replace(/</g, '<');
|
||||
}
|
||||
|
||||
export { trimTags };
|
Reference in New Issue
Block a user