Skip to content

Instantly share code, notes, and snippets.

@dustinheestand
Created May 22, 2019 17:49
Show Gist options
  • Save dustinheestand/91d985126852187df780d1dae4298a46 to your computer and use it in GitHub Desktop.
Save dustinheestand/91d985126852187df780d1dae4298a46 to your computer and use it in GitHub Desktop.
/*****************************************************************************
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