├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── README.md ├── app.js ├── bin └── www ├── model ├── codewars-api.js ├── freecodecamp-arrays.js ├── freecodecamp-crawl.js ├── github-commits-api.js ├── github-page.js ├── github-repo-api.js ├── meetups.js └── w3-validator.js ├── package-lock.json ├── package.json ├── public ├── images │ ├── blank-bulb.png │ ├── check-mark.svg │ ├── codewars.svg │ ├── favicon.ico │ ├── fcc.svg │ ├── feedback.svg │ ├── github.svg │ ├── home.svg │ ├── lightbulb.png │ ├── meetup.svg │ ├── side-leaf.png │ ├── side-stem.png │ ├── stem.png │ ├── top-leaf.png │ └── x-mark.svg └── index.js ├── routes ├── github-auth.js ├── index.js ├── report.js ├── scrape-links.js └── validate-request.js ├── scss ├── _animation.scss ├── _navbar.scss ├── _variables.scss └── main.scss ├── tests ├── codewars-api.test.js ├── dummy-data │ ├── authored-kata-detail.json │ ├── authored-kata-none.json │ ├── authored-kata-overview.json │ ├── codewars-response-fail.json │ ├── codewars-response-success.json │ ├── freecodecamp-html-fail.js │ ├── freecodecamp-html-success.js │ ├── github-commits-api-success.json │ ├── github-repos-api-success.json │ ├── meetups-googlesheet-success.js │ └── w3-validator-success.json ├── freecodecamp.test.js ├── get-cookies.helper.js ├── github-auth-route.test.js ├── github-commits-api.test.js ├── github-repos-api.test.js ├── github.test.js ├── meetups.test.js ├── scrape-links.test.js ├── src.test.js ├── validate-request.test.js └── w3-validator.test.js └── views ├── error.hbs ├── layouts └── main.hbs ├── login.hbs ├── partials ├── animation.hbs ├── codewars.hbs ├── codewarsSummary.hbs ├── freecodecamp.hbs ├── github.hbs ├── githubCalendar.hbs ├── githubSummary.hbs ├── htmlHead.hbs ├── meetup.hbs ├── navbar.hbs └── summary.hbs ├── report.hbs ├── scrape-form.hbs └── validate-form.hbs /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base" 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #Custom 2 | config.env 3 | .DS_Store 4 | public/main.css 5 | config.json 6 | admin.json 7 | coverage.lcov 8 | key.txt 9 | logs.txt 10 | 11 | # Logs 12 | logs 13 | *.log 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | 18 | # Runtime data 19 | pids 20 | *.pid 21 | *.seed 22 | *.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | lib-cov 26 | 27 | # Coverage directory used by tools like istanbul 28 | coverage 29 | coverage.lcov 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (http://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | jspm_packages/ 49 | 50 | # Typescript v1 declaration files 51 | typings/ 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variables file 69 | .env 70 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | notifications: 5 | email: false 6 | before_install: 7 | - npm install -g codecov 8 | after_success: npm run nyc -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide for Developers 2 | 3 | ## Getting Started 4 | Check the existing issues: 5 | - priority 1 is highest priority, 5 is lowest 6 | - issues labelled `starter` are good for beginners 7 | - if you want to work on an issue, it is normally best to first discuss with the maintainers (as simple as @ing them in a comment). Make sure to assign yourself to the issue and add the `in progress` label. 8 | 9 | ## Authors / Maintainers 10 | @matthewdking @ameliejyc @bartbucknill @dangerdak @astroash 11 | 12 | ## How to set up Locally 13 | To set up locally, first clone this repo: 14 | ```git clone https://github.com/ameliejyc/prereq-check.git``` 15 | 16 | ### Environment Variables 17 | Obtain a copy of the required environment variables json file from any of the maintainers (see note on github apps below) and place in the root of the project. 18 | 19 | ### Starting the app 20 | Use `npm run devStart` to start the dev server. Or to run tests: `npm test`. 21 | 22 | ### *A note on Authentication and Github apps* 23 | Authentication occurs through github oauth. This requires a github 'app' to be registered. Once github has completed the oauth flow, it 'calls back' prereq-check on a url specified in the app; if you are developing locally we want this url to point to localhost, but when deployed on heroku we want this url to point to the heroku app. 24 | For this reason we have two github oauth apps. The `CLIENT_ID` and `CLIENT_SECRET` are environment variables, and they need to be the id and secret for the correct github oauth app for local development or heroku depending on where the app is running. 25 | To accomplish this: 26 | - variables `CLIENT_ID` and `CLIENT_SECRET` with the correct values have been created on heroku; 27 | - to run this locally you need to have `CLIENT_ID` and `CLIENT_SECRET` with the correct values for running the local app configured in your `config.json`. 28 | 29 | ## Raising Issues 30 | Before raising an issue please first ensure that there is not already an issue covering this (you can always add a comment to an existing issue to expand on it). 31 | 32 | When raising an issue try to: 33 | - give it a clear and concise title 34 | - if it is a bug, provide clear instructions on how to replicate it 35 | 36 | ## Contributing to prereq-check 37 | 38 | Before starting work make sure there is an issue, and discuss with other contributors. 39 | 40 | ### Code Style 41 | The repo includes an `.eslintrc.json` with our style guide. You should enable the eslint plugin for your editor to help you conform to the style guide. Please disable any other linters you may have, as they may make unwanted changes to the codebase (e.g. disable prettier). 42 | 43 | ### Branch Names 44 | Please stick to the following naming conventions: 45 | **Bugfix naming convention**: `bug/`, for example `bug/99`. 46 | **Anything other than bugfix**: `feature/`, for example `feature/100`. 47 | 48 | ### Commits 49 | Please reference the commit issue number prefaced by a '#' in your commits. For example: 50 | `#101 Changes oauth logic to fix login bug` 51 | 52 | ### Tests and Test Coverage 53 | - Your PR should not break any tests 54 | - Your changes should not reduce test coverage of the codebase (you will probably need to add new tests) 55 | - Your tests must pass on Travis 56 | *You can see whether your tests pass on Travis, and the effects of your changes on the test coverage in the conversation tab for your PR*. 57 | 58 | ### Pull Requests 59 | Reference the original issue in your PR. Include any notes on the work you have done that you think will help the reviewer. 60 | Do not include whitespace / stylistic changes in your PR. 61 | When you are ready, assign a maintainer to review your PR. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # prereqCheck [![Build Status](https://travis-ci.org/ameliejyc/prereq-check.svg?branch=master)](https://travis-ci.org/ameliejyc/prereq-check) [![codecov](https://codecov.io/gh/ameliejyc/prereq-check/branch/master/graph/badge.svg)](https://codecov.io/gh/ameliejyc/prereq-check) 2 | A prerequisite checker for FAC applications :heavy_check_mark: 3 | 4 | ## Authors / Maintainers 5 | @matthewdking @ameliejyc @bartbucknill @dangerdak @astroash 6 | 7 | ## Why 8 | The selection committee needs to be able to easily see how applicants have done with their prerequisites. Applicants also need to see how they are doing as they work through them. This should be as transparent as possible so that applicants know what they're being judged on. 9 | 10 | Currently applications are saved into a Google Sheet, where some useful macros are used to help streamline the process. However it is still very tedious for the selection committee to sift through each applicant, as they have to click through to the applicant's various profiles on Github, Codewars, freeCodeCamp etc. 11 | 12 | ## What 13 | preReqCheck is a place where an applicant's progress on the prequisites can all be seen in one place. After signing in with GitHub, an applicant can enter their own GitHub Pages site url and then scroll through their current progress in the various prerequisites, as well as some extra information which is used by 14 | the selection panel. 15 | 16 | Selection panel members can see the same information, but for any applicant. 17 | 18 | ## Contributing 19 | See the [contributing guide](https://github.com/ameliejyc/prereq-check/blob/master/CONTRIBUTING.md). 20 | 21 | ### User Stories 22 | #### As a selection panel member I can: 23 | - [x] See whether the applicant has attained 5 Kyu. 24 | - [x] See whether the applicant has authored a kata. 25 | - [x] See whether the applicant has completed the 4 subsections on freeCodeCamp. 26 | - [x] See whether the applicant has created a website hosted on github pages. 27 | - [x] Input the URL of an applicant's GitHub page in order to see their prerequisite information. 28 | - [x] See extra information about an applicant's Codewars profile. 29 | - [x] See an applicants points score on freecodecamp. 30 | - [x] See more detail about an applicants github page and github usage. 31 | - [x] Login with my GitHub account to access the app. 32 | - [x] See info about an applicants meetup attendance. 33 | - [x] Navigate between all areas of prerequisite information easily. 34 | - [ ] Logout from the app. 35 | 36 | #### As an applicant I can: 37 | - [x] See whether I have attained 5 Kyu. 38 | - [x] See whether I have authored a kata. 39 | - [x] See whether I have completed the 4 subsections on freeCodeCamp. 40 | - [x] See confirmation that I have created a website hosted on github pages. 41 | - [x] Input the URL of my GitHub page in order to see my prerequisite information. 42 | - [x] See extra information about my Codewars profile. 43 | - [x] See my 'points score' from my freecodecamp account. 44 | - [x] See more detail about my github page and usage. 45 | - [x] Login with my GitHub account to access the app. 46 | - [x] See info about my meetup attendance. 47 | - [x] Navigate around all areas of my information easily. 48 | - [ ] Logout from the app. 49 | 50 | ## How & Things We Learned 51 | 52 | ### Stack 53 | * JavaScript 54 | * SCSS & Handlebars 55 | * Node.js & Express.js 56 | 57 | ### Promises 58 | 59 | As this project relies on numerous api calls to be made to return data we have used promises to simply this process. ```Promise.all``` is used to allow us to send all the api calls off in one go, and only render the page on return of them all. 60 | 61 | As the project also requires some page crawling, the node module request-promise is also used to return a page's HTML content as a string: 62 | 63 | ``` 64 | rp('http://www.google.com') 65 | .then(function (htmlString) { 66 | // Process html... 67 | }) 68 | .catch(function (err) { 69 | // Crawling failed... 70 | }); 71 | ``` 72 | 73 | ### Nock for mocking HTTP requests 74 | 75 | prereqCheck makes multiple API calls, and HTTP calls to webpages. Including actual live network requests in tests is problematic because: 76 | 77 | * there may be limitations on the frequency of API calls; 78 | * tests may fail as a result of network of other errors extraneous to the codebase. 79 | 80 | [Nock](https://github.com/node-nock/nock) is an npm module that facilitates the 'mocking' of HTTP requests. 81 | It will intercept any outgoing requests to a defined url, and respond with the data which you give it. 82 | 83 | For example, in the test below *nock* intercepts any request to the defined domain passed to ```nock```, and responds with the contents of the file passed to ```replyWithFile()```. 84 | ```js 85 | tape('Codewars API: getCodewars invalid username', (t) => { 86 | const username = 'astroashaaa'; 87 | nock('https://www.codewars.com/') 88 | .get(`/api/v1/users/${username}`) # <--- MUST start with a / 89 | .replyWithFile(404, path.join(__dirname, 'dummy-data', 'codewars-response-fail.json')); 90 | getCodewars(username) 91 | .then((response) => { 92 | t.deepEqual(response, { 93 | success: false, 94 | statusCode: 404, 95 | message: 'User not found', 96 | }, 'getCodewars for invalid username returns correct object'); 97 | t.end(); 98 | }); 99 | }) 100 | ``` 101 | 102 | ### SASS/SCSS 103 | 104 | prereqCheck uses SCSS to add functionality to CSS predominantly by making use of variables. 105 | 106 | SCSS, or Sassy CSS, is a syntax of SASS and is a superset of CSS3’s syntax. This means that every valid CSS3 stylesheet is valid SCSS as well. 107 | 108 | prereqCheck uses the node module node-sass to compile .scss files to browser-readable .css files. By combining a watch script using nodemon with a build script using node-sass, we are able to watch and build our main.css file on every change using an npm script. 109 | 110 | ``` 111 | “scripts”: { 112 | “build-css”: “node-sass --include-path scss scss/main.scss public/css/main.css”, 113 | “watch-css”: “nodemon -e scss -x \”npm run build-css\”” 114 | } 115 | ``` 116 | 117 | 118 | ## Useful Resources 119 | * [Watch & Compile Your Sass with npm - Medium](https://medium.com/@brianhan/watch-compile-your-sass-with-npm-9ba2b878415b) 120 | * [Promise.all documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all) 121 | * [Nock documentation](https://github.com/node-nock/nock) 122 | * [Request-Promise documentation](https://github.com/request/request-promise) 123 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const favicon = require('serve-favicon'); 4 | const logger = require('morgan'); 5 | const cookieParser = require('cookie-parser'); 6 | const bodyParser = require('body-parser'); 7 | const exphbs = require('express-handlebars'); 8 | const cookieSession = require('cookie-session'); 9 | 10 | const router = require('./routes/index'); 11 | 12 | require('env2')('config.json'); 13 | const app = express(); 14 | 15 | // view engine setup 16 | app.set('views', path.join(__dirname, 'views')); 17 | app.set('view engine', 'hbs'); 18 | const layoutsDir = path.join(__dirname, 'views', 'layouts'); 19 | app.engine('hbs', exphbs({ 20 | extname: 'hbs', 21 | layoutsDir, 22 | partialsDir: path.join(__dirname, 'views', 'partials'), 23 | defaultLayout: 'main', 24 | })); 25 | 26 | // uncomment after placing your favicon in /public 27 | app.use(favicon(path.join(__dirname, 'public', 'images', 'favicon.ico'))); 28 | app.use(logger('dev')); 29 | app.use(cookieSession({ 30 | name: 'session', 31 | secret: process.env.SESSION_SECRET, 32 | maxAge: 24 * 60 * 60 * 1000, // 24 hours 33 | })); 34 | app.use(bodyParser.json()); 35 | app.use(bodyParser.urlencoded({ extended: false })); 36 | app.use(cookieParser()); 37 | app.use(express.static(path.join(__dirname, 'public'))); 38 | 39 | app.use(router); 40 | 41 | // catch 404 and forward to error handler 42 | app.use((req, res, next) => { 43 | const err = new Error('Not Found'); 44 | err.status = 404; 45 | next(err); 46 | }); 47 | 48 | // error handler 49 | app.use((err, req, res, next) => { 50 | // set locals, only providing error in development 51 | res.locals.message = err.message; 52 | res.locals.error = req.app.get('env') === 'development' ? err : {}; 53 | 54 | // render the error page 55 | res.status(err.status || 500); 56 | res.render('error'); 57 | }); 58 | 59 | module.exports = app; 60 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('prereq-check:server'); 9 | var http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || '3000'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port, () => { 29 | `Magic on port: ${port}`; 30 | }); 31 | server.on('error', onError); 32 | server.on('listening', onListening); 33 | 34 | /** 35 | * Normalize a port into a number, string, or false. 36 | */ 37 | 38 | function normalizePort(val) { 39 | var port = parseInt(val, 10); 40 | 41 | if (isNaN(port)) { 42 | // named pipe 43 | return val; 44 | } 45 | 46 | if (port >= 0) { 47 | // port number 48 | return port; 49 | } 50 | 51 | return false; 52 | } 53 | 54 | /** 55 | * Event listener for HTTP server "error" event. 56 | */ 57 | 58 | function onError(error) { 59 | if (error.syscall !== 'listen') { 60 | throw error; 61 | } 62 | 63 | var bind = typeof port === 'string' 64 | ? 'Pipe ' + port 65 | : 'Port ' + port; 66 | 67 | // handle specific listen errors with friendly messages 68 | switch (error.code) { 69 | case 'EACCES': 70 | console.error(bind + ' requires elevated privileges'); 71 | process.exit(1); 72 | break; 73 | case 'EADDRINUSE': 74 | console.error(bind + ' is already in use'); 75 | process.exit(1); 76 | break; 77 | default: 78 | throw error; 79 | } 80 | } 81 | 82 | /** 83 | * Event listener for HTTP server "listening" event. 84 | */ 85 | 86 | function onListening() { 87 | var addr = server.address(); 88 | var bind = typeof addr === 'string' 89 | ? 'pipe ' + addr 90 | : 'port ' + addr.port; 91 | debug('Listening on ' + bind); 92 | } 93 | -------------------------------------------------------------------------------- /model/codewars-api.js: -------------------------------------------------------------------------------- 1 | const rp = require('request-promise-native'); 2 | 3 | const getKyu = (body) => { 4 | const codewarsRank = body.ranks.languages.javascript.rank; 5 | return Math.abs(codewarsRank); 6 | }; 7 | 8 | const hasAuthored = (kataArray) => {return kataArray.length >= 1}; 9 | 10 | const getAuthoredKatas = (username) => { 11 | const options = { 12 | uri: `https://www.codewars.com/api/v1/users/${username}/code-challenges/authored/`, 13 | json: true, // Automatically parses the JSON string in the response 14 | }; 15 | return rp(options) 16 | .then((apiRes) => { 17 | return apiRes.data.reduce((ourKataArray, responseKataArray) => { 18 | const data = { 19 | success: true, 20 | id: responseKataArray.id, 21 | name: responseKataArray.name, 22 | link: 'https://www.codewars.com/kata/' + responseKataArray.id, 23 | rank: Math.abs(responseKataArray.rank), 24 | beta: responseKataArray.rank === null, 25 | }; 26 | return [...ourKataArray, data]; 27 | }, []); 28 | }) 29 | .catch((err) => { 30 | console.error('Fetching authored katas failed'); 31 | console.error(err); 32 | const codewarsObj = {}; 33 | codewarsObj.success = false; 34 | codewarsObj.statusCode = err.statusCode; 35 | if (err.statusCode === 404) { 36 | codewarsObj.message = 'User not found'; 37 | } else { 38 | codewarsObj.message = 'Error retrieving data'; 39 | } 40 | return codewarsObj; 41 | }); 42 | }; 43 | 44 | const appendKataCompletions = (katas) => { 45 | if (!Array.isArray(katas)) { return katas; } 46 | const completionPromises = katas.map((kata) => { 47 | const options = { 48 | uri: `https://www.codewars.com/api/v1/code-challenges/${kata.id}`, 49 | json: true, // Automatically parses the JSON string in the response 50 | }; 51 | return rp(options) 52 | .then((kataDetail) => { 53 | return kataDetail.totalCompleted; 54 | }) 55 | .catch((err) => { 56 | console.error('Fetching codewars kata completions'); 57 | console.error(err); 58 | return null; 59 | }); 60 | }); 61 | return Promise.all(completionPromises) 62 | .then((completionsArray) => { 63 | return katas.map((kata, index) => { 64 | return Object.assign({}, kata, { completions: completionsArray[index] }); 65 | }); 66 | }); 67 | }; 68 | 69 | const getCodewars = (username) => { 70 | const options = { 71 | uri: 'https://www.codewars.com/api/v1/users/', 72 | json: true, // Automatically parses the JSON string in the response 73 | }; 74 | options.uri += username; 75 | return rp(options) 76 | .then((apiRes) => { 77 | const codewarsObj = {}; 78 | codewarsObj.success = true; 79 | codewarsObj.kyu = getKyu(apiRes); 80 | codewarsObj.achieved5Kyu = getKyu(apiRes) <= 5; 81 | codewarsObj.honor = apiRes.honor; 82 | codewarsObj.username = username; 83 | return codewarsObj; 84 | }) 85 | .catch((err) => { 86 | console.error('Fetching codewars info failed'); 87 | console.error(err); 88 | const codewarsObj = {}; 89 | codewarsObj.success = false; 90 | codewarsObj.statusCode = err.statusCode; 91 | if (err.statusCode === 404) { 92 | codewarsObj.message = 'User not found'; 93 | } else { 94 | codewarsObj.message = 'Error retrieving data'; 95 | } 96 | return codewarsObj; 97 | }); 98 | }; 99 | 100 | module.exports = { 101 | hasAuthored, 102 | getAuthoredKatas, 103 | getKyu, 104 | getCodewars, 105 | appendKataCompletions, 106 | }; 107 | -------------------------------------------------------------------------------- /model/freecodecamp-arrays.js: -------------------------------------------------------------------------------- 1 | const htmlCss = ['Say Hello to HTML Elements', 'Headline with the h2 Element', 'Inform with the Paragraph Element', 'Uncomment HTML', 'Comment out HTML', 'Fill in the Blank with Placeholder Text', 'Delete HTML Elements', 'Change the Color of Text', 'Use CSS Selectors to Style Elements', 'Use a CSS Class to Style an Element', 'Style Multiple Elements with a CSS Class', 'Change the Font Size of an Element', 'Set the Font Family of an Element', 'Import a Google Font', 'Specify How Fonts Should Degrade', 'Add Images to your Website', 'Size your Images', 'Add Borders Around your Elements', 'Add Rounded Corners with a Border Radius', 'Make Circular Images with a Border Radius', 'Link to External Pages with Anchor Elements', 'Nest an Anchor Element within a Paragraph', 'Make Dead Links using the Hash Symbol', 'Turn an Image into a Link', 'Create a Bulleted Unordered List', 'Create an Ordered List', 'Create a Text Field', 'Add Placeholder Text to a Text Field', 'Create a Form Element', 'Add a Submit Button to a Form', 'Use HTML5 to Require a Field', 'Create a Set of Radio Buttons', 'Create a Set of Checkboxes', 'Check Radio Buttons and Checkboxes by Default', 'Nest Many Elements within a Single Div Element', 'Give a Background Color to a Div Element', 'Set the ID of an Element', 'Use an ID Attribute to Style an Element', 'Adjusting the Padding of an Element', 'Adjust the Margin of an Element', 'Add a Negative Margin to an Element', 'Add Different Padding to Each Side of an Element', 'Add Different Margins to Each Side of an Element', 'Use Clockwise Notation to Specify the Padding of an Element', 'Use Clockwise Notation to Specify the Margin of an Element', 'Style the HTML Body Element', 'Inherit Styles from the Body Element', 'Prioritize One Style Over Another', 'Override Styles in Subsequent CSS', 'Override Class Declarations by Styling ID Attributes', 'Override Class Declarations with Inline Styles', 'Override All Other Styles by using Important', 'Use Hex Code for Specific Colors', 'Use Hex Code to Mix Colors', 'Use Abbreviated Hex Code', 'Use RGB values to Color Elements', 'Use RGB to Mix Colors']; 2 | 3 | const basicJavaScript = ['Comment your JavaScript Code', 'Declare JavaScript Variables', 'Storing Values with the Assignment Operator', 'Initializing Variables with the Assignment Operator', 'Understanding Uninitialized Variables', 'Understanding Case Sensitivity in Variables', 'Add Two Numbers with JavaScript', 'Subtract One Number from Another with JavaScript', 'Multiply Two Numbers with JavaScript', 'Divide One Number by Another with JavaScript', 'Increment a Number with JavaScript', 'Decrement a Number with JavaScript', 'Create Decimal Numbers with JavaScript', 'Multiply Two Decimals with JavaScript', 'Divide one Decimal by Another with JavaScript', 'Finding a Remainder in JavaScript', 'Compound Assignment With Augmented Addition', 'Compound Assignment With Augmented Subtraction', 'Compound Assignment With Augmented Multiplication', 'Compound Assignment With Augmented Division', 'Convert Celsius to Fahrenheit', 'Declare String Variables', 'Escaping Literal Quotes in Strings', 'Quoting Strings with Single Quotes', 'Escape Sequences in Strings', 'Concatenating Strings with Plus Operator', 'Concatenating Strings with the Plus Equals Operator', 'Constructing Strings with Variables', 'Appending Variables to Strings', 'Find the Length of a String', 'Use Bracket Notation to Find the First Character in a String', 'Understand String Immutability', 'Use Bracket Notation to Find the Nth Character in a String', 'Use Bracket Notation to Find the Last Character in a String', 'Use Bracket Notation to Find the NthtoLast Character in a String', 'Word Blanks', 'Store Multiple Values in one Variable using JavaScript Arrays', 'Nest one Array within Another Array', 'Access Array Data with Indexes', 'Modify Array Data With Indexes', 'Access MultiDimensional Arrays With Indexes', 'Manipulate Arrays With push', 'Manipulate Arrays With pop', 'Manipulate Arrays With shift', 'Manipulate Arrays With unshift', 'Shopping List', 'Write Reusable JavaScript with Functions', 'Passing Values to Functions with Arguments', 'Global Scope and Functions', 'Local Scope and Functions', 'Global vs Local Scope in Functions', 'Return a Value from a Function with Return', 'Assignment with a Returned Value', 'Stand in Line', 'Understanding Boolean Values', 'Use Conditional Logic with If Statements', 'Comparison with the Equality Operator', 'Comparison with the Strict Equality Operator', 'Comparison with the Inequality Operator', 'Comparison with the Strict Inequality Operator', 'Comparison with the Greater Than Operator', 'Comparison with the Greater Than Or Equal To Operator', 'Comparison with the Less Than Operator', 'Comparison with the Less Than Or Equal To Operator', 'Comparisons with the Logical And Operator', 'Comparisons with the Logical Or Operator', 'Introducing Else Statements', 'Introducing Else If Statements', 'Logical Order in If Else Statements', 'Chaining If Else Statements', 'Golf Code', 'Selecting from many options with Switch Statements', 'Adding a default option in Switch statements', 'Multiple Identical Options in Switch Statements', 'Replacing If Else Chains with Switch', 'Returning Boolean Values from Functions', 'Return Early Pattern for Functions', 'Counting Cards', 'Build JavaScript Objects', 'Accessing Objects Properties with the Dot Operator', 'Accessing Objects Properties with Bracket Notation', 'Accessing Objects Properties with Variables', 'Updating Object Properties', 'Add New Properties to a JavaScript Object', 'Delete Properties from a JavaScript Object', 'Using Objects for Lookups', 'Testing Objects for Properties', 'Manipulating Complex Objects', 'Accessing Nested Objects', 'Accessing Nested Arrays', 'Iterate with JavaScript For Loops', 'Iterate Odd Numbers With a For Loop', 'Count Backwards With a For Loop', 'Iterate Through an Array with a For Loop', 'Nesting For Loops', 'Iterate with JavaScript While Loops', 'Profile Lookup', 'Generate Random Fractions with JavaScript', 'Generate Random Whole Numbers with JavaScript', 'Generate Random Whole Numbers within a Range', 'Sift through Text with Regular Expressions', 'Find Numbers with Regular Expressions', 'Find Whitespace with Regular Expressions', 'Invert Regular Expression Matches with JavaScript']; 4 | 5 | const oOFunctionalProgramming = ['Declare JavaScript Objects as Variables', 'Construct JavaScript Objects with Functions', 'Make Instances of Objects with a Constructor Function', 'Make Unique Objects by Passing Parameters to our Constructor', 'Make Object Properties Private', 'Iterate over Arrays with map', 'Condense arrays with reduce', 'Filter Arrays with filter', 'Sort Arrays with sort', 'Reverse Arrays with reverse', 'Concatenate Arrays with concat', 'Split Strings with split', 'Join Strings with join']; 6 | 7 | const basicScripting = ['Get Set for our Algorithm Challenges', 'Reverse a String', 'Factorialize a Number', 'Check for Palindromes', 'Find the Longest Word in a String', 'Title Case a Sentence', 'Return Largest Numbers in Arrays', 'Confirm the Ending', 'Repeat a string repeat a string', 'Truncate a string', 'Chunky Monkey', 'Slasher Flick', 'Mutations', 'Falsy Bouncer', 'Seek and Destroy', 'Where do I belong', 'Caesars Cipher']; 8 | 9 | module.exports = { 10 | htmlCss, 11 | basicJavaScript, 12 | oOFunctionalProgramming, 13 | basicScripting, 14 | }; 15 | -------------------------------------------------------------------------------- /model/freecodecamp-crawl.js: -------------------------------------------------------------------------------- 1 | const rp = require('request-promise-native'); 2 | 3 | const { 4 | htmlCss, 5 | basicJavaScript, 6 | oOFunctionalProgramming, 7 | basicScripting, 8 | } = require('./freecodecamp-arrays'); 9 | 10 | const fccSectionValidator = (htmlString, fccArray) => fccArray.every(element => 11 | htmlString.indexOf(element) !== -1); 12 | 13 | const getFccScore = (htmlString) => { 14 | const regEx = /text-primary">\[ (\d+)/; 15 | return regEx.exec(htmlString)[1]; 16 | }; 17 | 18 | const getFreeCodeCamp = (username) => { 19 | const options = { 20 | uri: `https://www.freecodecamp.org/${username}`, 21 | }; 22 | return rp(options) 23 | .then((htmlString) => { 24 | const reg = new RegExp(username, 'gi'); 25 | if (!htmlString.match(reg)) { 26 | throw Error('User not found'); 27 | } else { 28 | const freeCodeCampObj = { 29 | success: true, 30 | score: getFccScore(htmlString), 31 | htmlCss: fccSectionValidator(htmlString, htmlCss), 32 | basicJavaScript: fccSectionValidator(htmlString, basicJavaScript), 33 | oOFunctionalProgramming: fccSectionValidator(htmlString, oOFunctionalProgramming), 34 | basicScripting: fccSectionValidator(htmlString, basicScripting), 35 | handle: username, 36 | }; 37 | freeCodeCampObj.complete = freeCodeCampObj.htmlCss && freeCodeCampObj.basicJavaScript 38 | && freeCodeCampObj.oOFunctionalProgramming && freeCodeCampObj.basicScripting; 39 | return freeCodeCampObj; 40 | } 41 | }) 42 | .catch((err) => { 43 | console.error('Fetching FreeCodeCamp crawl failed'); 44 | console.error(err); 45 | const freeCodeCampObj = {}; 46 | freeCodeCampObj.success = false; 47 | freeCodeCampObj.message = 'User not found'; 48 | return freeCodeCampObj; 49 | }); 50 | }; 51 | 52 | module.exports = { 53 | fccSectionValidator, 54 | getFreeCodeCamp, 55 | getFccScore, 56 | }; 57 | -------------------------------------------------------------------------------- /model/github-commits-api.js: -------------------------------------------------------------------------------- 1 | const rp = require('request-promise-native'); 2 | 3 | const getRepoName = (url) => { 4 | const newUrl = url.replace('https://', ''); 5 | const reg = /[\w.]+\/([\w-]+)/; 6 | const repo = reg.exec(url); 7 | return repo ? repo[1] : newUrl; 8 | }; 9 | 10 | const getGithubCommits = (githubHandle, url, token) => { 11 | const repo = url ? getRepoName(url) : ''; 12 | const options = { 13 | uri: `https://api.github.com/repos/${githubHandle}/${repo}/commits`, 14 | headers: { 15 | 'User-Agent': 'prereqCheck', 16 | }, 17 | json: true, // Automatically parses the JSON string in the response 18 | }; 19 | options.uri += token ? `?access_token=${token}` : ''; 20 | return rp(options) 21 | .then((apiRes) => { 22 | return { 23 | success: true, 24 | commits: apiRes.length, 25 | }; 26 | }) 27 | .catch((err) => { 28 | console.error('Get Github commit count failed'); 29 | console.error(err); 30 | return { 31 | success: false, 32 | message: 'Fetching Github commit count failed', 33 | }; 34 | }); 35 | }; 36 | 37 | module.exports = { 38 | getRepoName, 39 | getGithubCommits, 40 | }; 41 | -------------------------------------------------------------------------------- /model/github-page.js: -------------------------------------------------------------------------------- 1 | const rp = require('request-promise-native'); 2 | const normalizeUrl = require('normalize-url'); 3 | 4 | const getGithubPage = (url) => { 5 | const options = { 6 | uri: url ? normalizeUrl(url) : '', 7 | resolveWithFullResponse: true, 8 | }; 9 | 10 | return rp(options) 11 | .then((response) => { 12 | const githubObj = {}; 13 | githubObj.success = true; 14 | githubObj.url = options.uri; 15 | return githubObj; 16 | }) 17 | .catch((err) => { 18 | console.error('Fetching GitHub page failed'); 19 | console.error(err); 20 | const githubObj = {}; 21 | githubObj.success = false; 22 | githubObj.statusCode = err.statusCode; 23 | if (err.statusCode === 404) { 24 | githubObj.message = 'Page not found'; 25 | } else if (!err.url) { 26 | githubObj.message = 'No page entered'; 27 | } else { 28 | githubObj.message = 'Error retrieving page'; 29 | } 30 | return githubObj; 31 | }); 32 | }; 33 | 34 | module.exports = { getGithubPage }; 35 | -------------------------------------------------------------------------------- /model/github-repo-api.js: -------------------------------------------------------------------------------- 1 | const rp = require('request-promise-native'); 2 | 3 | const getGithubRepos = (username, token) => { 4 | const options = { 5 | uri: `https://api.github.com/users/${username}`, 6 | headers: { 7 | 'User-Agent': 'prereqCheck', 8 | }, 9 | json: true, // Automatically parses the JSON string in the response 10 | }; 11 | options.uri += token ? `?access_token=${token}` : ''; 12 | return rp(options) 13 | .then((apiRes) => { 14 | return { 15 | success: true, 16 | repos: apiRes.public_repos, 17 | }; 18 | }) 19 | .catch((err) => { 20 | console.error('Get Github Repos failed'); 21 | console.error(err); 22 | return { 23 | success: false, 24 | message: 'Fetching Github repo count failed', 25 | }; 26 | }); 27 | }; 28 | 29 | module.exports = { 30 | getGithubRepos, 31 | }; 32 | -------------------------------------------------------------------------------- /model/meetups.js: -------------------------------------------------------------------------------- 1 | const GoogleSpreadsheet = require('google-spreadsheet'); 2 | require('env2')('config.json'); 3 | 4 | const getMeetupCount = (githubHandle) => { 5 | const doc = new GoogleSpreadsheet(process.env.SPREADSHEET_KEY); 6 | 7 | const setAuth = new Promise((resolve, reject) => { 8 | const credsJson = { 9 | client_email: process.env.CLIENT_EMAIL, 10 | private_key: process.env.PRIVATE_KEY, 11 | }; 12 | doc.useServiceAccountAuth(credsJson, (err) => { 13 | if (err) return reject(err); 14 | resolve(); 15 | }); 16 | }); 17 | 18 | const getAttendance = () => new Promise((resolve, reject) => { 19 | // query google sheets api to get row where github handle matches 'githubHandle' argument 20 | // githubnameunique is the column name in google sheets 21 | doc.getRows( 22 | process.env.WORKSHEET_ID, 23 | { 24 | query: `githubnameunique=${githubHandle}`, 25 | }, 26 | (err, rows) => { 27 | if (err) return reject(err); 28 | resolve({ 29 | success: true, 30 | // countunique is the name of the column in google sheets 31 | count: rows[0] ? rows[0].countunique : 0, 32 | ghHandle: githubHandle, 33 | }); 34 | } 35 | ); 36 | }); 37 | 38 | return setAuth 39 | .then(getAttendance) 40 | .catch((err) => { 41 | console.error('Error getting meetup data from google-sheet'); 42 | console.error(err); 43 | return { success: false, message: 'Unable to retrieve meetup data' }; 44 | }); 45 | }; 46 | 47 | module.exports = getMeetupCount; 48 | -------------------------------------------------------------------------------- /model/w3-validator.js: -------------------------------------------------------------------------------- 1 | const rp = require('request-promise-native'); 2 | 3 | const getNumberOfErrors = array => array.reduce((sum, item) => { 4 | if (item.type === 'error') { 5 | sum += 1; 6 | return sum; 7 | } 8 | return sum; 9 | }, 0); 10 | 11 | const getW3Validator = (url) => { 12 | const w3Url = `http://validator.w3.org/nu/?doc=${url}`; 13 | const options = { 14 | uri: `${w3Url}/&out=json`, 15 | headers: { 16 | 'User-Agent': 'Request-Promise', 17 | }, 18 | json: true, 19 | }; 20 | return rp(options) 21 | .then((apiRes) => { 22 | const errors = getNumberOfErrors(apiRes.messages); 23 | return { 24 | success: true, 25 | errors, 26 | other: apiRes.messages.length - errors, 27 | url: w3Url, 28 | }; 29 | }) 30 | .catch((err) => { 31 | console.error('Fetching W3 Validator info failed'); 32 | console.error(err); 33 | return { 34 | success: false, 35 | message: 'Error retrieving data from W3 Validator', 36 | }; 37 | }); 38 | }; 39 | 40 | module.exports = { 41 | getNumberOfErrors, 42 | getW3Validator, 43 | }; 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prereq-check", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "test": "tape tests/*.js | tap-spec", 7 | "devStart": "nodemon -e scss,js -x \"npm run build-css && node bin/www\"", 8 | "start": "node ./bin/www", 9 | "build-css": "node-sass --include-path scss scss/main.scss public/main.css", 10 | "postinstall": "npm run build-css", 11 | "coverage": "istanbul cover tape tests/*.js", 12 | "nyc": "nyc npm test && nyc report --reporter=html && nyc report --reporter=text-lcov > coverage.lcov && codecov" 13 | }, 14 | "dependencies": { 15 | "body-parser": "~1.17.1", 16 | "codecov": "^2.3.0", 17 | "cookie-parser": "~1.4.3", 18 | "cookie-session": "^1.3.1", 19 | "debug": "~2.6.3", 20 | "env2": "^2.2.0", 21 | "express": "~4.15.2", 22 | "express-handlebars": "^3.0.0", 23 | "github-calendar": "^1.1.12", 24 | "google-spreadsheet": "^2.0.4", 25 | "hbs": "~4.0.1", 26 | "humanize-url": "^1.0.1", 27 | "morgan": "~1.8.1", 28 | "node-sass": "^4.5.3", 29 | "normalize-url": "^1.9.1", 30 | "pg": "^7.3.0", 31 | "pg-promise": "^6.5.1", 32 | "prepend-http": "^2.0.0", 33 | "request": "^2.81.0", 34 | "request-promise-native": "^1.0.4", 35 | "serve-favicon": "~2.4.2" 36 | }, 37 | "devDependencies": { 38 | "eslint": "^4.6.1", 39 | "eslint-config-airbnb-base": "^12.0.0", 40 | "eslint-plugin-import": "^2.7.0", 41 | "istanbul": "^0.4.5", 42 | "nock": "^9.0.14", 43 | "nodemon": "^1.12.0", 44 | "nyc": "^11.2.1", 45 | "supertest": "^3.0.0", 46 | "tap-spec": "^4.1.1", 47 | "tape": "^4.8.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /public/images/blank-bulb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foundersandcoders/prereq-check/0cd15b50c94934019634c457b809ce3ce9cbaeb6/public/images/blank-bulb.png -------------------------------------------------------------------------------- /public/images/check-mark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/codewars.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | codewars 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /public/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foundersandcoders/prereq-check/0cd15b50c94934019634c457b809ce3ce9cbaeb6/public/images/favicon.ico -------------------------------------------------------------------------------- /public/images/fcc.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | 13 | 14 | -------------------------------------------------------------------------------- /public/images/feedback.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/images/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/home.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/lightbulb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foundersandcoders/prereq-check/0cd15b50c94934019634c457b809ce3ce9cbaeb6/public/images/lightbulb.png -------------------------------------------------------------------------------- /public/images/meetup.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | 11 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /public/images/side-leaf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foundersandcoders/prereq-check/0cd15b50c94934019634c457b809ce3ce9cbaeb6/public/images/side-leaf.png -------------------------------------------------------------------------------- /public/images/side-stem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foundersandcoders/prereq-check/0cd15b50c94934019634c457b809ce3ce9cbaeb6/public/images/side-stem.png -------------------------------------------------------------------------------- /public/images/stem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foundersandcoders/prereq-check/0cd15b50c94934019634c457b809ce3ce9cbaeb6/public/images/stem.png -------------------------------------------------------------------------------- /public/images/top-leaf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foundersandcoders/prereq-check/0cd15b50c94934019634c457b809ce3ce9cbaeb6/public/images/top-leaf.png -------------------------------------------------------------------------------- /public/images/x-mark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/index.js: -------------------------------------------------------------------------------- 1 | var burger = document.getElementById('js-navbar'); 2 | var navbar = document.getElementById('js-navbar__list'); 3 | 4 | burger.addEventListener('click', function(){ 5 | burger.classList.toggle("change"); 6 | navbar.classList.toggle("navbar__list--visible"); 7 | }); 8 | 9 | navbar.addEventListener('click', function(){ 10 | burger.classList.toggle("change"); 11 | navbar.classList.toggle("navbar__list--visible"); 12 | }); 13 | -------------------------------------------------------------------------------- /routes/github-auth.js: -------------------------------------------------------------------------------- 1 | const rp = require('request-promise-native'); 2 | require('env2')('config.json'); 3 | 4 | const isInTeam = (teamMembersArray, user) => teamMembersArray.some(member => member.login === user); 5 | 6 | const getUserData = (token) => { 7 | const options = { 8 | uri: 'https://api.github.com/user', 9 | headers: { 10 | authorization: `token ${token}`, 11 | 'User-Agent': 'prereqCheck', 12 | }, 13 | json: true, 14 | }; 15 | return rp(options); 16 | }; 17 | 18 | const getAdminTeamMembers = (token) => { 19 | const options = { 20 | uri: `https://api.github.com/teams/${process.env.AUTHORISED_TEAM_ID}/members`, 21 | headers: { 22 | authorization: `token ${token}`, 23 | 'User-Agent': 'prereqCheck', 24 | }, 25 | json: true, 26 | simple: false, 27 | resolveWithFullResponse: true, 28 | }; 29 | return rp(options); 30 | }; 31 | 32 | 33 | const githubAuth = (req, res) => { 34 | if (req.query.code) { 35 | const options = { 36 | uri: 'https://github.com/login/oauth/access_token', 37 | qs: { 38 | client_id: process.env.CLIENT_ID, 39 | client_secret: process.env.CLIENT_SECRET, 40 | code: req.query.code, 41 | }, 42 | headers: { 43 | Accept: 'application/json', 44 | }, 45 | json: true, 46 | }; 47 | rp(options) 48 | .then((oauthResponse) => { 49 | req.session.token = oauthResponse.access_token; 50 | return getUserData(req.session.token); 51 | }) 52 | .catch((err) => { 53 | console.error('Couldn\'t log in with Github'); 54 | console.error(err); 55 | throw new Error('Couldn\'t log in with Github') 56 | }) 57 | .then((userData) => { 58 | req.session.user = userData.login; 59 | return getAdminTeamMembers(req.session.token); 60 | }) 61 | .then((response) => { 62 | if (response.statusCode === 200) { 63 | // if user is able to retrieve team they are probably a member, but check anyway 64 | req.session.isInTeam = isInTeam(response.body, req.session.user); 65 | } else { 66 | req.session.isInTeam = false; 67 | } 68 | res.redirect('/links'); 69 | }) 70 | .catch((err) => { 71 | if (err.message === 'Couldn\'t log in with Github') { 72 | res.render('error', { message: err.message }); 73 | } else { 74 | console.error('Error retrieving user team membership'); 75 | console.error(err); 76 | req.session.isInTeam = false; 77 | res.redirect('/links'); 78 | } 79 | }); 80 | } else { 81 | // login unsuccessful 82 | res.redirect('/'); 83 | } 84 | }; 85 | 86 | module.exports = { 87 | isInTeam, 88 | githubAuth, 89 | }; 90 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | 4 | const report = require('./report'); 5 | const scrapeLinks = require('./scrape-links'); 6 | const { githubAuth } = require('./github-auth'); 7 | 8 | const login = (req, res) => { 9 | res.render('login', { clientId: process.env.CLIENT_ID }); 10 | }; 11 | 12 | const links = (req, res) => { 13 | res.render('scrape-form'); 14 | }; 15 | const validateForm = (req, res) => { 16 | res.render('validate-form'); 17 | }; 18 | 19 | router.get('/', login); 20 | router.get('/links', links); 21 | router.get('/links-validate', validateForm); 22 | router.get('/scrape-links', scrapeLinks); 23 | router.get('/report', report); 24 | 25 | router.get('/auth', githubAuth); 26 | 27 | module.exports = router; 28 | -------------------------------------------------------------------------------- /routes/report.js: -------------------------------------------------------------------------------- 1 | const { getCodewars, getAuthoredKatas, appendKataCompletions, hasAuthored } = require("../model/codewars-api"); 2 | const { getFreeCodeCamp } = require("../model/freecodecamp-crawl"); 3 | const { getGithubPage } = require('../model/github-page'); 4 | const { getW3Validator } = require('../model/w3-validator'); 5 | const { getGithubRepos } = require('../model/github-repo-api'); 6 | const { getGithubCommits } = require('../model/github-commits-api'); 7 | const getMeetupCount = require('../model/meetups'); 8 | const isValidRequest = require('./validate-request'); 9 | 10 | const displayReport = (req, res) => { 11 | if (!isValidRequest(req.session, req.query)) { 12 | return res.redirect('/links'); 13 | } 14 | const { githubPage, fccHandle, cwHandle, ghHandle } = req.query; 15 | Promise.all([ 16 | getCodewars(cwHandle), 17 | getFreeCodeCamp(fccHandle), 18 | getGithubPage(githubPage), 19 | getW3Validator(githubPage), 20 | getGithubRepos(ghHandle, req.session.token), 21 | getGithubCommits(ghHandle, githubPage, req.session.token), 22 | getAuthoredKatas(cwHandle).then(appendKataCompletions), 23 | getMeetupCount(ghHandle) 24 | .catch((err) => { 25 | console.error('Fetching Promise.all Kata completions'); 26 | })]) 27 | .then((values) => { 28 | const summaryObject = {}; 29 | [summaryObject.codewars, 30 | summaryObject.freeCodeCamp, 31 | summaryObject.githubPage, 32 | summaryObject.w3Validation, 33 | summaryObject.githubRepos, 34 | summaryObject.githubCommits, 35 | summaryObject.codewarsKatas, 36 | summaryObject.meetups] = values; 37 | summaryObject.codewars.hasAuthored = hasAuthored(summaryObject.codewarsKatas); 38 | summaryObject.githubHandle = ghHandle; 39 | res.render('report', summaryObject); 40 | }) 41 | .catch((err) => { 42 | console.error('Danger, danger: Report Promise.all errored!'); 43 | console.error(err); 44 | }); 45 | }; 46 | 47 | module.exports = displayReport; 48 | -------------------------------------------------------------------------------- /routes/scrape-links.js: -------------------------------------------------------------------------------- 1 | const rp = require('request-promise-native'); 2 | const normalizeUrl = require('normalize-url'); 3 | const prependHttps = require('prepend-http'); 4 | const humanizeUrl = require('humanize-url'); 5 | 6 | const getGithubLink = (htmlString) => { 7 | const regEx = /github.com\/([\w-]*)/; 8 | const ghHandle = regEx.exec(htmlString); 9 | return ghHandle ? ghHandle[1] : null; 10 | }; 11 | 12 | const getFccLink = (htmlString) => { 13 | const regEx = /freecodecamp.(org|com)\/([\w-]*)/; 14 | const fccHandle = regEx.exec(htmlString); 15 | return fccHandle ? fccHandle[2] : null; 16 | }; 17 | 18 | const getCodewarsLink = (htmlString) => { 19 | const regEx = /codewars.com\/users\/([\w-]*)/; 20 | const cwHandle = regEx.exec(htmlString); 21 | return cwHandle ? cwHandle[1] : null; 22 | }; 23 | 24 | const formatUrl = (url) => { 25 | if (url.includes('github')) { 26 | return prependHttps(humanizeUrl(url), { https: true }); 27 | } 28 | return normalizeUrl(url); 29 | }; 30 | 31 | const scrapeLinks = (req, res) => { 32 | const url = req.query.githubPage ? formatUrl(req.query.githubPage) : ''; 33 | return rp(url) 34 | .then((htmlString) => { 35 | const githubScrape = { 36 | githubPageLink: url, 37 | success: true, 38 | githubHandle: getGithubLink(htmlString), 39 | fccHandle: getFccLink(htmlString), 40 | codewarsHandle: getCodewarsLink(htmlString), 41 | }; 42 | githubScrape.allHandles = githubScrape.githubHandle && 43 | githubScrape.fccHandle && githubScrape.codewarsHandle; 44 | res.render('validate-form', githubScrape); 45 | }) 46 | .catch((err) => { 47 | console.error('Fetching Github Pages URL failed'); 48 | console.error(err); 49 | const githubScrape = { 50 | success: false, 51 | message: 'Page not found', 52 | }; 53 | res.render('validate-form', githubScrape); 54 | }); 55 | }; 56 | module.exports = scrapeLinks; 57 | -------------------------------------------------------------------------------- /routes/validate-request.js: -------------------------------------------------------------------------------- 1 | module.exports = (session, query) => { 2 | const isEmpty = Object.keys(query).length === 0; 3 | if (isEmpty || !session.user) { 4 | return false; 5 | } 6 | return query.ghHandle === session.user || session.isInTeam; 7 | }; 8 | -------------------------------------------------------------------------------- /scss/_animation.scss: -------------------------------------------------------------------------------- 1 | /* Animated lightbulb on report page */ 2 | 3 | .logo-animation__container { 4 | position: relative; 5 | margin: 4rem auto 0 auto; 6 | height: 16rem; 7 | width: 225px; 8 | } 9 | 10 | .logo-animation { 11 | position: absolute; 12 | background-repeat: no-repeat; 13 | background-size: contain; 14 | } 15 | 16 | .logo-animation__lightbulb { 17 | background-image: url(/images/blank-bulb.png); 18 | height: 16rem; 19 | width: 100%; 20 | } 21 | 22 | .logo-animation__stem { 23 | background-image: url(/images/stem.png); 24 | animation: grow-stem 5s ease; 25 | animation-fill-mode: forwards; 26 | transform-origin: bottom; 27 | width: 20px; 28 | height: 93px; 29 | left: 100px; 30 | top: 86px; 31 | background-position: 50%; 32 | } 33 | 34 | .logo-animation__side-leaf { 35 | background-image: url(/images/side-leaf.png); 36 | animation: grow-side-leaf 5s ease; 37 | animation-fill-mode: forwards; 38 | transform-origin: 100% 100%; 39 | width: 70px; 40 | height: 60px; 41 | top: 80px; 42 | left: 41px; 43 | background-position: 50%; 44 | } 45 | 46 | .logo-animation__top-leaf { 47 | background-image: url(/images/top-leaf.png); 48 | animation: grow-top-leaf 5s ease; 49 | animation-fill-mode: forwards; 50 | transform-origin: bottom; 51 | width: 50px; 52 | height: 63px; 53 | left: 91px; 54 | top: 36px; 55 | background-position: 50%; 56 | } 57 | 58 | .logo-animation__side-stem { 59 | background-image: url(/images/side-stem.png); 60 | animation: grow-side-stem 5s ease; 61 | animation-fill-mode: forwards; 62 | transform-origin: 0% 100%; 63 | width: 30px; 64 | height: 30px; 65 | top: 86px; 66 | left: 108px; 67 | background-position: 50%; 68 | } 69 | 70 | @keyframes grow-stem { 71 | 0% { transform: scaleY(0); } 72 | 100% { transform: scaleY(1); } 73 | } 74 | 75 | @keyframes grow-side-leaf { 76 | 0% { transform: scale(0); opacity: 0; } 77 | 30% { transform: scale(0); opacity: 0; } 78 | 100% { transform: scale(1); opacity: 1; } 79 | } 80 | 81 | @keyframes grow-top-leaf { 82 | 0% { transform: scale(0) translate(-10px, 30px); opacity: 0; } 83 | 50% { transform: scale(0) translate(-10px, 30px); opacity: 0; } 84 | 100% { transform: scale(1) translate(0px, 0px); opacity: 1; } 85 | } 86 | 87 | @keyframes grow-side-stem { 88 | 0% { transform: scale(0); opacity: 0; } 89 | 60% { transform: scale(0); opacity: 0; } 90 | 100% { transform: scale(1); opacity: 1; } 91 | } 92 | -------------------------------------------------------------------------------- /scss/_navbar.scss: -------------------------------------------------------------------------------- 1 | .navbar__nav { 2 | width: 100%; 3 | position: fixed; 4 | bottom: 0; 5 | margin: 0 -4vw; 6 | padding-top: 1.5vh; 7 | padding-bottom: 0.5vh; 8 | background-color: $lowlight-color; 9 | } 10 | 11 | @media only screen and (min-width: 600px) { 12 | .navbar__nav { 13 | display: flex; 14 | align-items: center; 15 | width: 10vw; 16 | position: fixed; 17 | left: -0.5vw; 18 | top: 0; 19 | margin: 0 2rem; 20 | background-color: $main-color; 21 | } 22 | } 23 | 24 | .navbar__item { 25 | display: inline-block; 26 | font-size: 0.8rem; 27 | } 28 | 29 | @media only screen and (min-width: 600px) { 30 | .navbar__item { 31 | display: block; 32 | } 33 | } 34 | 35 | .navbar__icon { 36 | width: 8vw; 37 | margin: 0 3vw; 38 | } 39 | 40 | @media only screen and (min-width: 600px) { 41 | .navbar__icon { 42 | width: 3vw; 43 | margin: 1vh 1vw; 44 | } 45 | .navbar__icon:hover { 46 | transform: scale(1.1); 47 | transition: all .2s ease-in-out; 48 | } 49 | } 50 | 51 | .navbar__link { 52 | text-decoration: none; 53 | display: block; 54 | } 55 | 56 | @media only screen and (min-width: 600px) { 57 | .navbar__link { 58 | margin-top: 1vh; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /scss/_variables.scss: -------------------------------------------------------------------------------- 1 | $main-font: Helvetica, sans-serif; 2 | 3 | $main-color: rgb(168, 227, 230); 4 | $highlight-color: rgb(231, 250, 251); 5 | $links-color: rgb(3, 63, 87); 6 | $lowlight-color: rgb(93, 197, 204); 7 | $error-message-color: rgb(207, 4, 8); 8 | 9 | $report-title-fontsize: 2.5rem; 10 | $links-form-fontsize: 1.5rem; 11 | $report-subtitle-fontsize: 2rem; 12 | $report-description-fontsize: 1.2rem; 13 | $report-kata-description-fontsize: 1.7rem; 14 | -------------------------------------------------------------------------------- /scss/main.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | @import 'animation'; 3 | @import 'navbar'; 4 | 5 | /*********************************************** 6 | reset 7 | ***********************************************/ 8 | 9 | html { 10 | box-sizing: border-box; 11 | } 12 | 13 | *, 14 | *:before, 15 | *:after { 16 | box-sizing: inherit; 17 | margin: 0; 18 | padding: 0; 19 | } 20 | 21 | a { 22 | color: $links-color; 23 | text-decoration: underline; 24 | } 25 | 26 | li { 27 | list-style: none; 28 | } 29 | 30 | /*********************************************** 31 | page layout 32 | ***********************************************/ 33 | 34 | body { 35 | margin: 5vh 4vw; 36 | font: $main-font; 37 | background-color: $main-color; 38 | text-align: center; 39 | font-family: 'Open Sans', sans-serif; 40 | transition: all 1s; 41 | } 42 | 43 | @media only screen and (min-width: 600px) { 44 | body { 45 | margin: 5vh 10vw 0; 46 | transition: all 1s; 47 | } 48 | } 49 | 50 | @media only screen and (min-width: 769px) { 51 | body { 52 | margin: 5vh 10vw 0; 53 | transition: all 1s; 54 | } 55 | } 56 | 57 | h1 { 58 | font-size: $report-title-fontsize; 59 | } 60 | 61 | /*********************************************** 62 | login page 63 | ***********************************************/ 64 | 65 | .login__logo { 66 | width: 55vw; 67 | margin: 2vh; 68 | transition: all 1s; 69 | } 70 | 71 | @media only screen and (min-width: 600px) { 72 | .login__logo { 73 | width: 50vw; 74 | transition: all 1s; 75 | } 76 | } 77 | 78 | @media only screen and (min-width: 769px) { 79 | .login__logo { 80 | width: 25vw; 81 | transition: all 1s; 82 | } 83 | } 84 | 85 | .login__info { 86 | font-size: $report-description-fontsize; 87 | margin-bottom: 2vh; 88 | } 89 | 90 | .login__button { 91 | font-size: $links-form-fontsize; 92 | padding: 2vh 2vw; 93 | text-align: center; 94 | background-color: $highlight-color; 95 | text-align: center; 96 | border: black solid 2px; 97 | border-radius: 2px; 98 | display: inline-block; 99 | } 100 | 101 | .login__button:hover { 102 | background-color: white; 103 | cursor: pointer; 104 | } 105 | 106 | /*********************************************** 107 | links page 108 | ***********************************************/ 109 | 110 | .links__form, 111 | .links__submit, 112 | .links__input { 113 | font-size: $links-form-fontsize; 114 | padding: 0 2vw; 115 | text-align: center; 116 | } 117 | 118 | .links__name { 119 | font-weight: 600; 120 | } 121 | 122 | .links__submit, 123 | .links__input { 124 | display: block; 125 | height: 10vh; 126 | margin: 3vh 0; 127 | border: black solid 2px; 128 | border-radius: 2px; 129 | } 130 | 131 | .links__input { 132 | width: 100%; 133 | } 134 | 135 | .links__input:focus { 136 | outline: none; 137 | border: $links-color solid 2px; 138 | } 139 | 140 | .links__submit { 141 | width: 35%; 142 | background-color: $highlight-color; 143 | text-align: center; 144 | text-decoration: none; 145 | display: inline-block; 146 | } 147 | 148 | .links__submit:hover { 149 | background-color: white; 150 | cursor: pointer; 151 | } 152 | 153 | .links__error-message { 154 | color: $error-message-color; 155 | } 156 | 157 | /*********************************************** 158 | report page 159 | ***********************************************/ 160 | 161 | .report-section { 162 | min-height: 100vh; 163 | border-bottom: white solid 2px; 164 | } 165 | 166 | .report-section__title { 167 | padding: 2.5vh 0; 168 | font-weight: 700; 169 | } 170 | 171 | .report-section__link { 172 | text-decoration: none; 173 | color: black; 174 | } 175 | 176 | .report-section__link:hover { 177 | text-decoration: underline; 178 | } 179 | 180 | .report-section__prereq { 181 | font-size: 1.3rem; 182 | display: inline; 183 | } 184 | 185 | .report-section__list { 186 | text-align: left; 187 | } 188 | 189 | @media only screen and (min-width: 600px) { 190 | .report-section__list { 191 | text-align: center; 192 | transition: all 1s; 193 | } 194 | } 195 | 196 | @media only screen and (min-width: 769px) { 197 | .report-section__list { 198 | text-align: center; 199 | transition: all 1s; 200 | } 201 | } 202 | 203 | .report-section__list-item { 204 | padding: 2vh 0; 205 | margin-left: 4vw; 206 | transition: all 1s; 207 | } 208 | 209 | @media only screen and (min-width: 600px) { 210 | .report-section__list-item { 211 | margin: 0; 212 | transition: all 1s; 213 | } 214 | } 215 | 216 | @media only screen and (min-width: 769px) { 217 | .report-section__list-item { 218 | margin: 0; 219 | transition: all 1s; 220 | } 221 | } 222 | 223 | .report-section__icon { 224 | display: inline; 225 | margin-right: 2vw; 226 | } 227 | 228 | .report-section__banner { 229 | background-color: #ffffff; 230 | margin: 0 -4vw; 231 | } 232 | 233 | @media only screen and (min-width: 769px) { 234 | .report-section__banner { 235 | margin: 0; 236 | } 237 | } 238 | 239 | .report-section__error-message { 240 | color: $error-message-color; 241 | } 242 | 243 | /*********************************************** 244 | github section 245 | ***********************************************/ 246 | 247 | .report-section__website { 248 | margin: 0 auto; 249 | margin-top: 1em; 250 | border: none; 251 | background-color: white; 252 | width: 90%; 253 | max-width: 1600px; 254 | } 255 | .report-section__website-link { 256 | display: block; 257 | } 258 | .report-section__info-item { 259 | margin: 2vh 0; 260 | } 261 | 262 | .report-section__info-title { 263 | font-size: $report-subtitle-fontsize; 264 | } 265 | 266 | .report-section__info-description { 267 | font-size: $report-description-fontsize; 268 | } 269 | 270 | .report-section__calendar { 271 | max-width: 92vw; 272 | margin: 5vh auto; 273 | overflow-x: scroll; 274 | overflow-y: hidden; 275 | } 276 | 277 | @media only screen and (min-width: 600px) { 278 | .report-section__website { 279 | width: 100%; 280 | } 281 | .report-section__calendar { 282 | max-width: 80vw; 283 | transition: all 1s; 284 | } 285 | } 286 | 287 | @media only screen and (min-width: 769px) { 288 | .report-section__calendar { 289 | max-width: 80vw; 290 | transition: all 1s; 291 | } 292 | } 293 | 294 | .report-section__arrows { 295 | letter-spacing: 5px; 296 | font-weight: bolder; 297 | margin-bottom: 3vh; 298 | } 299 | 300 | @media only screen and (min-width: 750px) { 301 | .report-section__arrows { 302 | display: none; 303 | } 304 | } 305 | 306 | .calendar { 307 | border: none !important; 308 | } 309 | 310 | .text-normal { 311 | font-size: 1.2rem; 312 | } 313 | 314 | /*********************************************** 315 | freecodecamp section 316 | ***********************************************/ 317 | 318 | .report-section__freecodecamp-score { 319 | font-size: 5rem; 320 | margin-top: 5vh; 321 | margin-bottom: 1vh; 322 | font-weight: 600; 323 | } 324 | 325 | .report-section__fcc-subtitle { 326 | margin-bottom: 1vh; 327 | font-style: italic; 328 | } 329 | 330 | /*********************************************** 331 | codewars section 332 | ***********************************************/ 333 | 334 | .report-section__kata { 335 | width: 90vw; 336 | margin-bottom: 7vh; 337 | padding: 1.5vh 1.5vw; 338 | background-color: $highlight-color; 339 | border: black solid 3px; 340 | border-radius: 2px; 341 | transition: all 1s; 342 | } 343 | 344 | @media only screen and (min-width: 600px) { 345 | .report-section__kata { 346 | width: 80vw; 347 | transition: all 1s; 348 | } 349 | } 350 | 351 | @media only screen and (min-width: 769px) { 352 | .report-section__kata { 353 | margin-left: 5vw; 354 | margin-right: 5vw; 355 | width: 70vw; 356 | transition: all 1s; 357 | } 358 | } 359 | 360 | .report-section__authored-kata { 361 | font-size: $report-kata-description-fontsize; 362 | margin: 2vh; 363 | } 364 | 365 | .report-section__kata-name { 366 | font-size: $report-description-fontsize; 367 | } 368 | 369 | .report-section__kata-item { 370 | font-size: $report-description-fontsize; 371 | padding: 3vh 3vw; 372 | background-color: $highlight-color; 373 | border: black solid 3px; 374 | border-radius: 2px; 375 | margin: 3vh 2vw; 376 | display: inline; 377 | } 378 | -------------------------------------------------------------------------------- /tests/codewars-api.test.js: -------------------------------------------------------------------------------- 1 | const tape = require("tape"); 2 | const nock = require('nock'); 3 | const path = require('path'); 4 | const codewarsSuccessData = require("./dummy-data/codewars-response-success.json"); 5 | const kataOverview = require('./dummy-data/authored-kata-overview.json'); 6 | 7 | const { 8 | getKyu, 9 | hasAuthored, 10 | getCodewars, 11 | getAuthoredKatas, 12 | appendKataCompletions, 13 | } = require('../model/codewars-api'); 14 | 15 | tape('Codewars API: getKyu', (t) => { 16 | const actual = typeof getKyu(codewarsSuccessData); 17 | t.equal(actual, 'number', 'Kyu rank should be a number'); 18 | t.end(); 19 | }); 20 | 21 | tape('Codewars API: hasAuthored', (t) => { 22 | const kataArray = [kataOverview.data]; 23 | const noKata = []; 24 | t.ok(hasAuthored(kataArray), 'hasAuthored returns true if user has authored 1 kata'); 25 | t.notOk(hasAuthored(noKata), 'hasAuthored returns false if user has authored 0 kata'); 26 | t.end(); 27 | }); 28 | 29 | tape('Codewars API: getAuthoredKatas', (t) => { 30 | const expected = [{ 31 | success: true, 32 | id: '5884b6550785f7c58f000047', 33 | name: 'Organise duplicate numbers in list', 34 | rank: 6, 35 | beta: false, 36 | link: 'https://www.codewars.com/kata/5884b6550785f7c58f000047', 37 | }, { 38 | success: true, 39 | id: '58d64c8d14286ca558000083', 40 | name: 'Join command (simplified)', 41 | rank: 0, 42 | beta: true, 43 | link: 'https://www.codewars.com/kata/58d64c8d14286ca558000083', 44 | }]; 45 | const username = 'testuser'; 46 | nock('https://www.codewars.com/') 47 | .get(`/api/v1/users/${username}/code-challenges/authored/`) 48 | .replyWithFile(200, path.join(__dirname, 'dummy-data', 'authored-kata-overview.json')); 49 | getAuthoredKatas(username) 50 | .then((katas) => { 51 | t.deepEqual(katas, expected, 'Returns array of relevant kata data from api response'); 52 | t.end(); 53 | }); 54 | }); 55 | 56 | tape('Codewars API: appendKataCompletions', (t) => { 57 | const input = [{ 58 | success: true, 59 | id: '5884b6550785f7c58f000047', 60 | name: 'Organise duplicate numbers in list', 61 | rank: 6, 62 | beta: false, 63 | link: 'https://www.codewars.com/kata/5884b6550785f7c58f000047', 64 | }]; 65 | const expected = [{ 66 | success: true, 67 | id: '5884b6550785f7c58f000047', 68 | name: 'Organise duplicate numbers in list', 69 | rank: 6, 70 | beta: false, 71 | link: 'https://www.codewars.com/kata/5884b6550785f7c58f000047', 72 | completions: 434, 73 | }]; 74 | nock('https://www.codewars.com/') 75 | .get('/api/v1/code-challenges/5884b6550785f7c58f000047') 76 | .replyWithFile(200, path.join(__dirname, 'dummy-data', 'authored-kata-detail.json')); 77 | appendKataCompletions(input) 78 | .then((katas) => { 79 | t.deepEqual(katas, expected, 'Returns array of kata with completions key'); 80 | t.end(); 81 | }); 82 | }); 83 | 84 | tape('Codewars API: getCodewars valid username', (t) => { 85 | const username = 'astroash'; 86 | const expected = { 87 | success: true, 88 | kyu: 5, 89 | achieved5Kyu: true, 90 | honor: 352, 91 | username, 92 | }; 93 | nock('https://www.codewars.com/') 94 | .get(`/api/v1/users/${username}`) 95 | .replyWithFile(200, path.join(__dirname, 'dummy-data', 'codewars-response-success.json')) 96 | .get(`/api/v1/users/${username}/code-challenges/authored/`) 97 | .replyWithFile(200, path.join(__dirname, 'dummy-data', 'authored-kata-overview.json')); 98 | getCodewars(username) 99 | .then((actual) => { 100 | t.deepEqual(actual, expected, 'getCodewars for valid username returns correct object'); 101 | t.end(); 102 | }); 103 | }); 104 | 105 | tape('Codewars API: getCodewars invalid username', (t) => { 106 | const username = 'astroashaaa'; 107 | nock('https://www.codewars.com/') 108 | .get(`/api/v1/users/${username}`) 109 | .replyWithFile(404, path.join(__dirname, 'dummy-data', 'codewars-response-fail.json')); 110 | getCodewars(username) 111 | .then((response) => { 112 | t.deepEqual( 113 | response, 114 | { 115 | success: false, 116 | statusCode: 404, 117 | message: 'User not found', 118 | }, 119 | 'getCodewars for invalid username returns correct object', 120 | ); 121 | t.end(); 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /tests/dummy-data/authored-kata-detail.json: -------------------------------------------------------------------------------- 1 | { 2 | "id":"5884b6550785f7c58f000047", 3 | "name":"Organise duplicate numbers in list", 4 | "slug":"organise-duplicate-numbers-in-list", 5 | "category":"reference", 6 | "publishedAt":"2017-02-22T19:33:08Z", 7 | "approvedAt":"2017-02-26T20:32:34Z", 8 | "languages":[ 9 | "javascript", 10 | "php", 11 | "python", 12 | "ruby", 13 | "crystal" 14 | ], 15 | "url":"https://www.codewars.com/kata/organise-duplicate-numbers-in-list", 16 | "rank":{ 17 | "id":-6, 18 | "name":"6 kyu", 19 | "color":"yellow" 20 | }, 21 | "createdAt":"2017-01-22T13:40:37Z", 22 | "createdBy":{ 23 | "username":"dangerdak", 24 | "url":"https://www.codewars.com/users/dangerdak" 25 | }, 26 | "approvedBy":{ 27 | "username":"HerrWert", 28 | "url":"https://www.codewars.com/users/HerrWert" 29 | }, 30 | "description":"Sam is an avid collector of numbers. Every time he finds a new number he throws it on the top of his number-pile. Help Sam organise his collection so he can take it to the International Number Collectors Conference in Cologne. Given an array of numbers, your function should return an array of arrays, where each subarray contains all the duplicates of a particular number. Subarrays should be in the same order as the first occurence of the number they contain: ```javascript group([3, 2, 6, 2, 1, 3]) >>> [[3, 3], [2, 2], [6], [1]] ``` Assume the input is always going to be an array of numbers. If the input is an empty array, an empty array should be returned.", 31 | "totalAttempts":2175, 32 | "totalCompleted":434, 33 | "totalStars":21, 34 | "voteScore":134, 35 | "tags":[ 36 | "Fundamentals", 37 | "Arrays", 38 | "Data Types", 39 | "Sorting", 40 | "Algorithms", 41 | "Logic" 42 | ], 43 | "contributorsWanted":true, 44 | "unresolved":{ 45 | "issues":2, 46 | "suggestions":0 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/dummy-data/authored-kata-none.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [] 3 | } 4 | -------------------------------------------------------------------------------- /tests/dummy-data/authored-kata-overview.json: -------------------------------------------------------------------------------- 1 | { 2 | "data":[ 3 | { 4 | "id":"5884b6550785f7c58f000047", 5 | "name":"Organise duplicate numbers in list", 6 | "description":"Sam is an avid collector of numbers. Every time he finds a new number he throws it on the top of his number-pile. Help Sam organise his collection so he can take it to the International Number Collectors Conference in Cologne. Given an array of numbers, your function should return an array of arrays, where each subarray contains all the duplicates of a particular number. Subarrays should be in the same order as the first occurence of the number they contain: ```javascript group([3, 2, 6, 2, 1, 3]) >>> [[3, 3], [2, 2], [6], [1]] ``` Assume the input is always going to be an array of numbers. If the input is an empty array, an empty array should be returned.", 7 | "rank":-6, 8 | "rankName":"6 kyu", 9 | "tags":[ 10 | "Fundamentals", 11 | "Arrays", 12 | "Data Types", 13 | "Sorting", 14 | "Algorithms", 15 | "Logic" 16 | ], 17 | "languages":[ 18 | "javascript", 19 | "php", 20 | "python", 21 | "ruby", 22 | "crystal" 23 | ] 24 | }, 25 | { 26 | "id":"58d64c8d14286ca558000083", 27 | "name":"Join command (simplified)", 28 | "description":"The [join command](https://shapeshed.com/unix-join/) in Linux joins the lines of two data files on a common field. Your task is to write a function *join* which simulates this, but with some differences: Empty cells resulting from unpairable lines should be filled with a substitute value, which is passed as the first argument to *join*. Instead of files, your data sets are contained in arrays. Each row of data is contained in a subarray, and each data value is represented by an element in the subarray: ```javascript dataTable = [ [join1, val1, val2], [join3, val3, val4], ]; dataTable1 = [ [join1, val5, val6], [join2, val7, val8], [join3, val9, val10] ]; join(sub, dataTable, dataTable1); >>> [ [join1, val1, val2, val5, val6], [join2, sub, sub, val7, val8], [join3, val3, val4, val9, val10] ] ``` (This is similar to the data format used by [Google Charts](https://developers.google.com/chart/interactive/docs/datatables_dataviews)). Data tables should be joined in the order in which they appear as arguments. You may assume that the join fields in a given data table will always be unique. Also assume each cell contains only strings or numbers (no need to test for deep equality), and data tables can have different numbers of lines, but all lines across all inputs have the same length.", 29 | "rank":null, 30 | "rankName":null, 31 | "tags":[ 32 | "Algorithms", 33 | "Arrays", 34 | "Data Types", 35 | "Data Structures" 36 | ], 37 | "languages":[ 38 | "javascript" 39 | ] 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /tests/dummy-data/codewars-response-fail.json: -------------------------------------------------------------------------------- 1 | { success: false, reason: 'not found' } -------------------------------------------------------------------------------- /tests/dummy-data/codewars-response-success.json: -------------------------------------------------------------------------------- 1 | { 2 | "username":"astroash", 3 | "name":"AstroAsh", 4 | "honor":352, 5 | "clan":"Founders & Coders", 6 | "leaderboardPosition":22056, 7 | "skills":[ 8 | 9 | ], 10 | "ranks":{ 11 | "overall":{ 12 | "rank":-5, 13 | "name":"5 kyu", 14 | "color":"yellow", 15 | "score":280 16 | }, 17 | "languages":{ 18 | "javascript":{ 19 | "rank":-5, 20 | "name":"5 kyu", 21 | "color":"yellow", 22 | "score":280 23 | } 24 | } 25 | }, 26 | "codeChallenges":{ 27 | "totalAuthored":1, 28 | "totalCompleted":49 29 | } 30 | } -------------------------------------------------------------------------------- /tests/dummy-data/freecodecamp-html-fail.js: -------------------------------------------------------------------------------- 1 | module.exports = `

fcce3abbd74-b40e-4e5d-96a8-c7e1992dcfe1


[ 1 ]

`; 48 | -------------------------------------------------------------------------------- /tests/dummy-data/github-repos-api-success.json: -------------------------------------------------------------------------------- 1 | { 2 | "login": "astroash", 3 | "id": 14337958, 4 | "avatar_url": "https://avatars3.githubusercontent.com/u/14337958?v=4", 5 | "gravatar_id": "", 6 | "url": "https://api.github.com/users/astroash", 7 | "html_url": "https://github.com/astroash", 8 | "followers_url": "https://api.github.com/users/astroash/followers", 9 | "following_url": "https://api.github.com/users/astroash/following{/other_user}", 10 | "gists_url": "https://api.github.com/users/astroash/gists{/gist_id}", 11 | "starred_url": "https://api.github.com/users/astroash/starred{/owner}{/repo}", 12 | "subscriptions_url": "https://api.github.com/users/astroash/subscriptions", 13 | "organizations_url": "https://api.github.com/users/astroash/orgs", 14 | "repos_url": "https://api.github.com/users/astroash/repos", 15 | "events_url": "https://api.github.com/users/astroash/events{/privacy}", 16 | "received_events_url": "https://api.github.com/users/astroash/received_events", 17 | "type": "User", 18 | "site_admin": false, 19 | "name": "AstroAsh", 20 | "company": null, 21 | "blog": "http://www.astroash.com/", 22 | "location": null, 23 | "email": null, 24 | "hireable": null, 25 | "bio": "Full Stack Developer @foundersandcoders ", 26 | "public_repos": 12, 27 | "public_gists": 0, 28 | "followers": 13, 29 | "following": 13, 30 | "created_at": "2015-09-17T21:24:58Z", 31 | "updated_at": "2017-09-11T16:45:04Z" 32 | } -------------------------------------------------------------------------------- /tests/dummy-data/meetups-googlesheet-success.js: -------------------------------------------------------------------------------- 1 | module.exports = { responseData: "https://spreadsheets.google.com/feeds/list/1_GpdOSpwivXZRZcMzJvz25K6u4j9B7SuWgvqeSwB6tk/o1az7e0/private/full2017-09-15T12:10:18.991ZForm responses 1dakshina.scott11https://spreadsheets.google.com/feeds/list/1_GpdOSpwivXZRZcMzJvz25K6u4j9B7SuWgvqeSwB6tk/o1az7e0/di2tg2017-09-15T12:10:18.991Z08/08/2017 18:43:46email: githubname: #N/A, githubnameunique: bartbucknill, countunique: 108/08/2017 18:43:46#N/Abartbucknill1", responseHeaders: ['Content-Type', 2 | 'application/atom+xml; charset=UTF-8; type=feed', 3 | 'X-Robots-Tag', 4 | 'noindex, nofollow, nosnippet', 5 | 'Expires', 6 | 'Sat, 16 Sep 2017 17:30:35 GMT', 7 | 'Date', 8 | 'Sat, 16 Sep 2017 17:30:35 GMT', 9 | 'Cache-Control', 10 | 'private, max-age=0, must-revalidate, no-transform', 11 | 'Vary', 12 | 'Accept, X-GData-Authorization, GData-Version', 13 | 'GData-Version', 14 | '3.0', 15 | 'ETag', 16 | 'W/"DUMFSXcyeSt7ImA9XBZWFEo."', 17 | 'Last-Modified', 18 | 'Fri, 15 Sep 2017 12:10:18 GMT', 19 | 'Transfer-Encoding', 20 | 'chunked', 21 | 'P3P', 22 | 'CP="This is not a P3P policy! See https://support.google.com/accounts/answer/151657?hl=en for more info."', 23 | 'P3P', 24 | 'CP="This is not a P3P policy! See https://support.google.com/accounts/answer/151657?hl=en for more info."', 25 | 'X-Content-Type-Options', 26 | 'nosniff', 27 | 'X-Frame-Options', 28 | 'SAMEORIGIN', 29 | 'X-XSS-Protection', 30 | '1; mode=block', 31 | 'Server', 32 | 'GSE', 33 | 'Set-Cookie', 34 | 'NID=112=cneSEOA5ADiGiekXUWvS1e3Nj5ecO3wULz5QtKsNYNuXs3_QPZfog3PzIVBmGSpmbvggF9oOXrz5eg_koHDkdE9Nsx2MBI6aEHDa0q0GKKA-nApwSFk9U4aXiBZ2qTMr;Domain=.google.com;Path=/;Expires=Sun, 18-Mar-2018 17:30:35 GMT;HttpOnly', 35 | 'Set-Cookie', 36 | 'NID=112=lTx9UnzNkJ1Cz-bJBblNKZdBhJHdHtgzWriqklBKy73YrswhmteX2djoTRjHSgqvPFLguv46aizhrnCypjXuhbksN8CoXlVfHAbAHJzHUWB3AkyQn49S5C7jIy49uVJ7;Domain=.google.com;Path=/;Expires=Sun, 18-Mar-2018 17:30:35 GMT;HttpOnly', 37 | 'Alt-Svc', 38 | 'quic=":443"; ma=2592000; v="39,38,37,35"', 39 | 'Connection', 40 | 'close'] 41 | }; 42 | -------------------------------------------------------------------------------- /tests/dummy-data/w3-validator-success.json: -------------------------------------------------------------------------------- 1 | {"url":"http://www.astroash.com/","messages":[{"type":"error","lastLine":6,"lastColumn":61,"firstColumn":3,"message":"A “meta” element with an “http-equiv” attribute whose value is “X-UA-Compatible” must have a “content” attribute with the value “IE=edge”.","extract":"UTF-8\">\n \n -->\n \n \n\n \n <","hiliteStart":10,"hiliteLength":6},{"type":"error","lastLine":123,"lastColumn":137,"firstColumn":6,"message":"Element “form” not allowed as child of element “h3” in this context. (Suppressing further errors from this subtree.)","extract":"
\n
\n ","hiliteStart":10,"hiliteLength":132},{"type":"error","lastLine":135,"lastColumn":9,"firstColumn":3,"message":"End tag for “body” seen, but there were unclosed elements.","extract":"\n\n \n\n\n
\n <","hiliteStart":10,"hiliteLength":17},{"type":"error","lastLine":34,"lastColumn":29,"firstColumn":1,"message":"Unclosed element “div”.","extract":"\n\n\n
\n\n \n
\n","hiliteStart":10,"hiliteLength":6},{"type":"error","lastLine":136,"lastColumn":7,"message":"End of file seen and there were open elements.","extract":"dy>\n
\n","hiliteStart":10,"hiliteLength":1}]} -------------------------------------------------------------------------------- /tests/freecodecamp.test.js: -------------------------------------------------------------------------------- 1 | const tape = require('tape'); 2 | const nock = require('nock'); 3 | 4 | const { 5 | htmlCss, 6 | basicJavaScript, 7 | oOFunctionalProgramming, 8 | basicScripting, 9 | } = require('../model/freecodecamp-arrays'); 10 | const { fccSectionValidator, getFreeCodeCamp, getFccScore } = require('../model/freecodecamp-crawl'); 11 | const fccCompleteHtmlString = require('./dummy-data/freecodecamp-html-success'); 12 | const fccIncompleteHtmlString = require('./dummy-data/freecodecamp-html-fail'); 13 | 14 | tape('Get FCC Score', (t) => { 15 | const actual = getFccScore(fccCompleteHtmlString); 16 | t.equal(actual, '289', 'Function should return the FCC score from page scrape'); 17 | t.end(); 18 | }); 19 | 20 | tape('FCC Validation on HTML Page Crawls', (t) => { 21 | let actual = fccSectionValidator(fccCompleteHtmlString, htmlCss); 22 | t.ok(actual, 'Returns true when HTML/CSS section is done on dummy success data'); 23 | actual = fccSectionValidator(fccIncompleteHtmlString, htmlCss); 24 | t.notok(actual, 'Returns false when HTML/CSS section is done on dummy fail data'); 25 | 26 | actual = fccSectionValidator(fccCompleteHtmlString, basicJavaScript); 27 | t.ok(actual, 'Returns true when Basic JavaScript section is done on dummy success data'); 28 | actual = fccSectionValidator(fccIncompleteHtmlString, basicJavaScript); 29 | t.notok(actual, 'Returns false when Basic JavaScript section is done on dummy fail data'); 30 | 31 | actual = fccSectionValidator(fccCompleteHtmlString, oOFunctionalProgramming); 32 | t.ok(actual, 'Returns true when Object Oriented & Functional Programming section is done on dummy success data'); 33 | actual = fccSectionValidator(fccIncompleteHtmlString, oOFunctionalProgramming); 34 | t.notok(actual, 'Returns false when Object Oriented & Functional Programming section is done on dummy fail data'); 35 | 36 | actual = fccSectionValidator(fccCompleteHtmlString, basicScripting); 37 | t.ok(actual, 'Returns true when Basic Algorithm Scripting section is done on dummy success data'); 38 | actual = fccSectionValidator(fccIncompleteHtmlString, basicScripting); 39 | t.notok(actual, 'Returns false when Basic Algorithm Scripting section is done on dummy fail data'); 40 | t.end(); 41 | }); 42 | 43 | tape('FCC Crawl: getFreeCodeCamp valid username', (t) => { 44 | nock('https://www.freecodecamp.org/') 45 | .get('/astroash') 46 | .reply(200, fccCompleteHtmlString); 47 | getFreeCodeCamp('astroash') 48 | .then((actual) => { 49 | t.deepEqual(actual, { 50 | success: true, 51 | score: '289', 52 | htmlCss: true, 53 | basicJavaScript: true, 54 | oOFunctionalProgramming: true, 55 | basicScripting: true, 56 | complete: true, 57 | handle: 'astroash' 58 | }, 'getFreeCodeCamp returns an obect for a valid username'); 59 | t.end(); 60 | }); 61 | }); 62 | 63 | tape('FCC Crawl: getFreeCodeCamp invalid username', (t) => { 64 | nock('https://www.freecodecamp.org/') 65 | .get('/astroashaaa') 66 | .reply(200, fccIncompleteHtmlString); 67 | getFreeCodeCamp('astroash') 68 | .then((actual) => { 69 | t.deepEqual(actual, { 70 | success: false, 71 | message: 'User not found', 72 | }, 'getFreeCodeCamp returns an obect for a invalid username'); 73 | t.end(); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /tests/get-cookies.helper.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const nock = require('nock'); 3 | const app = require('../app'); 4 | 5 | // getCookies uses the github-auth module to get a valid session/cookie. 6 | // Steps: 7 | // request to /auth: /auth?code=3aa491426dc2f4130a6b 8 | // intercept outgoing call to: https://github.com/login/oauth/access_token 9 | // respond with { access_token: 'token'} 10 | // intercept outgoing call to: https://api.github.com/user 11 | // respond with object { login: 'ghusername' } 12 | // intercept outgoing call to: https://api.github.com/teams/* 13 | // respond with respond with 200 and array of team members containing the ghHandle arg (to grant this user team member privileges) if isInTeam === true, else respond with 400 14 | // grab cookie and set on request to restricted route 15 | 16 | const getCookies = (ghHandle, isInTeam, cb) => { 17 | nock('https://github.com/login/oauth/access_token') 18 | .get(/.*/) 19 | .reply(200, { access_token: 'token' }); 20 | 21 | nock('https://api.github.com/user') 22 | .get(/.*/) 23 | .reply(200, { login: ghHandle }); 24 | 25 | nock('https://api.github.com/teams') 26 | .get(/.*/) 27 | .reply(() => { 28 | if (isInTeam) { 29 | return [ 30 | 200, 31 | [ 32 | { 33 | login: ghHandle, 34 | }, 35 | ], 36 | ]; 37 | } 38 | return [400]; 39 | }); 40 | 41 | request(app) 42 | .get('/auth?code=3aa491426dc2f4130a6b') 43 | .end((err, res) => { 44 | if (err) { 45 | console.error('error in getCookies: ', err); 46 | return; 47 | } 48 | cb(res.headers['set-cookie']); // 49 | }); 50 | }; 51 | 52 | module.exports = getCookies; 53 | -------------------------------------------------------------------------------- /tests/github-auth-route.test.js: -------------------------------------------------------------------------------- 1 | const tape = require('tape'); 2 | const nock = require('nock'); 3 | const request = require('supertest'); 4 | const app = require('../app'); 5 | const { isInTeam } = require('../routes/github-auth'); 6 | 7 | const teamMembersArray = [ 8 | { 9 | login: 'lucy-in-the-sky', 10 | }, 11 | { 12 | login: 'lucy-lu', 13 | }, 14 | ]; 15 | 16 | const isInTeamUser = 'lucy-lu'; 17 | const notInTeamUser = 'lucy'; 18 | 19 | tape('Test isInTeam pure function', (t) => { 20 | t.equal(isInTeam(teamMembersArray, isInTeamUser), true, 'If user is in the team should return true'); 21 | t.equal(isInTeam(teamMembersArray, notInTeamUser), false, 'If user is not in the team should return false'); 22 | t.end(); 23 | }); 24 | 25 | tape('Test githubAuth: success case for non-team member', (t) => { 26 | nock('https://github.com/login/oauth/access_token') 27 | .get(/.*/) 28 | .reply(200, { access_token: 'token' }); 29 | 30 | nock('https://api.github.com/user') 31 | .get(/.*/) 32 | .reply(200, { login: 'astroash' }); 33 | 34 | nock('https://api.github.com/teams') 35 | .get(/.*/) 36 | .reply(400); 37 | 38 | request(app) 39 | .get('/auth?code=3aa491426dc2f4130a6b') 40 | .end((err, res) => { 41 | t.equal(res.statusCode, 302, 'Status code should be 302'); 42 | t.equal(res.headers['set-cookie'].length, 2, 'Should have 2 cookies'); 43 | t.end(); 44 | }); 45 | }); 46 | 47 | tape('Test githubAuth: failure case', (t) => { 48 | request(app) 49 | .get('/auth') 50 | .end((err, res) => { 51 | t.equal(res.statusCode, 302, 'Status code should be 302'); 52 | t.ok(!res.headers['set-cookie'], 'Should not have cookies'); 53 | t.end(); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /tests/github-commits-api.test.js: -------------------------------------------------------------------------------- 1 | const tape = require('tape'); 2 | const nock = require('nock'); 3 | const { getGithubCommits, getRepoName } = require('../model/github-commits-api'); 4 | const githubResponseSuccess = require('./dummy-data/github-commits-api-success.json'); 5 | 6 | tape('getRepoName test', (t) => { 7 | const longUrl = 'https://bartbucknill.github.io/fac-application/'; 8 | const shortUrl = 'https://bartbucknill.github.io'; 9 | t.equal(getRepoName(longUrl), 'fac-application', 'Returns the repo name for non-user url'); 10 | t.equal(getRepoName(shortUrl), 'bartbucknill.github.io', 'Returns the repo name for user url'); 11 | t.end(); 12 | }); 13 | 14 | tape('getGithubCommits success', (t) => { 15 | nock('https://api.github.com/repos') 16 | .get('/bartbucknill/fac-application/commits') 17 | .reply(200, githubResponseSuccess); 18 | getGithubCommits('bartbucknill', 'https://bartbucknill.github.io/fac-application/') 19 | .then((actual) => { 20 | t.deepEqual(actual, { success: true, commits: 30 }, 'should return correct object'); 21 | t.end(); 22 | }); 23 | }); 24 | 25 | tape('getGithubCommits failure', (t) => { 26 | nock('https://api.github.com/repos') 27 | .get('/bartbucknill/fac-applicationxxx/commits') 28 | .reply(404); 29 | getGithubCommits('bartbucknill', 'https://bartbucknill.github.io/fac-applicationxxx/') 30 | .then((actual) => { 31 | t.deepEqual(actual, { success: false, message: 'Fetching Github commit count failed' }, 'should return error object'); 32 | t.end(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/github-repos-api.test.js: -------------------------------------------------------------------------------- 1 | const tape = require('tape'); 2 | const nock = require('nock'); 3 | const { getGithubRepos } = require('../model/github-repo-api'); 4 | const githubResponseSuccess = require('./dummy-data/github-repos-api-success.json'); 5 | 6 | tape('getGithubRepos success', (t) => { 7 | nock('https://api.github.com/users') 8 | .get('/astroash') 9 | .reply(200, githubResponseSuccess); 10 | getGithubRepos('astroash') 11 | .then((actual) => { 12 | t.deepEqual(actual, { success: true, repos: 12 }, 'should return correct object'); 13 | t.end(); 14 | }); 15 | }); 16 | 17 | tape('getGithubRepos fail', (t) => { 18 | nock('https://api.github.com/users') 19 | .get('/astroashaaa') 20 | .reply(404); 21 | getGithubRepos('astroashaaa') 22 | .then((actual) => { 23 | t.deepEqual(actual, { success: false, message: 'Fetching Github repo count failed' }, 'should return correct object'); 24 | t.end(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/github.test.js: -------------------------------------------------------------------------------- 1 | const tape = require('tape'); 2 | const nock = require('nock'); 3 | 4 | const { getGithubPage } = require('../model/github-page'); 5 | 6 | tape('getGithubPage with status code 404', (t) => { 7 | const url = 'https://dangerdak.github.io/effectivealtruisms/'; 8 | nock('https://dangerdak.github.io') 9 | .get('/effectivealtruisms') 10 | .reply(404); 11 | getGithubPage(url) 12 | .then((githubObj) => { 13 | t.notOk(githubObj.success, 'Returns object with success false'); 14 | t.equal(githubObj.statusCode, 404, 'Returns object with correct statusCode'); 15 | t.end(); 16 | }); 17 | }); 18 | 19 | tape('getGithubPage with status code 200', (t) => { 20 | const url = 'https://dangerdak.github.io/effectivealtruism'; 21 | nock('https://dangerdak.github.io') 22 | .get('/effectivealtruism') 23 | .reply(200); 24 | getGithubPage(url) 25 | .then((githubObj) => { 26 | t.equal(githubObj.url, url, 'Returns object with correct url value'); 27 | t.ok(githubObj.success, 'Returns object with success true'); 28 | t.end(); 29 | }); 30 | }); 31 | 32 | tape('getGithubPage not entered', (t) => { 33 | const url = ''; 34 | getGithubPage(url) 35 | .then((githubObj) => { 36 | t.notOk(githubObj.success, 'Returns object with success false'); 37 | t.equal(githubObj.message, 'No page entered', 'Returns object with correct message'); 38 | t.end(); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /tests/meetups.test.js: -------------------------------------------------------------------------------- 1 | const nock = require('nock'); 2 | const tape = require('tape'); 3 | const getMeetupCount = require('../model/meetups'); 4 | const { responseData, responseHeaders } = require('./dummy-data/meetups-googlesheet-success'); 5 | 6 | tape('getMeetupCount: failure case', (t) => { 7 | nock('https://spreadsheets.google.com') 8 | .get('/feeds/list/1_GpdOSpwivXZRZcMzJvz25K6u4j9B7SuWgvqeSwB6tk/o1az7e0/private/full?sq=githubnameunique=bartbucknill') 9 | .reply(404); 10 | getMeetupCount('bartbucknill') 11 | .then((actual) => { 12 | t.deepEqual(actual, { success: false, message: 'Unable to retrieve meetup data' }, 'should return correct object'); 13 | t.end(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /tests/scrape-links.test.js: -------------------------------------------------------------------------------- 1 | const test = require('tape'); 2 | const request = require('supertest'); 3 | const nock = require('nock'); 4 | 5 | const app = require('../app'); 6 | 7 | test('Test /scrape-links endpoint with invalid URL', (t) => { 8 | nock('https://faccer.github.io') 9 | .get('/') 10 | .reply(404, { 11 | status: 404, 12 | message: 'This is a mocked response', 13 | }); 14 | 15 | request(app) 16 | .get('/scrape-links?githubPage=faccer.github.io&submit=Submit') 17 | .expect(200) 18 | .expect('Content-Type', /json/) 19 | .end((err, res) => { 20 | t.same(res.statusCode, 200, 'Status code is 200 as Error is caught for unknown page'); 21 | t.ok(res.text.includes('Sorry this URL could not be found'), 'Error message is displayed on the front end to show URL could not be found'); 22 | t.end(); 23 | }); 24 | }); 25 | 26 | test('Test /scrape-links endpoint with valid URL', (t) => { 27 | nock('https://validuser.github.io') 28 | .get('/') 29 | .reply(200, 'github.com/usernamegh freecodecamp.org/usernamefcc codewars.com/users/usernamecw'); 30 | 31 | request(app) 32 | .get('/scrape-links?githubPage=validuser.github.io&submit=Submit') 33 | .expect(200) 34 | .expect('Content-Type', /json/) 35 | .end((err, res) => { 36 | t.same(res.statusCode, 200, 'Status code is 200'); 37 | t.ok(res.text.includes('value="https://validuser.github.io"'), 'Html form populates with Github page url value'); 38 | t.ok(res.text.includes('value="usernamefcc"'), 'Html form populates with freeCodeCamp username value'); 39 | t.ok(res.text.includes('value="usernamecw"'), 'Html form populates with Codewars username value'); 40 | t.ok(res.text.includes('value="usernamegh"'), 'Html form populates with Github username value'); 41 | t.end(); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /tests/src.test.js: -------------------------------------------------------------------------------- 1 | const test = require('tape'); 2 | const request = require('supertest'); 3 | const nock = require('nock'); 4 | const getCookies = require('./get-cookies.helper'); 5 | 6 | const app = require('../app'); 7 | 8 | test('Test home route', (t) => { 9 | request(app) 10 | .get('/') 11 | .expect('Content-Type', /text\/html/) 12 | .end((err, res) => { 13 | t.same(res.statusCode, 200, 'Status code is 200'); 14 | t.error(err, 'No error'); 15 | t.end(); 16 | }); 17 | }); 18 | 19 | 20 | test('Test /links', (t) => { 21 | request(app) 22 | .get('/links') 23 | .expect('Content-Type', /text\/html/) 24 | .end((err, res) => { 25 | t.same(res.statusCode, 200, 'Status code is 200'); 26 | t.error(err, 'No error'); 27 | t.end(); 28 | }); 29 | }); 30 | 31 | test('Test /links-validate', (t) => { 32 | request(app) 33 | .get('/links-validate') 34 | .expect('Content-Type', /text\/html/) 35 | .end((err, res) => { 36 | t.same(res.statusCode, 200, 'Status code is 200'); 37 | t.error(err, 'No error'); 38 | t.end(); 39 | }); 40 | }); 41 | 42 | test('Test /scrape-links', (t) => { 43 | request(app) 44 | .get('/scrape-links?githubPage=astroash.github.io') 45 | .expect('Content-Type', /text\/html/) 46 | .end((err, res) => { 47 | t.same(res.statusCode, 200, 'Status code is 200'); 48 | t.error(err, 'No error'); 49 | t.end(); 50 | }); 51 | }); 52 | 53 | test('Test /report without querystring', (t) => { 54 | request(app) 55 | .get('/report') 56 | .expect('Content-Type', /text\/plain/) 57 | .end((err, res) => { 58 | t.same(res.statusCode, 302, 'Redirects'); 59 | t.error(err, 'No error'); 60 | t.end(); 61 | }); 62 | }); 63 | 64 | test('Test /report with querystring, non team member requesting own report', (t) => { 65 | 66 | nock('https://api.github.com/repos') 67 | .get('/astroash.github.io') 68 | .reply(200); 69 | 70 | nock('https://www.codewars.com/') 71 | .get('/api/v1/users/astroash/code-challenges/authored/') 72 | .reply(200); 73 | 74 | nock('https://www.freecodecamp.org/') 75 | .get('/astroash') 76 | .reply(200); 77 | 78 | nock('https://api.github.com/repos') 79 | .get('/astroash.github.io/commits') 80 | .reply(200); 81 | 82 | nock('https://spreadsheets.google.com') 83 | .get('/feeds/list/1_GpdOSpwivXZRZcMzJvz25K6u4j9B7SuWgvqeSwB6tk/o1az7e0/private/full?sq=githubnameunique=astroash') 84 | .reply(404); 85 | 86 | getCookies('astroash', false, (cookies) => { 87 | request(app) 88 | .get('/report?githubPage=astroash.github.io&fccHandle=astroash&cwHandle=astroash&ghHandle=astroash') 89 | .set('Cookie', cookies) 90 | .expect('Content-Type', /text\/html/) 91 | .end((err, res) => { 92 | t.same(res.statusCode, 200, 'Responds with 200'); 93 | t.error(err, 'No error'); 94 | t.end(); 95 | }); 96 | }); 97 | }); 98 | 99 | test('Test /report with querystring, team member requesting other user\'s report', (t) => { 100 | 101 | nock('https://api.github.com/repos') 102 | .get('/astroash.github.io') 103 | .reply(200); 104 | 105 | nock('https://www.codewars.com/') 106 | .get('/api/v1/users/astroash/code-challenges/authored/') 107 | .reply(200); 108 | 109 | nock('https://www.freecodecamp.org/') 110 | .get('/astroash') 111 | .reply(200); 112 | 113 | nock('https://api.github.com/repos') 114 | .get('/astroash.github.io/commits') 115 | .reply(200); 116 | 117 | nock('https://spreadsheets.google.com') 118 | .get('/feeds/list/1_GpdOSpwivXZRZcMzJvz25K6u4j9B7SuWgvqeSwB6tk/o1az7e0/private/full?sq=githubnameunique=astroash') 119 | .reply(404); 120 | 121 | getCookies('dan', true, (cookies) => { 122 | request(app) 123 | .get('/report?githubPage=astroash.github.io&fccHandle=astroash&cwHandle=astroash&ghHandle=astroash') 124 | .set('Cookie', cookies) 125 | .expect('Content-Type', /text\/html/) 126 | .end((err, res) => { 127 | t.same(res.statusCode, 200, 'Responds with 200'); 128 | t.error(err, 'No error'); 129 | t.end(); 130 | }); 131 | }); 132 | }); 133 | 134 | test('Test /report with querystring, non-team member requesting other user\'s report', (t) => { 135 | 136 | getCookies('lucy', false, (cookies) => { 137 | request(app) 138 | .get('/report?githubPage=astroash.github.io&fccHandle=astroash&cwHandle=astroash&ghHandle=astroash') 139 | .set('Cookie', cookies) 140 | .end((err, res) => { 141 | t.same(res.statusCode, 302, 'Responds with 302'); 142 | t.error(err, 'No error'); 143 | t.end(); 144 | }); 145 | }); 146 | }); 147 | 148 | test('Page not found error route', (t) => { 149 | request(app) 150 | .get('/wigwammyzzz') 151 | .expect('Content-Type', /text\/html/) 152 | .end((err, res) => { 153 | t.same(res.statusCode, 404, 'Status code is 404'); 154 | t.ok(res.text.includes(res.statusCode), 'Rendered page contains status code'); 155 | t.end(); 156 | }); 157 | }); 158 | -------------------------------------------------------------------------------- /tests/validate-request.test.js: -------------------------------------------------------------------------------- 1 | const tape = require('tape'); 2 | 3 | const isValidRequest = require('../routes/validate-request'); 4 | 5 | tape('isValidRequest', (t) => { 6 | t.notOk( 7 | isValidRequest({ user: 'testuser', isInTeam: false }, { }), 8 | 'If query is empty should return false', 9 | ); 10 | 11 | t.ok( 12 | isValidRequest({ user: 'testuser', isInTeam: false }, { ghHandle: 'testuser' }), 13 | 'Normal user has permission to view their own info', 14 | ); 15 | 16 | t.notOk( 17 | isValidRequest({ user: 'testuser', isInTeam: false }, { ghHandle: 'otheruser' }), 18 | 'Normal user doesn\'t have permission to view other users info', 19 | ); 20 | 21 | t.ok( 22 | isValidRequest({ user: 'testuser', isInTeam: true }, { ghHandle: 'otheruser' }, ['admin', 'anotherAdmin']), 23 | 'Admin user has permission to view other users', 24 | ); 25 | 26 | t.ok( 27 | isValidRequest({ user: 'testuser', isInTeam: true }, { ghHandle: 'testuser' }), 28 | 'Admin user has permission to view their own info', 29 | ); 30 | t.end(); 31 | }); 32 | 33 | -------------------------------------------------------------------------------- /tests/w3-validator.test.js: -------------------------------------------------------------------------------- 1 | const test = require('tape'); 2 | const nock = require('nock'); 3 | const { getNumberOfErrors, getW3Validator } = require('../model/w3-validator'); 4 | const w3ValidatorSuccessData = require('./dummy-data/w3-validator-success.json'); 5 | 6 | test('W3 Validator getNumberOfErrors', (t) => { 7 | const actual = getNumberOfErrors(w3ValidatorSuccessData.messages); 8 | t.equal(typeof actual, 'number', 'should return a number'); 9 | t.equal(actual, 9, 'should return 9'); 10 | t.end(); 11 | }); 12 | 13 | test('W3 Validator: getW3Validator success', (t) => { 14 | nock('http://validator.w3.org/nu') 15 | .get('/?doc=http://www.astroash.com/&out=json') 16 | .reply(200, w3ValidatorSuccessData); 17 | 18 | getW3Validator('http://www.astroash.com') 19 | .then((actual) => { 20 | t.deepEqual(actual, { 21 | success: true, 22 | errors: 9, 23 | other: 0, 24 | url: 'http://validator.w3.org/nu/?doc=http://www.astroash.com', 25 | }, 'returns correct object'); 26 | t.end(); 27 | }); 28 | }); 29 | 30 | test('W3 Validator: getW3Validator fail', (t) => { 31 | nock('http://validator.w3.org/nu') 32 | .get('/?doc=http://www.astroash.com/&out=json') 33 | .reply(404); 34 | 35 | getW3Validator('http://www.astroash.com') 36 | .then((actual) => { 37 | t.deepEqual(actual, { 38 | success: false, 39 | message: 'Error retrieving data from W3 Validator', 40 | }, 'returns correct object'); 41 | t.end(); 42 | }); 43 | }); -------------------------------------------------------------------------------- /views/error.hbs: -------------------------------------------------------------------------------- 1 |

{{message}}

2 |

{{error.status}}

3 | -------------------------------------------------------------------------------- /views/layouts/main.hbs: -------------------------------------------------------------------------------- 1 | {{>htmlHead}} 2 | 3 | 4 | {{{body}}} 5 | 6 | 7 | -------------------------------------------------------------------------------- /views/login.hbs: -------------------------------------------------------------------------------- 1 |
11 | -------------------------------------------------------------------------------- /views/partials/animation.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | -------------------------------------------------------------------------------- /views/partials/codewars.hbs: -------------------------------------------------------------------------------- 1 |
2 |

Codewars

3 |
    4 | {{>codewarsSummary}} 5 |
6 | 7 |
    8 | {{#each codewarsKatas}} 9 | {{#if this.success}} 10 |
  • 11 | Kata Name: {{this.name}} 12 |
  • 13 |
  • 14 | {{#unless this.beta}} 15 | Kata Rank: {{this.rank}} 16 | {{else}} 17 | Kata in Beta 18 | {{/unless}} 19 |
  • 20 |
  • 21 | Completions: {{this.completions}} 22 |
  • 23 | {{else}} 24 |
  • 25 | {{this.message}} 26 |
  • 27 | {{/if}} 28 | {{/each}} 29 |
30 |
    31 |
  • 32 | {{codewars.kyu}} Kyu 33 |
  • 34 |
  • 35 | {{codewars.honor}} Honor 36 |
  • 37 |
38 |
39 | -------------------------------------------------------------------------------- /views/partials/codewarsSummary.hbs: -------------------------------------------------------------------------------- 1 |
  • 2 | {{#unless codewars.success}} 3 | 4 |

    Codewars: {{codewars.message}}

    5 |
    6 | {{/unless}} 7 | {{#if codewars.success}} 8 | {{#if codewars.achieved5Kyu}} 9 | Completed 10 | 11 |

    Codewars: 5 Kyu or better

    12 |
    13 | {{else}} 14 | Not completed 15 | 16 |

    Codewars: 5 Kyu or better

    17 |
    18 | {{/if}} 19 | {{/if}} 20 |
  • 21 |
  • 22 | {{#unless codewars.success}} 23 | 24 |

    Codewars: {{codewars.message}}

    25 |
    26 | {{/unless}} 27 | {{#if codewars.success}} 28 | {{#if codewars.hasAuthored}} 29 | Completed 30 | {{else}} 31 | Not completed 32 | {{/if}} 33 | 34 |

    Codewars: authored 1+ Kata

    35 |
    36 | {{/if}} 37 | 38 |
  • 39 | -------------------------------------------------------------------------------- /views/partials/freecodecamp.hbs: -------------------------------------------------------------------------------- 1 |
    2 |

    freeCodeCamp

    3 |

    freeCodeCamp: 4 sections

    4 |
      5 | {{#unless freeCodeCamp.success}} 6 |
    • 7 |

      User not found

      8 |
    • 9 | {{/unless}} 10 | {{#if freeCodeCamp.success}} 11 | 12 |
    • 13 | {{#if freeCodeCamp.htmlCss}} 14 | Complete 15 | {{else}} 16 | Incomplete 17 | {{/if}} 18 |

      HTML5 & CSS

      19 |
    • 20 | 21 |
    • 22 | {{#if freeCodeCamp.basicJavaScript}} 23 | Complete 24 | {{else}} 25 | Incomplete 26 | {{/if}} 27 |

      Basic JavaScript

      28 |
    • 29 | 30 |
    • 31 | {{#if freeCodeCamp.oOFunctionalProgramming}} 32 | Complete 33 | {{else}} 34 | Incomplete 35 | {{/if}} 36 |

      Object Oriented & Functional Programming

      37 |
    • 38 | 39 |
    • 40 | {{#if freeCodeCamp.basicScripting}} 41 | Complete 42 | {{else}} 43 | Incomplete 44 | {{/if}} 45 |

      Basic Algorithm Scripting

      46 |
    • 47 | 48 | {{/if}} 49 |
    50 |

    [ {{freeCodeCamp.score}} ]

    51 | 52 |
    53 | -------------------------------------------------------------------------------- /views/partials/github.hbs: -------------------------------------------------------------------------------- 1 |
    2 |

    Website & GitHub

    3 |
      4 | {{>githubSummary}} 5 |
    6 | 7 | Go to site 8 |
      9 | {{#if githubCommits.success}} 10 | 14 | {{else}} 15 | 18 | {{/if}} 19 | {{#unless w3Validation.success}} 20 | 23 | {{/unless}} 24 | {{#if w3Validation.success}} 25 | 31 | {{/if}} 32 | {{#if githubRepos.success}} 33 | 37 | {{else}} 38 | 41 | {{/if}} 42 |
    43 | {{>githubCalendar}} 44 |
    45 | -------------------------------------------------------------------------------- /views/partials/githubCalendar.hbs: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 11 | 12 | 13 |
    14 | 15 | Loading the data just for you. 16 |
    17 |

    Scroll >>

    18 | 21 | -------------------------------------------------------------------------------- /views/partials/githubSummary.hbs: -------------------------------------------------------------------------------- 1 |
  • 2 | {{#if githubPage.success}} 3 | Completed 4 | 5 |

    Website on GitHub Pages

    6 |
    7 | {{else}} 8 | 9 |

    GitHub Page: {{githubPage.message}}

    10 |
    11 | {{/if}} 12 |
  • 13 | -------------------------------------------------------------------------------- /views/partials/htmlHead.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | prereqCheck 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /views/partials/meetup.hbs: -------------------------------------------------------------------------------- 1 |
    2 |

    Meetup

    3 | {{#if meetups.success}} 4 |

    {{meetups.count}}

    5 |

    meetups

    6 |

    But don't worry if you've struggled to attend!

    7 | {{else}} 8 |

    {{meetups.message}}

    9 | {{/if}} 10 |
    11 | -------------------------------------------------------------------------------- /views/partials/navbar.hbs: -------------------------------------------------------------------------------- 1 | 35 | -------------------------------------------------------------------------------- /views/partials/summary.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{>animation}} 3 |

    Summary

    4 | 28 | Edit applicant details 29 |
    30 | 31 | -------------------------------------------------------------------------------- /views/report.hbs: -------------------------------------------------------------------------------- 1 | {{>navbar}} 2 | {{>summary}} 3 | {{>github}} 4 | {{>freecodecamp}} 5 | {{>codewars}} 6 | {{>meetup}} 7 | -------------------------------------------------------------------------------- /views/scrape-form.hbs: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /views/validate-form.hbs: -------------------------------------------------------------------------------- 1 | 22 | --------------------------------------------------------------------------------