Created
May 22, 2019 17:49
-
-
Save dustinheestand/91d985126852187df780d1dae4298a46 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/***************************************************************************** | |
This is a Node.js script. It requires any supported version of Node, i.e. | |
Node LTS 8, LTS 10, or 12. | |
Node may be downloaded here: https://nodejs.org/en/. | |
The script has been tested on Ubuntu Linux and on Windows. | |
Once you have installed Node, save this file where you wish the output to | |
reside and run | |
`node challenge.js [-f filename.csv]` | |
from your console. If no filename is specified, the output will be saved | |
as "SPR-challenge.csv". | |
*****************************************************************************/ | |
const https = require("https"); | |
const fs = require("fs"); | |
// CSV manipulation utilities | |
// Structure of API response is known; filename is unknown | |
// This returns the data from one CSV file, irrespective of | |
// whether the gist contains multiple files | |
const getCsvString = resJson => { | |
const files = resJson["files"]; | |
const csvName = Object.keys(files).find(k => k.endsWith(".csv")); | |
return files[csvName]["content"]; | |
}; | |
const parseCsvString = data => | |
data | |
.trim() | |
.split("\n") | |
.map(r => r.split(",")); | |
// Structure of table not known; assume last name field will contain "last." | |
// If no such field is found, data will be returned unsorted. | |
const sortCsvData = ([headers, ...data]) => { | |
const lastNameIndex = headers.findIndex(x => /last/i.test(x)); | |
return lastNameIndex > -1 | |
? [headers].concat( | |
// Use localeCompare in case data contains non-ASCII characters | |
data.sort((a, b) => a[lastNameIndex].localeCompare(b[lastNameIndex])) | |
) | |
: headers.concat(data); | |
}; | |
const addColumns = ([headers, ...data]) => | |
[headers.concat(['"Password"', '"UPN"'])].concat( | |
data.map(([id, first, last]) => [ | |
id, | |
first, | |
last, | |
/* WARNING ******* PASSWORDS IN PLAINTEXT PER SPEC ******* WARNING */ | |
makePassword(), | |
`"${rmQuotes(first)}.${rmQuotes(last)}@gmail.com"` | |
]) | |
); | |
const getCsvArray = resJson => | |
addColumns(sortCsvData(parseCsvString(getCsvString(resJson)))); | |
const rmQuotes = quotedStr => quotedStr.replace(/\"/g, ""); | |
// Password randomizer: | |
// Passwords must contain 3 of the following character types: | |
// lowercase, uppercase, digits, and special characters. | |
const makePassword = () => { | |
const specials = "!@#$%^&*"; | |
let pass = Math.random() | |
.toString(36) | |
.slice(2); | |
// So that we start with a letter as is usual for passwords | |
pass = pass.replace(/^\d+/, "").split(""); | |
pass[randCharPosition(pass) + 1] = specials[randCharPosition(specials)]; | |
pass[randCharPosition(pass)] = pass[randCharPosition(pass)].toUpperCase(); | |
pass[randCharPosition(pass)] = pass[randCharPosition(pass)].toUpperCase(); | |
// So that passwords are not all the same length, but at least 8 characters | |
pass = pass.join("").slice(0, Math.floor(Math.random() * 4) + 9); | |
// Random process may return something not meeting the criteria; if not, rerun | |
return /[a-z]/.test(pass) + | |
/[A-Z]/.test(pass) + | |
/[!@#$%^&*]/.test(pass) + | |
/\d/.test(pass) >= | |
3 | |
? '"' + pass + '"' | |
: makePassword(); | |
}; | |
randCharPosition = str => Math.floor(Math.random() * str.length); | |
// Turn the CSV data from array into a string and write to file | |
// specified in the argument following the -f flag | |
const writeFile = (finalCsvArray, args) => { | |
filename = | |
args.indexOf("-f") > -1 | |
? args[args.indexOf("-f") + 1] | |
: "SPR-challenge.csv"; | |
// Synchronous as don't want script to end until this is complete | |
fs.writeFileSync( | |
filename, | |
finalCsvArray.map(a => a.join(",")).join("\n") + "\n" | |
); | |
}; | |
// API call | |
const options = { | |
// GitHub requests I use my username; fine by me | |
headers: { "User-Agent": "dustinheestand" }, | |
hostname: "api.github.com", | |
path: "/gists/6bb69329f50efb7b79f7e5a2bf31597d" | |
}; | |
const req = https.get(options, res => { | |
data = []; | |
res.setEncoding("UTF-8"); | |
res.on("data", d => { | |
data.push(d); | |
}); | |
res.on("end", () => { | |
//Don't need the data as array, so I'll shadow it | |
data = JSON.parse(data); | |
const origData = parseCsvString(getCsvString(data)); | |
const finalData = getCsvArray(data); | |
if (testsSucceed(origData, finalData)) { | |
writeFile(finalData, process.argv); | |
} else { | |
console.error("Data was changed - review script for errors!"); | |
} | |
}); | |
}); | |
req.on("error", e => console.error(e)); | |
req.end(); | |
// Rudimentary tests to make sure input is not being corrupted | |
// Not pulling in any frameworks as I don't want user to need to install packages | |
const arrayDeepEq = (arr1, arr2) => arr1.every((e, i) => e === arr2[i]); | |
const findRowById = (rows, id) => rows.find(r => r[0] === id); | |
const testsSucceed = (origData, finalData) => | |
origData.length === finalData.length && | |
finalData.every(r => r.length === origData[0].length + 2) && | |
finalData | |
.map(r => r.slice(0, 3)) | |
.every(r => arrayDeepEq(r, findRowById(origData, r[0]))); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment