UNPKG

mochapi

Version:
357 lines (295 loc) 11.3 kB
// Copyright 2015 The Home Depot // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import http = require("http"); import fs = require("fs"); import URL = require("url"); import queryString = require("query-string"); import formidable = require("formidable"); import pjson = require("pjson"); import {parse} from "jsonplus"; // Colors updates the String object import "colors"; interface EndpointResponse { response: any; status: number; } interface EndpointDetails { GET: EndpointResponse; POST: EndpointResponse; PUT: EndpointResponse; DELETE: EndpointResponse; OPTIONS: EndpointResponse; ALL: EndpointResponse; [method: string]: EndpointResponse; } interface EndpointMap { [url: string]: EndpointDetails; default404: EndpointDetails; } interface MapSearchResult { url?: string; fixture?: string; status?: number; notFound?: boolean; } interface NormalizedURL { original?: string; trailing?: string; noTrailing?: string; } export class Mapi { map: EndpointMap; constructor(args: string[]) { // Get args let dbFile = args[0], port = args[1] ? Number(args[1]) : 9000, hostname = args[2] || "localhost"; if (dbFile) { this.map = parse(this.readFile(args[0])); } else { this.usage("Please provide a DB"); return this.exit(1); } console.log("%s %s", "Mock server started".green, `http://${hostname}:${port}/_mapi/`.magenta.underline ); this.createServer(port, hostname); } /** * Exits application with given status */ exit(status: number): any { return process.exit(status); } /** * Creates a server */ createServer(port: number, hostname: string): void { http.createServer(this.server.bind(this)).listen(port, hostname); } /** * Prints the usage information */ usage(errorMessage: string = ""): void { console.log(errorMessage.red); console.log("Usage:".green.underline); console.log(" mapi db.json".yellow, " # Just point a file as database".grey); console.log(" mapi db.json 8080".yellow, " # You can set a port as well".grey); console.log(" mapi db.json 8080 127.0.0.1".yellow, " # You can set a hostname as well".grey); console.log("Version: %s".green, pjson.version); console.log("More details on %s".green, pjson.homepage); } /** * Reads the given file and returns it as a string */ readFile(fileName: string): string { let file; try { file = fs.readFileSync(fileName, { encoding: "utf-8" }); } catch (e) { this.usage(`Could not read ${fileName}`); return this.exit(1); } return file; } /** * Send a response to the request with given content and status code */ sendResponse(ServerResponse: http.ServerResponse, content: string, status: number = 200): http.ServerResponse { ServerResponse.writeHead(status, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" }); ServerResponse.end(content); return ServerResponse; } serveStatic(ServerResponse: http.ServerResponse, filename: string, mimeType: string): http.ServerResponse { let stats: fs.Stats, fileStream: fs.ReadStream; try { stats = fs.lstatSync(filename); // throws if path doesn't exist } catch (e) { ServerResponse.writeHead(404, { "Content-Type": "text/plain" }); ServerResponse.write("404 Not Found\n"); ServerResponse.end(); return ServerResponse; } if (stats.isFile()) { // path exists, is a file ServerResponse.writeHead(200, { "Content-Type": mimeType }); fileStream = fs.createReadStream(filename); fileStream.pipe(ServerResponse); } return ServerResponse; } /** * Writes an entry to server logs */ log(status: number, url: string, message: string = ""): string { console.log("- %s %s, %s", // Pick color for the status code (`[ ${status} ]`)[status === 200 ? "green" : "red"], url.yellow, message.grey); return message; } searchMapRegExp(url: NormalizedURL): EndpointDetails { // Determines whether url should be treated as a regular expression let rgxToken = ":", // Get all the keys to look for wildcards // TODO: Find all these keys during initialization and cache the results urls = Object.keys(this.map), entry: EndpointDetails; // Going through all endpoints, create a regexp from wildcards and // try to match them to URL provided. urls.forEach(endpoint => { // Detect regex if (endpoint.indexOf(":") === 0) { // Remove the rgxToken from the current endpoint let expression = endpoint.slice(1, endpoint.length).trim(); // Create a REGEXP from the current endpoint let rgx = new RegExp(expression, "gim"); if (rgx.test(url.noTrailing) || rgx.test(url.trailing)) { entry = this.map[endpoint]; } } // We have a wild card (not a regex) else if (endpoint.indexOf("*") !== -1) { // First sanitize all possible REGEXP signs let sanitized = endpoint.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"); // Then find sanitized * and replace it with URL ready wildcard let rgx = new RegExp(sanitized.replace(/\\\*/g, "([^\\/]*?)"), "gim"); // Try url with regexp, make sure to test // with trailing slash as well. if (rgx.test(url.noTrailing) || rgx.test(url.trailing)) { entry = this.map[endpoint]; } } }); return entry; } /** * Searches request URL in the endpoint map with given method. Returns information found. */ searchMap(url: NormalizedURL, method: string = "GET"): MapSearchResult { let entry: EndpointDetails, result: MapSearchResult = {}; entry = this.map[url.noTrailing] || this.map[url.trailing] || this.searchMapRegExp(url); // If url is not in the map if (entry === undefined) { result.notFound = true; } else { result = { url: url.original }; // Make sure data exists if (entry[method]) { result.fixture = entry[method].response; result.status = entry[method].status || 200; } else if (entry.ALL) { result.fixture = entry.ALL.response; result.status = entry.ALL.status || 200; } else { result.notFound = true; } } // Return the found response return result; } /** * Normalizes the url but creating trailing and non trailing slash * versions of it. it also contains the original url */ normalizeUrl(url: string): NormalizedURL { let cleaned: string, result: NormalizedURL = { original: url }, parsed: URL.Url; cleaned = url.replace(/\/+/g, "/").replace("/mapi", "/api"); parsed = URL.parse(cleaned); if (/\/$/.test(parsed.pathname)) { result.trailing = URL.format(parsed); parsed.pathname = parsed.pathname.replace(/\/$/, ""); result.noTrailing = URL.format(parsed); } else { result.noTrailing = URL.format(parsed); parsed.pathname = parsed.pathname + "/"; result.trailing = URL.format(parsed); } return result; } /** * Takes a normalized url and appends a given query string */ appendQueryString(url: NormalizedURL, query: string) { url.original += "\\?" + query; url.noTrailing += "?" + query; url.trailing += "?" + query; return url; } getEndpoint(ServerRequest: http.ServerRequest, callback: Function): void { let reqUrl = this.normalizeUrl(ServerRequest.url); if(ServerRequest.method === "POST") { let post = new formidable.IncomingForm(); post.parse(ServerRequest, function(err, params, files) { var query = queryString.stringify(params); if(query.length > 0) { //Assumes the POST url is a regex, so we escape the ? reqUrl = this.appendQueryString(reqUrl, query); } callback(this.searchMap(reqUrl, ServerRequest.method)); }.bind(this)); } else { callback(this.searchMap(reqUrl, ServerRequest.method)); } } /** * Handles the requests and sends response back accordingly. */ server(ServerRequest: http.ServerRequest, ServerResponse: http.ServerResponse): void { let response: string, status: number, logMessage: string, endpoint: MapSearchResult, reqUrl = this.normalizeUrl(ServerRequest.url); this.getEndpoint(ServerRequest, function(endpoint: MapSearchResult) { if (endpoint.notFound !== true) { // if url found in the endpoint map, display the // fixture data with status code response = JSON.stringify(endpoint.fixture); status = endpoint.status; logMessage = endpoint.url; } else if (reqUrl.noTrailing === "/favicon.ico") { // Serve this file statically return this.serveStatic(ServerResponse, "src/images/favicon.ico", "image/x-icon"); } else if (reqUrl.noTrailing === "/_mapi") { // for this url, display all mocked API Endpoints response = JSON.stringify(Object.keys(this.map)); status = 200; logMessage = "show all urls"; } else { if (this.map.default404) { response = this.map.default404.ALL.response; status = this.map.default404.ALL.status; logMessage = "default 404"; } else { // If URL was not found display 404 message response = `{ "error": "Could not find ${ reqUrl.original }" }`; status = 404; logMessage = "url not mapped"; } } this.log(status, reqUrl.original, logMessage); this.sendResponse(ServerResponse, response, status); }.bind(this)); } }