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