3 const { StubLogger
} = require('@squeep/test-helper');
4 const assert
= require('node:assert');
5 const sinon
= require('sinon');
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');
12 const noExpectedException
= 'did not get expected exception';
14 describe('Manager', function () {
15 let manager
, logger
, options
;
18 beforeEach(function () {
19 logger
= new StubLogger(sinon
);
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(),
33 setHeader: sinon
.stub(),
38 manager
= new Manager(logger
, stubDb
, options
);
41 afterEach(function () {
45 it('instantiates', function () {
49 it('defaults options', function () {
50 manager
= new Manager({}, {});
53 describe('rootContent', function () {
54 it('generates content for empty context', function () {
55 const result
= manager
.rootContent(ctx
);
56 assert(result
.length
);
58 it('generates json content', function () {
59 ctx
.responseType
= Enum
.ContentType
.ApplicationJson
;
60 const result
= manager
.rootContent(ctx
);
61 assert(result
.length
);
64 it('generates html content', function () {
65 ctx
.responseType
= Enum
.ContentType
.TextHTML
;
66 const result
= manager
.rootContent(ctx
);
67 assert(result
.length
);
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
);
79 describe('getRoot', function () {
81 beforeEach(function () {
82 sinon
.stub(common
, 'isClientCached');
85 it('normal response', async
function () {
86 common
.isClientCached
.returns(false);
87 await manager
.getRoot(req
, res
, ctx
);
88 assert(res
.end
.called
);
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
);
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);
104 describe('_getNewIdentifier', function () {
105 const url
= 'http://example.com/bar';
107 beforeEach(function () {
109 nextStub
= sinon
.stub();
110 manager
.db
.getLinkById
.onCall(0).resolves({ id:'existing' }).onCall(1).resolves();
111 sinon
.stub(manager
, 'makeSlugGenerator').callsFake(() => {
117 it('gets identifiers', async
function () {
118 nextStub
.resolves({ value: 'slug', done: false });
119 const result
= await manager
._getNewIdentifier(dbCtx
, url
);
121 assert
.strictEqual(nextStub
.callCount
, 2);
123 it('handles empty slug', async
function () {
124 nextStub
.resolves({ value: '', done: false });
126 await manager
._getNewIdentifier(dbCtx
, url
);
127 assert
.fail(noExpectedException
);
129 assert(e
instanceof SlugGeneratorExhausted
, noExpectedException
);
132 it('handles end of generator', async
function () {
133 nextStub
.resolves({ value: 'blah', done: true });
135 await manager
._getNewIdentifier(dbCtx
, url
);
136 assert
.fail(noExpectedException
);
138 assert(e
instanceof SlugGeneratorExhausted
, noExpectedException
);
141 }); // _getNewIdentifier
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
);
149 it('accepts valid url', function () {
150 ctx
.sourceLink
= 'http://example.com/file.txt';
151 manager
._validateContextURL(ctx
);
153 it('rejects missing url', function () {
155 manager
._validateContextURL(ctx
);
156 assert
.fail(noExpectedException
);
158 assert
.strictEqual(e
.message
, Enum
.ErrorResponse
.InvalidURLParameter
.errorMessage
, noExpectedException
);
161 it('rejects invalid url', function () {
162 ctx
.sourceLink
= 'not a url';
164 manager
._validateContextURL(ctx
);
165 assert
.fail(noExpectedException
);
167 assert
.strictEqual(e
.message
, Enum
.ErrorResponse
.InvalidURLParameter
.errorMessage
, noExpectedException
);
170 }); // _validateContextURL
172 describe('getById', function () {
174 beforeEach(function () {
177 it('handles missing link', async
function () {
179 await manager
.getById(res
, ctx
);
180 assert
.fail(noExpectedException
);
182 assert
.strictEqual(e
.message
, 'Not Found');
185 it('handles expired link', async
function () {
189 manager
.db
.accessLink
.resolves(link
);
191 await manager
.getById(res
, ctx
);
192 assert
.fail(noExpectedException
);
194 assert
.strictEqual(e
.message
, 'Gone');
197 it('handles local static link', async
function () {
198 const file
= 'foop.txt';
200 url: `${manager.staticDirectory}${file}`,
202 manager
.db
.accessLink
.resolves(link
);
204 await manager
.getById(res
, ctx
);
205 assert
.fail(noExpectedException
);
207 assert(e
instanceof ServeStaticFile
);
208 assert
.strictEqual(e
.file
, file
);
211 it('redirects a link', async
function () {
213 url: 'http://example.com/awoo',
215 manager
.db
.accessLink
.resolves(link
);
216 await manager
.getById(res
, ctx
);
217 assert
.strictEqual(res
.statusCode
, 307);
221 describe('postRoot', function () {
222 beforeEach(function () {
225 it('requires url parameter', async
function () {
227 await manager
.postRoot(res
, ctx
);
228 assert
.fail(noExpectedException
);
230 assert
.strictEqual(e
.message
, 'Bad Request');
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
);
239 it('returns existing link', async
function () {
240 ctx
.parsedBody
.url
= 'http://example.com/existing';
241 const existingLink
= {
243 sourceLink: ctx
.parsedBody
.url
,
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
);
251 it('restores expired link', async
function () {
252 ctx
.parsedBody
.url
= 'http://example.com/expired';
253 const existingLink
= {
255 sourceLink: ctx
.parsedBody
.url
,
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
);
266 describe('putById', function () {
268 beforeEach(function () {
269 url
= 'http://example.com/put';
272 it('requires url parameter', async
function () {
274 await manager
.putById(res
, ctx
);
275 assert
.fail(noExpectedException
);
277 assert
.strictEqual(e
.message
, 'Bad Request');
280 it('updates existing', async
function () {
281 ctx
.parsedBody
.url
= url
;
282 const existingLink
= {
285 manager
.db
.getLinkById
.resolves(existingLink
);
286 await manager
.putById(res
, ctx
);
287 assert(manager
.db
.insertLink
.called
);
288 assert(res
.end
.called
);
290 it('does not create without admin', async
function () {
291 ctx
.parsedBody
.url
= url
;
293 await manager
.putById(res
, ctx
);
294 assert
.fail(noExpectedException
);
296 assert
.strictEqual(e
.message
, 'Forbidden');
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
);
309 describe('deleteById', function () {
310 it('handles missing id', async
function () {
312 await manager
.deleteById(res
, ctx
);
313 assert
.fail(noExpectedException
);
315 assert
.strictEqual(e
.message
, 'Not Found');
318 it('expires link', async
function () {
319 const existingLink
= {
322 manager
.db
.getLinkById
.resolves(existingLink
);
323 await manager
.deleteById(res
, ctx
);
324 assert(manager
.db
.expireLink
.called
);
325 assert
.strictEqual(res
.statusCode
, 204);
327 it('ignores expired link', async
function () {
328 const existingLink
= {
332 manager
.db
.getLinkById
.resolves(existingLink
);
333 await manager
.deleteById(res
, ctx
);
334 assert(!manager
.db
.expireLink
.called
);
335 assert
.strictEqual(res
.statusCode
, 304);
339 describe('infoContent', function () {
341 beforeEach(function () {
344 lastAccess: 1604155861,
348 it('generates info', function () {
349 details
.expires
= 1604155862;
350 const result
= manager
.infoContent(ctx
, details
);
353 it('generates json info', function () {
354 ctx
.responseType
= Enum
.ContentType
.ApplicationJson
;
355 const result
= manager
.infoContent(ctx
, details
);
358 it('generates html info', function () {
359 ctx
.responseType
= Enum
.ContentType
.TextHTML
;
360 const result
= manager
.infoContent(ctx
, details
);
365 describe('getByIdInfo', function () {
366 it('handles missing link', async
function () {
368 await manager
.getByIdInfo(res
, ctx
);
369 assert
.fail(noExpectedException
);
371 assert
.strictEqual(e
.message
, 'Not Found');
374 it('gets link', async
function () {
375 const existingLink
= {
378 manager
.db
.getLinkById
.resolves(existingLink
);
379 await manager
.getByIdInfo(res
, ctx
);
380 assert(res
.end
.called
);
384 describe('reportContent', function () {
386 it('generates report', function () {
391 lastAccess: 1604155861,
392 url: 'http://example.com/awoo',
395 const result
= manager
.reportContent(ctx
, links
);
398 it('generates json report', function () {
400 ctx
.responseType
= Enum
.ContentType
.ApplicationJson
;
401 const result
= manager
.reportContent(ctx
, links
);
404 it('generates html report', function () {
406 ctx
.responseType
= Enum
.ContentType
.TextHTML
;
407 const result
= manager
.reportContent(ctx
, links
);
412 describe('getAdminReport', function () {
413 it('does links', async
function () {
415 manager
.db
.getAllLinks
.resolves(links
);
416 await manager
.getAdminReport(res
, ctx
);
417 assert(res
.end
.called
);
419 }); // getAdminReport