all.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. /*
  2. * Basic caching tests
  3. */
  4. var fs = require('fs');
  5. var http = require('http');
  6. var path = require('path');
  7. var url = require('url');
  8. var util = require('util');
  9. var assert = require('assert-plus');
  10. var mkdirp = require('mkdirp');
  11. var rimraf = require('rimraf');
  12. var test = require('tape');
  13. var lib = require('./lib');
  14. var FsCachingServer = require('../').FsCachingServer;
  15. var f = util.format;
  16. var config = lib.readConfig();
  17. var cachingServerURL = f('http://%s:%d', config.cachingServer.host,
  18. config.cachingServer.port);
  19. var backendServerURL = f('http://%s:%d', config.backendServer.host,
  20. config.backendServer.port);
  21. var cachingServer;
  22. var backendServer;
  23. var dir = path.join(__dirname, 'tmp');
  24. /*
  25. * wrapper for making web requests to the caching server
  26. */
  27. function cacheRequest(p, opts, cb) {
  28. assert.string(p, 'p');
  29. assert.object(opts, 'opts');
  30. assert.string(opts.method, 'opts.method');
  31. assert.func(cb, 'cb');
  32. var uri = f('%s%s', cachingServerURL, p);
  33. var o = url.parse(uri);
  34. Object.keys(opts).forEach(function (key) {
  35. var val = opts[key];
  36. o[key] = val;
  37. });
  38. var req = http.request(o, function (res) {
  39. var data = '';
  40. res.setEncoding('utf-8');
  41. res.on('data', function (d) {
  42. data += d;
  43. });
  44. res.on('end', function () {
  45. cb(null, data, res);
  46. });
  47. res.once('error', function (err) {
  48. cb(err);
  49. });
  50. });
  51. req.end();
  52. }
  53. /*
  54. * start the caching frontend
  55. */
  56. test('start cachingServer', function (t) {
  57. var opts = {
  58. cacheDir: dir,
  59. host: config.cachingServer.host,
  60. port: config.cachingServer.port,
  61. backendUrl: backendServerURL
  62. };
  63. mkdirp.sync(dir);
  64. rimraf.sync(dir + '/*');
  65. t.pass(f('tmp dir "%s" cleared', dir));
  66. cachingServer = new FsCachingServer(opts);
  67. cachingServer.once('start', function () {
  68. t.pass('cachingServer started');
  69. t.end();
  70. });
  71. if (process.env.NODE_DEBUG) {
  72. cachingServer.on('log', console.log);
  73. }
  74. cachingServer.start();
  75. });
  76. /*
  77. * start the web server backend
  78. */
  79. test('start backendServer', function (t) {
  80. backendServer = http.createServer(onRequest);
  81. backendServer.listen(config.backendServer.port, config.backendServer.host,
  82. onListen);
  83. function onRequest(req, res) {
  84. var p = url.parse(req.url).pathname;
  85. var s = f('%s request by pid %d at %s\n', p, process.pid, Date.now());
  86. // handle: /statusCode/<num>
  87. var matches;
  88. if ((matches = p.match(/^\/statusCode\/([0-9]+)/))) {
  89. res.statusCode = parseInt(matches[1], 10);
  90. res.end(s);
  91. return;
  92. }
  93. switch (p) {
  94. case '/301.png':
  95. res.statusCode = 301;
  96. res.setHeader('Location', '/foo.png');
  97. res.end();
  98. break;
  99. case '/header.png':
  100. res.setHeader('x-fun-header', 'woo');
  101. res.end();
  102. default:
  103. res.end(s);
  104. break;
  105. }
  106. }
  107. function onListen() {
  108. t.pass(f('backendServer started on %s', backendServerURL));
  109. t.end();
  110. }
  111. });
  112. /*
  113. * Basic request that should be cached.
  114. */
  115. test('simple cached request', function (t) {
  116. var f = '/hello.png';
  117. cacheRequest(f, {method: 'GET'}, function (err, webData) {
  118. t.error(err, 'GET ' + f);
  119. cachingServer.onIdle(function () {
  120. // check to make sure the cache has this data
  121. var fileData = fs.readFileSync(path.join(dir, f), 'utf-8');
  122. t.equal(webData, fileData, 'file data in sync with web data');
  123. // request it again to ensure it's correct
  124. cacheRequest(f, {method: 'GET'}, function (err, webData2) {
  125. t.error(err, 'GET ' + f);
  126. t.equal(webData, webData2, 'both web requests same data');
  127. cachingServer.onIdle(function () {
  128. t.end();
  129. });
  130. });
  131. });
  132. });
  133. });
  134. /*
  135. * Basic request that should not be cached.
  136. */
  137. test('simple non-cached request', function (t) {
  138. var f = '/hello.txt';
  139. cacheRequest(f, {method: 'GET'}, function (err, webData) {
  140. t.error(err, 'GET ' + f);
  141. cachingServer.onIdle(function () {
  142. // check to make sure the cache DOES NOT have this data
  143. var file = path.join(dir, f);
  144. t.throws(function () {
  145. fs.statSync(file);
  146. }, file + ' should not exist');
  147. // request it again to ensure the data is different (time difference)
  148. cacheRequest(f, {method: 'GET'}, function (err, webData2) {
  149. t.error(err, 'GET ' + f);
  150. t.notEqual(webData, webData2, 'both web requests different data');
  151. cachingServer.onIdle(function () {
  152. t.end();
  153. });
  154. });
  155. });
  156. });
  157. });
  158. /*
  159. * Codes that *should not* be proxied.
  160. */
  161. test('statusCodes without proxy', function (t) {
  162. var codes = [301, 302, 403, 404, 500, 501, 502];
  163. var idx = 0;
  164. function go() {
  165. var code = codes[idx++];
  166. if (!code) {
  167. t.end();
  168. return;
  169. }
  170. var uri = f('/statusCode/%d/foo.png', code);
  171. cacheRequest(uri, {method: 'GET'}, function (err, webData) {
  172. t.error(err, 'GET ' + uri);
  173. t.equal(webData, '', 'webData should be empty from caching server');
  174. cachingServer.onIdle(function () {
  175. // ensure the file does NOT exist in the cache dir
  176. var file = path.join(dir, uri);
  177. t.throws(function () {
  178. fs.statSync(file);
  179. }, file + ' should not exist');
  180. go();
  181. });
  182. });
  183. }
  184. go();
  185. });
  186. /*
  187. * Codes that *should* be proxied.
  188. */
  189. test('statusCodes with proxy', function (t) {
  190. var codes = [200];
  191. var idx = 0;
  192. function go() {
  193. var code = codes[idx++];
  194. if (!code) {
  195. t.end();
  196. return;
  197. }
  198. var uri = f('/statusCode/%d/foo.png', code);
  199. cacheRequest(uri, {method: 'GET'}, function (err, webData) {
  200. t.error(err, 'GET ' + uri);
  201. t.ok(webData, 'webData should have data from server');
  202. cachingServer.onIdle(function () {
  203. // ensure the file exists with the corect data
  204. var file = path.join(dir, uri);
  205. var fileData = fs.readFileSync(file, 'utf-8');
  206. t.equal(webData, fileData, 'file data in sync with web data');
  207. go();
  208. });
  209. });
  210. }
  211. go();
  212. });
  213. /*
  214. * The first request to an item (cache-miss) will result in the request being
  215. * proxied directly to the backendServer. Subsequent requests will not be
  216. * proxied and instead will just be handed the cached file without any of the
  217. * original headers.
  218. */
  219. test('headers proxied only on first request', function (t) {
  220. var uri = '/header.png';
  221. var serverHeader = 'x-fun-header';
  222. // initial request (cache miss) should proxy headers from server
  223. cacheRequest(uri, {method: 'GET'}, function (err, webData, res) {
  224. t.error(err, 'GET ' + uri);
  225. var headers = res.headers;
  226. var customHeader = headers[serverHeader];
  227. t.equal(customHeader, 'woo', f('custom header %s seen', serverHeader));
  228. cachingServer.onIdle(function () {
  229. // second request (cache hit) won't remember headers from server
  230. cacheRequest(uri, {method: 'GET'}, function (err, webData, res) {
  231. t.error(err, 'GET ' + uri);
  232. var headers = res.headers;
  233. var customHeader = headers[serverHeader];
  234. t.ok(!customHeader, f('custom header %s not seen', serverHeader));
  235. cachingServer.onIdle(function () {
  236. t.end();
  237. });
  238. });
  239. });
  240. });
  241. });
  242. /*
  243. * FsCachingServer handles redirects by specifically choosing to not handle
  244. * them. Instead, the statusCodes and headers from the backendServer will be
  245. * sent directly to the caller, and it is up to that caller if they'd like to
  246. * follow the redirect. If the redirects eventually hit a GET or HEAD request
  247. * that falls within the 200 range, then it will be cached as normal.
  248. */
  249. test('301 redirect', function (t) {
  250. var uri = '/301.png';
  251. var redirect = '/foo.png';
  252. cacheRequest(uri, {method: 'GET'}, function (err, webData, res) {
  253. t.error(err, 'GET ' + uri);
  254. t.ok(!webData, 'body empty');
  255. t.equal(res.statusCode, 301, '301 seen');
  256. var headers = res.headers;
  257. var loc = headers.location;
  258. t.equal(loc, redirect, 'location is correct');
  259. cachingServer.onIdle(function () {
  260. t.end();
  261. });
  262. });
  263. });
  264. /*
  265. * Requesting a directory that exists in the cache should result in a 400.
  266. */
  267. test('GET directory in cache', function (t) {
  268. var uri = '/directory.png';
  269. fs.mkdirSync(path.join(dir, uri));
  270. cacheRequest(uri, {method: 'GET'}, function (err, webData, res) {
  271. t.error(err, 'GET ' + uri);
  272. t.ok(!webData, 'body empty');
  273. t.equal(res.statusCode, 400, '400 seen');
  274. cachingServer.onIdle(function () {
  275. t.end();
  276. });
  277. });
  278. });
  279. /*
  280. * Two simulataneous requests for a cache-miss. This will result in one of the
  281. * requests being responsible for downloading the file and getting it streamed
  282. * to them live, and the other request being paused until the data is fully
  283. * downloaded.
  284. *
  285. * To simulate this fs.stat will be artifically slown down so both requests
  286. * will block before the cache download begins.
  287. */
  288. test('Two simultaneous requests', function (t) {
  289. var originalStat = fs.stat.bind(fs);
  290. fs.stat = function slowStat(f, cb) {
  291. setTimeout(function () {
  292. originalStat(f, cb);
  293. }, 100);
  294. };
  295. var uri = '/simultaneous.png';
  296. var todo = 2;
  297. cacheRequest(uri, {method: 'GET'}, requestOne);
  298. setTimeout(function () {
  299. cacheRequest(uri, {method: 'GET'}, requestTwo);
  300. }, 30);
  301. var data1;
  302. var data2;
  303. function requestOne(err, data, res) {
  304. t.error(err, '1. GET ' + uri);
  305. data1 = data;
  306. finish();
  307. }
  308. function requestTwo(err, data, res) {
  309. t.error(err, '2. GET ' + uri);
  310. data2 = data;
  311. finish();
  312. }
  313. function finish() {
  314. if (--todo > 0) {
  315. return;
  316. }
  317. fs.stat = originalStat;
  318. cachingServer.onIdle(function () {
  319. t.equal(data1, data2, 'data the same');
  320. t.end();
  321. });
  322. }
  323. });
  324. /*
  325. * HEAD requests should only server cached results and not cache them itself.
  326. * i.e. if a HEAD request is seen first it should just be proxied to the
  327. * backend directly with no caching.
  328. */
  329. test('HEAD request', function (t) {
  330. var uri = '/head-cache-test.png';
  331. cacheRequest(uri, {method: 'HEAD'}, function (err, data, res) {
  332. t.error(err, 'HEAD ' + uri);
  333. cachingServer.onIdle(function () {
  334. // check to make sure the cache DOES NOT have this data
  335. var file = path.join(dir, uri);
  336. t.throws(function () {
  337. fs.statSync(file);
  338. }, file + ' should not exist');
  339. t.end();
  340. });
  341. });
  342. });
  343. /*
  344. * Close the backend HTTP server
  345. */
  346. test('close backendServer', function (t) {
  347. backendServer.once('close', function () {
  348. t.pass('backendServer closed');
  349. t.end();
  350. });
  351. backendServer.close();
  352. });
  353. /*
  354. * Stop the caching server
  355. */
  356. test('stop cachingServer', function (t) {
  357. cachingServer.once('stop', function () {
  358. t.pass('cachingServer stopped');
  359. t.end();
  360. });
  361. cachingServer.stop();
  362. });