initial commit
[urlittler] / test / src / manager.js
1 /* eslint-env mocha */
2 /* eslint-disable capitalized-comments */
3
4 'use strict';
5
6 const assert = require('assert');
7 const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-require
8
9 const Manager = require('../../src/manager');
10 const common = require('../../src/common');
11 const Enum = require('../../src/enum');
12 const { ServeStaticFile, SlugGeneratorExhausted } = require('../../src/errors');
13
14 const noExpectedException = 'did not get expected exception';
15
16 describe('Manager', function () {
17 let manager, options;
18 let res, ctx;
19
20 beforeEach(function () {
21 const stubLogger = {
22 debug: sinon.stub(),
23 error: sinon.stub(),
24 info: sinon.stub(),
25 };
26 const stubDb = {
27 context: async (fn) => await fn({}),
28 transaction: async (_dbCtx, fn) => await fn({}),
29 getLinkById: sinon.stub(),
30 accessLink: sinon.stub(),
31 getLinkByUrl: sinon.stub(),
32 expireLink: sinon.stub(),
33 insertLink: sinon.stub(),
34 getAllLinks: sinon.stub(),
35 };
36 options = {};
37 res = {
38 end: sinon.stub(),
39 setHeader: sinon.stub(),
40 };
41 ctx = {
42 params: {},
43 };
44 manager = new Manager(stubLogger, stubDb, options);
45
46 });
47 afterEach(function () {
48 sinon.restore();
49 });
50
51 it('instantiates', function () {
52 assert(manager);
53 });
54
55 it('defaults options', function () {
56 manager = new Manager({}, {});
57 });
58
59 describe('rootContent', function () {
60 it('generates content for empty context', function () {
61 const result = manager.rootContent(ctx);
62 assert(result.length);
63 });
64 it('generates json content', function () {
65 ctx.responseType = Enum.ContentType.ApplicationJson;
66 const result = manager.rootContent(ctx);
67 assert(result.length);
68 JSON.parse(result);
69 });
70 it('generates html content', function () {
71 ctx.responseType = Enum.ContentType.TextHTML;
72 const result = manager.rootContent(ctx);
73 assert(result.length);
74 });
75 it('includes context fields', function () {
76 ctx.createdLink = 'http://example.com/foo';
77 ctx.authToken = 'token';
78 ctx.message = 'message';
79 ctx.sourceLink = 'http://source.example.com/';
80 const result = manager.rootContent(ctx);
81 assert(result.length);
82 });
83 }); // rootContent
84
85 describe('getRoot', function () {
86 let req;
87 beforeEach(function () {
88 sinon.stub(common, 'isClientCached');
89 req = {};
90 });
91 it('normal response', async function () {
92 common.isClientCached.returns(false);
93 await manager.getRoot(req, res, ctx);
94 assert(res.end.called);
95 });
96 it('repeat response', async function () {
97 manager.startTime = (new Date()).toGMTString();
98 common.isClientCached.returns(true);
99 await manager.getRoot(req, res, ctx);
100 assert(res.end.called);
101 });
102 it('cached response', async function () {
103 common.isClientCached.returns(true);
104 await manager.getRoot(req, res, ctx);
105 assert(res.end.called);
106 assert.strictEqual(res.statusCode, 304);
107 });
108 }); // getRoot
109
110 describe('_getNewIdentifier', function () {
111 const url = 'http://example.com/bar';
112 let dbCtx, nextStub;
113 beforeEach(function () {
114 dbCtx = {};
115 nextStub = sinon.stub();
116 manager.db.getLinkById.onCall(0).resolves({ id:'existing' }).onCall(1).resolves();
117 sinon.stub(manager, 'makeSlugGenerator').callsFake(() => {
118 return {
119 next: nextStub,
120 };
121 });
122 });
123 it('gets identifiers', async function () {
124 nextStub.resolves({ value: 'slug', done: false });
125 const result = await manager._getNewIdentifier(dbCtx, url);
126 assert(result);
127 assert.strictEqual(nextStub.callCount, 2);
128 });
129 it('handles empty slug', async function () {
130 nextStub.resolves({ value: '', done: false });
131 try {
132 await manager._getNewIdentifier(dbCtx, url);
133 assert.fail(noExpectedException);
134 } catch (e) {
135 assert(e instanceof SlugGeneratorExhausted, noExpectedException);
136 }
137 });
138 it('handles end of generator', async function () {
139 nextStub.resolves({ value: 'blah', done: true });
140 try {
141 await manager._getNewIdentifier(dbCtx, url);
142 assert.fail(noExpectedException);
143 } catch (e) {
144 assert(e instanceof SlugGeneratorExhausted, noExpectedException);
145 }
146 });
147 }); // _getNewIdentifier
148
149 describe('_validateContextURL', function () {
150 it('allows admin to create local static link', function () {
151 ctx.sourceLink = `${manager.staticDirectory}file.txt`;
152 ctx.authenticationId = 'awoo';
153 manager._validateContextURL(ctx);
154 });
155 it('accepts valid url', function () {
156 ctx.sourceLink = 'http://example.com/file.txt';
157 manager._validateContextURL(ctx);
158 });
159 it('rejects missing url', function () {
160 try {
161 manager._validateContextURL(ctx);
162 assert.fail(noExpectedException);
163 } catch (e) {
164 assert.strictEqual(e.message, Enum.ErrorResponse.InvalidURLParameter.errorMessage, noExpectedException);
165 }
166 });
167 it('rejects invalid url', function () {
168 ctx.sourceLink = 'not a url';
169 try {
170 manager._validateContextURL(ctx);
171 assert.fail(noExpectedException);
172 } catch (e) {
173 assert.strictEqual(e.message, Enum.ErrorResponse.InvalidURLParameter.errorMessage, noExpectedException);
174 }
175 });
176 }); // _validateContextURL
177
178 describe('getById', function () {
179 let link;
180 beforeEach(function () {
181 link = undefined;
182 });
183 it('handles missing link', async function () {
184 try {
185 await manager.getById(res, ctx);
186 assert.fail(noExpectedException);
187 } catch (e) {
188 assert.strictEqual(e.message, 'Not Found');
189 }
190 });
191 it('handles expired link', async function () {
192 link = {
193 expires: 1,
194 };
195 manager.db.accessLink.resolves(link)
196 try {
197 await manager.getById(res, ctx);
198 assert.fail(noExpectedException);
199 } catch (e) {
200 assert.strictEqual(e.message, 'Gone');
201 }
202 });
203 it('handles local static link', async function () {
204 const file = 'foop.txt';
205 link = {
206 url: `${manager.staticDirectory}${file}`,
207 };
208 manager.db.accessLink.resolves(link);
209 try {
210 await manager.getById(res, ctx);
211 assert.fail(noExpectedException);
212 } catch (e) {
213 assert(e instanceof ServeStaticFile);
214 assert.strictEqual(e.file, file);
215 }
216 });
217 it('redirects a link', async function () {
218 link = {
219 url: 'http://example.com/awoo',
220 };
221 manager.db.accessLink.resolves(link);
222 await manager.getById(res, ctx);
223 assert.strictEqual(res.statusCode, 307);
224 });
225 }); // getById
226
227 describe('postRoot', function () {
228 beforeEach(function () {
229 ctx.parsedBody = {};
230 });
231 it('requires url parameter', async function () {
232 try {
233 await manager.postRoot(res, ctx);
234 assert.fail(noExpectedException);
235 } catch (e) {
236 assert.strictEqual(e.message, 'Bad Request');
237 }
238 });
239 it('creates a link', async function () {
240 ctx.parsedBody.url = 'http://example.com/insert';
241 await manager.postRoot(res, ctx);
242 assert(manager.db.insertLink.called);
243 assert(res.end.called);
244 });
245 it('returns existing link', async function () {
246 ctx.parsedBody.url = 'http://example.com/existing';
247 const existingLink = {
248 id: 'blah',
249 sourceLink: ctx.parsedBody.url,
250 };
251 manager.db.getLinkByUrl.resolves(existingLink);
252 await manager.postRoot(res, ctx);
253 assert(!manager.db.insertLink.called);
254 assert(!manager.db.expireLink.called);
255 assert(res.end.called);
256 });
257 it('restores expired link', async function () {
258 ctx.parsedBody.url = 'http://example.com/expired';
259 const existingLink = {
260 id: 'blah',
261 sourceLink: ctx.parsedBody.url,
262 expires: 1,
263 };
264 manager.db.getLinkByUrl.resolves(existingLink);
265 await manager.postRoot(res, ctx);
266 assert(!manager.db.insertLink.called);
267 assert(manager.db.expireLink.called);
268 assert(res.end.called);
269 });
270 }); // postRoot
271
272 describe('putById', function () {
273 let url;
274 beforeEach(function () {
275 url = 'http://example.com/put';
276 ctx.parsedBody = {};
277 });
278 it('requires url parameter', async function () {
279 try {
280 await manager.putById(res, ctx);
281 assert.fail(noExpectedException);
282 } catch (e) {
283 assert.strictEqual(e.message, 'Bad Request');
284 }
285 });
286 it('updates existing', async function () {
287 ctx.parsedBody.url = url;
288 const existingLink = {
289 id: 'blah',
290 };
291 manager.db.getLinkById.resolves(existingLink);
292 await manager.putById(res, ctx);
293 assert(manager.db.insertLink.called);
294 assert(res.end.called);
295 });
296 it('does not create without admin', async function () {
297 ctx.parsedBody.url = url;
298 try {
299 await manager.putById(res, ctx);
300 assert.fail(noExpectedException);
301 } catch (e) {
302 assert.strictEqual(e.message, 'Forbidden');
303 }
304 });
305 it('allows admin creation', async function () {
306 ctx.parsedBody.url = url;
307 ctx.authenticationId = 'blah';
308 await manager.putById(res, ctx);
309 assert(manager.db.insertLink.called);
310 assert.strictEqual(res.statusCode, 201);
311 assert(res.end.called);
312 });
313 }); // putById
314
315 describe('deleteById', function () {
316 it('handles missing id', async function () {
317 try {
318 await manager.deleteById(res, ctx);
319 assert.fail(noExpectedException);
320 } catch (e) {
321 assert.strictEqual(e.message, 'Not Found');
322 }
323 });
324 it('expires link', async function () {
325 const existingLink = {
326 id: 'awoo',
327 };
328 manager.db.getLinkById.resolves(existingLink);
329 await manager.deleteById(res, ctx);
330 assert(manager.db.expireLink.called);
331 assert.strictEqual(res.statusCode, 204);
332 });
333 it('ignores expired link', async function () {
334 const existingLink = {
335 id: 'awoo',
336 expires: 123,
337 };
338 manager.db.getLinkById.resolves(existingLink);
339 await manager.deleteById(res, ctx);
340 assert(!manager.db.expireLink.called);
341 assert.strictEqual(res.statusCode, 304);
342 });
343 }); // deleteById
344
345 describe('infoContent', function () {
346 let details;
347 beforeEach(function () {
348 details = {
349 created: 1604155860,
350 lastAccess: 1604155861,
351 accesses: 6,
352 };
353 });
354 it('generates info', function () {
355 details.expires = 1604155862;
356 const result = manager.infoContent(ctx, details);
357 assert(result);
358 });
359 it('generates json info', function () {
360 ctx.responseType = Enum.ContentType.ApplicationJson;
361 const result = manager.infoContent(ctx, details);
362 JSON.parse(result);
363 });
364 it('generates html info', function () {
365 ctx.responseType = Enum.ContentType.TextHTML;
366 const result = manager.infoContent(ctx, details);
367 assert(result);
368 });
369 }); // infoContent
370
371 describe('getByIdInfo', function () {
372 it('handles missing link', async function () {
373 try {
374 await manager.getByIdInfo(res, ctx);
375 assert.fail(noExpectedException);
376 } catch (e) {
377 assert.strictEqual(e.message, 'Not Found');
378 }
379 });
380 it('gets link', async function () {
381 const existingLink = {
382 id: 'blah',
383 };
384 manager.db.getLinkById.resolves(existingLink);
385 await manager.getByIdInfo(res, ctx);
386 assert(res.end.called);
387 });
388 }); // getByIdInfo
389
390 describe('reportContent', function () {
391 let links;
392 it('generates report', function () {
393 links = [
394 {
395 id: 'awoo',
396 created: 1604155860,
397 lastAccess: 1604155861,
398 url: 'http://example.com/awoo',
399 },
400 ];
401 const result = manager.reportContent(ctx, links);
402 assert(result);
403 });
404 it('generates json report', function () {
405 links = [];
406 ctx.responseType = Enum.ContentType.ApplicationJson;
407 const result = manager.reportContent(ctx, links);
408 JSON.parse(result);
409 });
410 it('generates html report', function () {
411 links = [];
412 ctx.responseType = Enum.ContentType.TextHTML;
413 const result = manager.reportContent(ctx, links);
414 assert(result);
415 });
416 }); // reportContent
417
418 describe('getAdminReport', function () {
419 it('does links', async function () {
420 const links = [];
421 manager.db.getAllLinks.resolves(links);
422 await manager.getAdminReport(res, ctx);
423 assert(res.end.called);
424 });
425 }); // getAdminReport
426
427 }); // Manager