revert(api): decouple api from curriculum

This reverts commit c077ffe4b9
via PR #40703
This commit is contained in:
Mrugesh Mohapatra
2021-02-25 01:46:46 +05:30
committed by Mrugesh Mohapatra
parent 454b267138
commit 8f0e441644
172 changed files with 391 additions and 377 deletions

View File

@@ -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'
}
};

View File

@@ -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');
}

View File

@@ -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];
}

View File

@@ -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'
};

View File

@@ -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);
}

View File

@@ -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"
}

View File

@@ -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)];

View File

@@ -1,6 +0,0 @@
export function createCookieConfig(req) {
return {
signed: !!req.signedCookies,
domain: process.env.COOKIE_DOMAIN || 'localhost'
};
}

View File

@@ -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
});

View File

@@ -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
)
);
}

View File

@@ -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);
}
);
});
});

View File

@@ -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'
};
}

View File

@@ -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();
});
});
});

View File

@@ -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)];
}, []);
}

View File

@@ -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);
});
});
}

View File

@@ -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'
};

View File

@@ -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]);
});
});
});

View File

@@ -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;

View File

@@ -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);
});
});
});

View File

@@ -1,3 +0,0 @@
exports.addPlaceholderImage = function addPlaceholderImage(name) {
return `https://example.com/${name}.png`;
};

View File

@@ -1,4 +0,0 @@
export default ['auth', 'services', 'link'].reduce((throughs, route) => {
throughs[route] = true;
return throughs;
}, {});

View File

@@ -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();
};

View File

@@ -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 };
}

View File

@@ -1,6 +0,0 @@
export const errorThrowerMiddleware = () => next => action => {
if (action.error) {
throw action.payload;
}
return next(action);
};

View File

@@ -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;

View File

@@ -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
);
});
});
});

View File

@@ -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);
});
}

View File

@@ -1,3 +0,0 @@
export function getEmailSender() {
return process.env.SES_MAIL_FROM || 'team@freecodecamp.org';
}

View File

@@ -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);
}

View File

@@ -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();
})
);
});
});
});

View File

@@ -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, '&lt;');
}
export { trimTags };