From 3d7876be4ba9b0497e30039b786372a821c747fb Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Mon, 7 Oct 2024 12:24:45 -0300 Subject: [PATCH] Add tests --- apps/meteor/.mocharc.js | 1 + .../routes/avatar/middlewares/auth.spec.ts | 87 +++++++ .../avatar/middlewares/browserVersion.spec.ts | 87 +++++++ apps/meteor/server/routes/avatar/room.spec.ts | 139 ++++++++++++ apps/meteor/server/routes/avatar/user.spec.ts | 155 +++++++++++++ .../meteor/server/routes/avatar/utils.spec.ts | 214 ++++++++++++++++++ 6 files changed, 683 insertions(+) create mode 100644 apps/meteor/server/routes/avatar/middlewares/auth.spec.ts create mode 100644 apps/meteor/server/routes/avatar/middlewares/browserVersion.spec.ts create mode 100644 apps/meteor/server/routes/avatar/room.spec.ts create mode 100644 apps/meteor/server/routes/avatar/user.spec.ts create mode 100644 apps/meteor/server/routes/avatar/utils.spec.ts diff --git a/apps/meteor/.mocharc.js b/apps/meteor/.mocharc.js index fb5324f4c0fd..aa092068a9b0 100644 --- a/apps/meteor/.mocharc.js +++ b/apps/meteor/.mocharc.js @@ -32,6 +32,7 @@ module.exports = { 'tests/unit/app/**/*.tests.js', 'tests/unit/app/**/*.tests.ts', 'tests/unit/lib/**/*.tests.ts', + 'server/routes/avatar/**/*.spec.ts', 'tests/unit/lib/**/*.spec.ts', 'tests/unit/server/**/*.tests.ts', 'tests/unit/server/**/*.spec.ts', diff --git a/apps/meteor/server/routes/avatar/middlewares/auth.spec.ts b/apps/meteor/server/routes/avatar/middlewares/auth.spec.ts new file mode 100644 index 000000000000..cffa82eabd0b --- /dev/null +++ b/apps/meteor/server/routes/avatar/middlewares/auth.spec.ts @@ -0,0 +1,87 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +const mocks = { + utils: { + userCanAccessAvatar: sinon.stub(), + renderSVGLetters: sinon.stub(), + }, +}; + +const { protectAvatarsWithFallback } = proxyquire.noCallThru().load('./auth.ts', { + '../utils': mocks.utils, +}); + +describe('#protectAvatarsWithFallback()', () => { + const response = { + setHeader: sinon.spy(), + writeHead: sinon.spy(), + write: sinon.spy(), + end: sinon.spy(), + }; + const next = sinon.spy(); + + afterEach(() => { + response.setHeader.resetHistory(); + response.writeHead.resetHistory(); + response.end.resetHistory(); + next.resetHistory(); + + Object.values(mocks.utils).forEach((mock) => mock.reset()); + }); + + it(`should write 404 to head if no url provided`, async () => { + await protectAvatarsWithFallback({}, response, next); + expect(next.called).to.be.false; + expect(response.setHeader.called).to.be.false; + expect(response.writeHead.calledWith(404)).to.be.true; + expect(response.end.calledOnce).to.be.true; + }); + + it(`should write 200 to head and write fallback to body (user avatar)`, async () => { + mocks.utils.renderSVGLetters.returns('fallback'); + + await protectAvatarsWithFallback({ url: '/jon' }, response, next); + expect(next.called).to.be.false; + expect(response.setHeader.called).to.be.false; + + expect(response.writeHead.calledWith(200, { 'Content-Type': 'image/svg+xml' })).to.be.true; + expect(mocks.utils.renderSVGLetters.calledWith('jon')).to.be.true; + expect(response.write.calledWith('fallback')).to.be.true; + + expect(response.end.calledOnce).to.be.true; + }); + + it(`should write 200 to head and write fallback to body (user avatar)`, async () => { + mocks.utils.renderSVGLetters.returns('fallback'); + + await protectAvatarsWithFallback({ url: '/jon' }, response, next); + expect(next.called).to.be.false; + expect(response.setHeader.called).to.be.false; + expect(response.writeHead.calledWith(200, { 'Content-Type': 'image/svg+xml' })).to.be.true; + expect(response.write.calledWith('fallback')).to.be.true; + expect(response.end.calledOnce).to.be.true; + }); + + it(`should write 200 to head and write fallback to body (room avatar)`, async () => { + mocks.utils.renderSVGLetters.returns('fallback'); + + await protectAvatarsWithFallback({ url: '/room/jon' }, response, next); + expect(next.called).to.be.false; + expect(response.setHeader.called).to.be.false; + expect(response.writeHead.calledWith(200, { 'Content-Type': 'image/svg+xml' })).to.be.true; + expect(response.write.calledWith('fallback')).to.be.true; + expect(response.end.calledOnce).to.be.true; + }); + + it(`should call next if user can access avatar`, async () => { + mocks.utils.userCanAccessAvatar.returns(true); + const request = { url: '/jon' }; + + await protectAvatarsWithFallback(request, response, next); + expect(mocks.utils.userCanAccessAvatar.calledWith(request)).to.be.true; + expect(next.called).to.be.true; + }); +}); diff --git a/apps/meteor/server/routes/avatar/middlewares/browserVersion.spec.ts b/apps/meteor/server/routes/avatar/middlewares/browserVersion.spec.ts new file mode 100644 index 000000000000..6b627cb3b224 --- /dev/null +++ b/apps/meteor/server/routes/avatar/middlewares/browserVersion.spec.ts @@ -0,0 +1,87 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +// const getCookie = sinon.stub(); +class CookiesMock { + public get = (_key: any, value: any) => value; +} +const { handleBrowserVersionCheck, isIEOlderThan11 } = proxyquire.noCallThru().load('./browserVersion', { + 'meteor/ostrio:cookies': { + Cookies: CookiesMock, + }, + '../../../../app/utils/server/getURL': { + getURL: () => '', + }, +}); + +describe('#isIEOlderThan11()', () => { + it('should return false if user agent is IE11', () => { + const userAgent = { + browser: { + name: 'IE', + version: '11.0', + }, + }; + expect(isIEOlderThan11(userAgent)).to.be.false; + }); + + it('should return true if user agent is IE < 11', () => { + const userAgent = { + browser: { + name: 'IE', + version: '10.0', + }, + }; + expect(isIEOlderThan11(userAgent)).to.be.true; + }); +}); + +describe('#handleBrowserVersionCheck()', () => { + it('should call next if browser_version_check cookie is set to "bypass"', async () => { + const next = sinon.spy(); + const request = { + headers: { + cookie: 'bypass', + }, + }; + handleBrowserVersionCheck(request as any, {} as any, next); + + expect(next.calledOnce).to.be.true; + }); + + it('should call next if browser_version_check cookie is not set to "force" and user agent is not IE < 11', async () => { + const next = sinon.spy(); + const request = { + headers: { + 'cookie': 'anything', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36', + }, + }; + handleBrowserVersionCheck(request as any, {} as any, next); + + expect(next.calledOnce).to.be.true; + }); + + it('should respond with Browser not supported', async () => { + const next = sinon.spy(); + const request = { + headers: { + 'cookie': 'anything', + 'user-agent': 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 7.0; InfoPath.3; .NET CLR 3.1.40767; Trident/6.0; en-IN)', + }, + }; + + const response = { + setHeader: sinon.spy(), + write: sinon.spy(), + end: sinon.spy(), + }; + handleBrowserVersionCheck(request as any, response as any, next); + + expect(response.setHeader.calledWith('content-type', 'text/html; charset=utf-8')).to.be.true; + expect(response.write.calledWith(sinon.match('Browser not supported'))).to.be.true; + expect(response.end.calledOnce).to.be.true; + }); +}); diff --git a/apps/meteor/server/routes/avatar/room.spec.ts b/apps/meteor/server/routes/avatar/room.spec.ts new file mode 100644 index 000000000000..1b2fd71e95d9 --- /dev/null +++ b/apps/meteor/server/routes/avatar/room.spec.ts @@ -0,0 +1,139 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +const mocks = { + settingsGet: sinon.stub(), + findOneById: sinon.stub(), + utils: { + serveSvgAvatarInRequestedFormat: sinon.spy(), + wasFallbackModified: sinon.stub(), + setCacheAndDispositionHeaders: sinon.spy(), + serveAvatarFile: sinon.spy(), + }, + avatarFindOneByRoomId: sinon.stub(), + getRoomName: sinon.stub(), +}; + +class CookiesMock { + public get = (_key: any, value: any) => value; +} + +const { roomAvatar } = proxyquire.noCallThru().load('./room', { + '@rocket.chat/models': { + Rooms: { + findOneById: mocks.findOneById, + }, + Avatars: { + findOneByRoomId: mocks.avatarFindOneByRoomId, + }, + }, + '../../../app/settings/server': { + settings: { + get: mocks.settingsGet, + }, + }, + './utils': mocks.utils, + '../../lib/rooms/roomCoordinator': { + roomCoordinator: { + getRoomName: mocks.getRoomName, + }, + }, + 'meteor/ostrio:cookies': { + Cookies: CookiesMock, + }, +}); + +describe('#roomAvatar()', () => { + const response = { + setHeader: sinon.spy(), + writeHead: sinon.spy(), + end: sinon.spy(), + }; + const next = sinon.spy(); + + afterEach(() => { + mocks.settingsGet.reset(); + mocks.avatarFindOneByRoomId.reset(); + mocks.findOneById.reset(); + + response.setHeader.resetHistory(); + response.writeHead.resetHistory(); + response.end.resetHistory(); + next.resetHistory(); + + Object.values(mocks.utils).forEach((mock) => ('reset' in mock ? mock.reset() : mock.resetHistory())); + }); + + it(`should do nothing if url is not in request object`, async () => { + await roomAvatar({}, response, next); + expect(next.called).to.be.false; + expect(response.setHeader.called).to.be.false; + expect(response.writeHead.called).to.be.false; + expect(response.end.called).to.be.false; + }); + + it(`should write 404 if room is not found`, async () => { + mocks.findOneById.returns(null); + await roomAvatar({ url: '/' }, response, next); + expect(next.called).to.be.false; + expect(response.setHeader.called).to.be.false; + expect(response.writeHead.calledWith(404)).to.be.true; + expect(response.end.calledOnce).to.be.true; + }); + + it(`should serve avatar file if found`, async () => { + const request = { url: '/roomId' }; + + const file = { uploadedAt: new Date(0), type: 'image/png', size: 100 }; + + mocks.findOneById.withArgs('roomId').returns({ _id: 'roomId' }); + mocks.avatarFindOneByRoomId.withArgs('roomId').returns(file); + + await roomAvatar(request, response, next); + + expect(mocks.utils.setCacheAndDispositionHeaders.calledWith(request, response)).to.be.true; + expect(mocks.utils.serveAvatarFile.calledWith(file, request, response, next)).to.be.true; + }); + + it(`should serve parent room avatar file if current room avatar is not found`, async () => { + const request = { url: '/roomId' }; + + const file = { uploadedAt: new Date(0), type: 'image/png', size: 100 }; + + mocks.findOneById.withArgs('roomId').returns({ _id: 'roomId', prid: 'roomId2' }); + mocks.findOneById.withArgs('roomId2').returns({ _id: 'roomId2' }); + mocks.avatarFindOneByRoomId.withArgs('roomId2').returns(file); + + await roomAvatar(request, response, next); + + expect(mocks.utils.setCacheAndDispositionHeaders.calledWith(request, response)).to.be.true; + expect(mocks.utils.serveAvatarFile.calledWith(file, request, response, next)).to.be.true; + }); + + it(`should write 304 if fallback content is not modified`, async () => { + const request = { url: '/roomId', headers: {} }; + + mocks.findOneById.withArgs('roomId').returns({ _id: 'roomId' }); + mocks.utils.wasFallbackModified.returns(false); + await roomAvatar(request, response, next); + + expect(mocks.utils.setCacheAndDispositionHeaders.calledWith(request, response)).to.be.true; + expect(response.writeHead.calledWith(304)).to.be.true; + expect(response.end.calledOnce).to.be.true; + }); + + it(`should serve svg fallback if no file found`, async () => { + const request = { url: '/roomId', headers: { cookie: 'userId' } }; + + mocks.utils.wasFallbackModified.returns(true); + mocks.findOneById.withArgs('roomId').returns({ _id: 'roomId' }); + mocks.getRoomName.returns('roomName'); + + await roomAvatar(request, response, next); + + expect(mocks.utils.setCacheAndDispositionHeaders.calledWith(request, response)).to.be.true; + expect(mocks.utils.serveSvgAvatarInRequestedFormat.calledWith({ nameOrUsername: 'roomName', req: request, res: response })).to.be.true; + }); +}); diff --git a/apps/meteor/server/routes/avatar/user.spec.ts b/apps/meteor/server/routes/avatar/user.spec.ts new file mode 100644 index 000000000000..d34948cd3964 --- /dev/null +++ b/apps/meteor/server/routes/avatar/user.spec.ts @@ -0,0 +1,155 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import { Headers } from 'node-fetch'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +const mocks = { + settingsGet: sinon.stub(), + findOneByUsernameIgnoringCase: sinon.stub(), + utils: { + serveSvgAvatarInRequestedFormat: sinon.spy(), + wasFallbackModified: sinon.stub(), + setCacheAndDispositionHeaders: sinon.spy(), + serveAvatarFile: sinon.spy(), + }, + serverFetch: sinon.stub(), + avatarFindOneByName: sinon.stub(), +}; + +const { userAvatarByUsername } = proxyquire.noCallThru().load('./user', { + '@rocket.chat/models': { + Users: { + findOneByUsernameIgnoringCase: mocks.findOneByUsernameIgnoringCase, + }, + Avatars: { + findOneByName: mocks.avatarFindOneByName, + }, + }, + '../../../app/settings/server': { + settings: { + get: mocks.settingsGet, + }, + }, + './utils': mocks.utils, + '@rocket.chat/server-fetch': { + serverFetch: mocks.serverFetch, + }, +}); + +describe('#userAvatarByUsername()', () => { + const response = { + setHeader: sinon.spy(), + writeHead: sinon.spy(), + end: sinon.spy(), + }; + const next = sinon.spy(); + + afterEach(() => { + mocks.settingsGet.reset(); + mocks.avatarFindOneByName.reset(); + + response.setHeader.resetHistory(); + response.writeHead.resetHistory(); + response.end.resetHistory(); + next.resetHistory(); + + Object.values(mocks.utils).forEach((mock) => ('reset' in mock ? mock.reset() : mock.resetHistory())); + }); + + it(`should do nothing if url is not in request object`, async () => { + await userAvatarByUsername({}, response, next); + expect(next.called).to.be.false; + expect(response.setHeader.called).to.be.false; + expect(response.writeHead.called).to.be.false; + expect(response.end.called).to.be.false; + }); + + it(`should write 404 if username is not provided`, async () => { + await userAvatarByUsername({ url: '/' }, response, next); + expect(next.called).to.be.false; + expect(response.setHeader.called).to.be.false; + expect(response.writeHead.calledWith(404)).to.be.true; + expect(response.end.calledOnce).to.be.true; + }); + + it(`should call external provider`, async () => { + const request = { url: '/jon' }; + + const pipe = sinon.spy(); + const mockResponseHeaders = new Headers(); + mockResponseHeaders.set('header1', 'true'); + mockResponseHeaders.set('header2', 'false'); + + mocks.serverFetch.returns({ + headers: mockResponseHeaders, + body: { pipe }, + }); + + mocks.settingsGet.returns('test123/{username}'); + + await userAvatarByUsername(request, response, next); + + expect(mocks.serverFetch.calledWith('test123/jon')).to.be.true; + expect(mocks.utils.setCacheAndDispositionHeaders.calledWith(request, response)).to.be.true; + expect(response.setHeader.calledTwice).to.be.true; + expect(response.setHeader.getCall(0).calledWith('header1', 'true')).to.be.true; + expect(response.setHeader.getCall(1).calledWith('header2', 'false')).to.be.true; + expect(pipe.calledWith(response)).to.be.true; + }); + + it(`should serve svg if requestUsername starts with @`, async () => { + const request = { url: '/@jon' }; + + await userAvatarByUsername(request, response, next); + + expect(mocks.utils.setCacheAndDispositionHeaders.calledWith(request, response)).to.be.true; + expect(mocks.utils.serveSvgAvatarInRequestedFormat.calledWith({ nameOrUsername: 'jon', req: request, res: response })).to.be.true; + }); + + it(`should serve avatar file if found`, async () => { + const request = { url: '/jon' }; + + const file = { uploadedAt: new Date(0), type: 'image/png', size: 100 }; + mocks.avatarFindOneByName.returns(file); + + await userAvatarByUsername(request, response, next); + + expect(mocks.utils.setCacheAndDispositionHeaders.calledWith(request, response)).to.be.true; + expect(mocks.utils.serveAvatarFile.calledWith(file, request, response, next)).to.be.true; + }); + + it(`should write 304 to head if content is not modified`, async () => { + const request = { url: '/jon', headers: {} }; + + mocks.utils.wasFallbackModified.returns(false); + await userAvatarByUsername(request, response, next); + + expect(mocks.utils.setCacheAndDispositionHeaders.calledWith(request, response)).to.be.true; + expect(response.writeHead.calledWith(304)).to.be.true; + expect(response.end.calledOnce).to.be.true; + }); + + it(`should fallback to SVG if no avatar found`, async () => { + const request = { url: '/jon', headers: {} }; + + mocks.utils.wasFallbackModified.returns(true); + await userAvatarByUsername(request, response, next); + + expect(mocks.utils.setCacheAndDispositionHeaders.calledWith(request, response)).to.be.true; + expect(mocks.utils.serveSvgAvatarInRequestedFormat.calledWith({ nameOrUsername: 'jon', req: request, res: response })).to.be.true; + }); + + it(`should fallback to SVG with user name if UI_Use_Name_Avatar is true`, async () => { + const request = { url: '/jon', headers: {} }; + + mocks.utils.wasFallbackModified.returns(true); + mocks.settingsGet.withArgs('UI_Use_Name_Avatar').returns(true); + mocks.findOneByUsernameIgnoringCase.returns({ name: 'Doe' }); + + await userAvatarByUsername(request, response, next); + + expect(mocks.utils.setCacheAndDispositionHeaders.calledWith(request, response)).to.be.true; + expect(mocks.utils.serveSvgAvatarInRequestedFormat.calledWith({ nameOrUsername: 'Doe', req: request, res: response })).to.be.true; + }); +}); diff --git a/apps/meteor/server/routes/avatar/utils.spec.ts b/apps/meteor/server/routes/avatar/utils.spec.ts new file mode 100644 index 000000000000..3c58fb4c8b97 --- /dev/null +++ b/apps/meteor/server/routes/avatar/utils.spec.ts @@ -0,0 +1,214 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +class CookiesMock { + public get = (_key: any, value: any) => value; +} + +const mocks = { + settingsGet: sinon.stub(), + findOneByIdAndLoginToken: sinon.stub(), + fileUploadGet: sinon.spy(), +}; + +const { + getAvatarSizeFromRequest, + MIN_SVG_AVATAR_SIZE, + MAX_SVG_AVATAR_SIZE, + renderSVGLetters, + setCacheAndDispositionHeaders, + userCanAccessAvatar, + wasFallbackModified, + serveSvgAvatarInRequestedFormat, + serveAvatarFile, +} = proxyquire.noCallThru().load('./utils', { + 'meteor/ostrio:cookies': { + Cookies: CookiesMock, + }, + '../../../../app/utils/server/getURL': { + getURL: () => '', + }, + '@rocket.chat/models': { + Users: { + findOneByIdAndLoginToken: mocks.findOneByIdAndLoginToken, + }, + }, + '../../../app/file-upload/server': { + FileUpload: { + get: mocks.fileUploadGet, + }, + }, + '../../../app/settings/server': { + settings: { + get: mocks.settingsGet, + }, + }, + 'sharp': () => ({ toFormat: (format: any) => ({ pipe: (res: any) => res.write(format) }) }), +}); + +describe('#serveAvatarFile()', () => { + const file = { uploadedAt: new Date(0), type: 'image/png', size: 100 }; + const response = { + setHeader: sinon.spy(), + writeHead: sinon.spy(), + end: sinon.spy(), + }; + const next = sinon.spy(); + + afterEach(() => { + response.setHeader.resetHistory(); + response.writeHead.resetHistory(); + response.end.resetHistory(); + next.resetHistory(); + }); + + it(`should return code 304 if avatar was not modified`, () => { + serveAvatarFile(file, { headers: { 'if-modified-since': new Date(0).toUTCString() } }, response, next); + expect(response.setHeader.getCall(0).calledWith('Content-Security-Policy', "default-src 'none'")).to.be.true; + expect(response.setHeader.getCall(1).calledWith('Last-Modified', new Date(0).toUTCString())).to.be.true; + expect(response.writeHead.calledWith(304)).to.be.true; + expect(response.end.calledOnce).to.be.true; + }); + + it('should serve avatar', () => { + const request = { headers: { 'if-modified-since': new Date(200000).toUTCString() } }; + serveAvatarFile(file, request, response, next); + expect(response.setHeader.getCall(0).calledWith('Content-Security-Policy', "default-src 'none'")).to.be.true; + expect(response.setHeader.getCall(1).calledWith('Last-Modified', new Date(0).toUTCString())).to.be.true; + expect(response.setHeader.getCall(2).calledWith('Content-Type', file.type)).to.be.true; + expect(response.setHeader.getCall(3).calledWith('Content-Length', file.size)).to.be.true; + expect(mocks.fileUploadGet.calledWith(file, request, response, next)).to.be.true; + }); +}); + +describe('#serveSvgAvatarInRequestedFormat()', () => { + it('should serve SVG avatar in requested format', () => { + ['png', 'jpg', 'jpeg'].forEach((format) => { + const response = { + setHeader: sinon.spy(), + write: sinon.spy(), + end: sinon.spy(), + }; + serveSvgAvatarInRequestedFormat({ req: { query: { format, size: 100 } }, res: response, nameOrUsername: 'name' }); + + expect(response.setHeader.getCall(0).calledWith('Last-Modified', 'Thu, 01 Jan 2015 00:00:00 GMT')).to.be.true; + expect(response.setHeader.getCall(1).calledWith('Content-Type', `image/${format}`)).to.be.true; + expect(response.write.calledWith(format)).to.be.true; + }); + }); + it(`should serve avatar in SVG format if requested format is not png, jpg or jpeg`, () => { + const response = { + setHeader: sinon.spy(), + write: sinon.spy(), + end: sinon.spy(), + }; + serveSvgAvatarInRequestedFormat({ req: { query: { format: 'anythingElse', size: 100 } }, res: response, nameOrUsername: 'name' }); + + expect(response.setHeader.getCall(0).calledWith('Last-Modified', 'Thu, 01 Jan 2015 00:00:00 GMT')).to.be.true; + expect(response.setHeader.getCall(1).calledWith('Content-Type', 'image/svg+xml')).to.be.true; + expect(response.write.called).to.be.true; + expect(response.end.calledOnce).to.be.true; + }); +}); + +describe('#getAvatarSizeFromRequest()', () => { + it('should return value passed in the request if it falls in the range limit', () => { + expect(getAvatarSizeFromRequest({ query: { size: 100 } })).to.equal(100); + }); + it(`should return ${MIN_SVG_AVATAR_SIZE} if requested size is smaller than the limit`, () => { + expect(getAvatarSizeFromRequest({ query: { size: 2 } })).to.equal(16); + }); + it(`should return ${MAX_SVG_AVATAR_SIZE} if requested size is bigger than the limit`, () => { + expect(getAvatarSizeFromRequest({ query: { size: 2000 } })).to.equal(1024); + }); +}); + +describe('#wasFallbackModified()', () => { + it('should return true if reqModifiedHeader is different from FALLBACK_LAST_MODIFIED', () => { + expect(wasFallbackModified('')).to.equal(true); + }); + it('should false if reqModifiedHeader is the same as FALLBACK_LAST_MODIFIED', () => { + expect(wasFallbackModified('Thu, 01 Jan 2015 00:00:00 GMT')).to.equal(false); + }); +}); + +describe('#renderSvgLetters', () => { + it('should show capitalized initial letter in the svg', () => { + expect(renderSVGLetters('arthur', 16)).to.include('>\nA\n'); + expect(renderSVGLetters('Bob', 16)).to.include('>\nB\n'); + expect(renderSVGLetters('yan', 16)).to.include('>\nY\n'); + }); + it('should render question mark with color #000', () => { + expect(renderSVGLetters('?', 16)).to.include('>\n?\n').and.to.include('fill="#000"'); + }); + it('should include size in viewBox property', () => { + expect(renderSVGLetters('arthur', 16)).to.include('viewBox="0 0 16 16"'); + expect(renderSVGLetters('Bob', 32)).to.include('viewBox="0 0 32 32"'); + expect(renderSVGLetters('yan', 64)).to.include('viewBox="0 0 64 64"'); + }); +}); + +describe('#setCacheAndDispositionHeaders', () => { + it('should set the Cache-Control header based on the query cacheTime', () => { + const request = { + query: { + cacheTime: 100, + }, + }; + + const response = { + setHeader: sinon.spy(), + }; + setCacheAndDispositionHeaders(request as any, response as any); + expect(response.setHeader.calledWith('Cache-Control', 'public, max-age=100')).to.be.true; + expect(response.setHeader.calledWith('Content-Disposition', 'inline')).to.be.true; + }); + + it('should set the Cache-Control header based on the setting if the query cacheTime is not set', () => { + const request = { + query: {}, + }; + + const response = { + setHeader: sinon.spy(), + }; + + mocks.settingsGet.returns(100); + setCacheAndDispositionHeaders(request as any, response as any); + expect(response.setHeader.calledWith('Cache-Control', 'public, max-age=100')).to.be.true; + expect(response.setHeader.calledWith('Content-Disposition', 'inline')).to.be.true; + }); +}); + +describe('#userCanAccessAvatar()', async () => { + beforeEach(() => { + mocks.findOneByIdAndLoginToken.reset(); + mocks.settingsGet.reset(); + }); + it('should return true if setting is set to not block avatars', async () => { + await expect(userCanAccessAvatar({})).to.eventually.equal(true); + }); + it('should return true if user is authenticated', async () => { + mocks.settingsGet.returns(true); + mocks.findOneByIdAndLoginToken.returns({ _id: 'id' }); + + await expect(userCanAccessAvatar({ query: { rc_token: 'token', rc_uid: 'id' } })).to.eventually.equal(true); + await expect(userCanAccessAvatar({ headers: { cookie: 'rc_token=token; rc_uid=id' } })).to.eventually.equal(true); + }); + it('should return false and warn if user is unauthenticated', async () => { + console.warn = sinon.spy(); + mocks.findOneByIdAndLoginToken.returns(undefined); + mocks.settingsGet.returns(true); + + await expect(userCanAccessAvatar({})).to.eventually.equal(false); + expect((console.warn as sinon.SinonSpy).calledWith(sinon.match('unauthenticated'))).to.be.true; + + await expect(userCanAccessAvatar({ headers: { cookie: 'rc_token=token; rc_uid=id' } })).to.eventually.equal(false); + expect((console.warn as sinon.SinonSpy).calledWith(sinon.match('unauthenticated'))).to.be.true; + + await expect(userCanAccessAvatar({ query: { rc_token: 'token', rc_uid: 'id' } })).to.eventually.equal(false); + expect((console.warn as sinon.SinonSpy).calledWith(sinon.match('unauthenticated'))).to.be.true; + }); +});