logger_helpers.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  1. /**
  2. * Copyright © 2016-2019 Threema GmbH (https://threema.ch/).
  3. *
  4. * This file is part of Threema Web.
  5. *
  6. * Threema Web is free software: you can redistribute it and/or modify it
  7. * under the terms of the GNU Affero General Public License as published by
  8. * the Free Software Foundation, either version 3 of the License, or (at
  9. * your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful, but
  12. * WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
  14. * General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Affero General Public License
  17. * along with Threema Web. If not, see <http://www.gnu.org/licenses/>.
  18. */
  19. // tslint:disable:no-reference
  20. /// <reference path="../../src/threema.d.ts" />
  21. import {Logger} from 'ts-log';
  22. import {BaseConfidential} from '../../src/helpers/confidential';
  23. import {ConsoleLogger, LevelLogger, MemoryLogger, TagLogger, TeeLogger, UnveilLogger} from '../../src/helpers/logger';
  24. import LogLevel = threema.LogLevel;
  25. import LogType = threema.LogType;
  26. interface EnsureLoggedArgs {
  27. level: LogLevel,
  28. logger: TestLogger | TestLogger[],
  29. tag?: string | string[],
  30. }
  31. type LogRecord = [LogType, any?, ...any[]];
  32. type LogFunction = (message?: any, ...args: any[]) => void;
  33. // Supported log (level) types
  34. const LOG_TYPES: LogType[] = ['debug', 'trace', 'info', 'warn', 'error'];
  35. /**
  36. * Stores all log records in memory for evaluation.
  37. */
  38. class TestLogger implements Logger {
  39. public readonly records: LogRecord[] = [];
  40. public readonly debug: LogFunction;
  41. public readonly trace: LogFunction;
  42. public readonly info: LogFunction;
  43. public readonly warn: LogFunction;
  44. public readonly error: LogFunction;
  45. constructor() {
  46. // Bind log level type methods
  47. for (const type of LOG_TYPES) {
  48. this[type] = this.append.bind(this, type);
  49. }
  50. }
  51. private append(type: LogType, message?: any, ...args: any[]): void {
  52. this.records.push([type, message, ...args])
  53. }
  54. }
  55. /**
  56. * A confidential log record for testing purposes.
  57. */
  58. class TestConfidential extends BaseConfidential<string, string> {
  59. public readonly uncensored: string = 'uncensored';
  60. public censored(): string {
  61. return 'censored';
  62. }
  63. }
  64. /**
  65. * Log a record of each type.
  66. */
  67. function logEachType(...loggers: Logger[]): void {
  68. for (const logger of loggers) {
  69. for (const type of LOG_TYPES) {
  70. logger[type](type);
  71. }
  72. }
  73. }
  74. /**
  75. * Format a log record. This splices a tag into the expected place.
  76. */
  77. function formatLogRecord(record: LogRecord, tag?: string[]): LogRecord {
  78. if (tag === undefined) {
  79. return record;
  80. }
  81. // Splice tag into message
  82. const [type, ...args] = record;
  83. return [type, ...tag.concat(args)];
  84. }
  85. /**
  86. * Ensure a log record of a level equal to or above has been logged.
  87. */
  88. function expectLogged(args: EnsureLoggedArgs): void {
  89. // Prepare arguments
  90. if (!(args.logger instanceof Array)) {
  91. args.logger = [args.logger];
  92. }
  93. let tag: string[];
  94. if (args.tag !== undefined) {
  95. tag = args.tag instanceof Array ? args.tag : [args.tag];
  96. }
  97. // Check for log records to be present, depending on the level
  98. for (const logger of args.logger) {
  99. // noinspection FallThroughInSwitchStatementJS
  100. switch (args.level) {
  101. case 'debug':
  102. expect(logger.records).toContain(formatLogRecord(['debug', 'debug'], tag));
  103. expect(logger.records).toContain(formatLogRecord(['trace', 'trace'], tag));
  104. case 'info':
  105. expect(logger.records).toContain(formatLogRecord(['info', 'info'], tag));
  106. case 'warn':
  107. expect(logger.records).toContain(formatLogRecord(['warn', 'warn'], tag));
  108. case 'error':
  109. expect(logger.records).toContain(formatLogRecord(['error', 'error'], tag));
  110. default:
  111. break;
  112. }
  113. }
  114. }
  115. describe('Logger Helpers', () => {
  116. describe('TestLogger', () => {
  117. it('stores log records', () => {
  118. const logger = new TestLogger();
  119. // Ensure a record of each type is being logged
  120. logEachType(logger);
  121. expectLogged({ level: 'debug', logger: logger });
  122. });
  123. });
  124. describe('TeeLogger', () => {
  125. it('forwards log records to each underlying logger', () => {
  126. const loggers: TestLogger[] = [
  127. new TestLogger(),
  128. new TestLogger(),
  129. new TestLogger(),
  130. ];
  131. const root = new TeeLogger(loggers);
  132. // Ensure a record of each type is being logged
  133. logEachType(root);
  134. expectLogged({ level: 'debug', logger: loggers });
  135. });
  136. });
  137. describe('LevelLogger', () => {
  138. it("'none' level discards everything", () => {
  139. const logger = new TestLogger();
  140. const root = new LevelLogger(logger, 'none');
  141. // Ensure a record of each expected type is being logged
  142. logEachType(root);
  143. expect(logger.records.length).toBe(0);
  144. });
  145. it("'debug' level discards nothing", () => {
  146. const logger = new TestLogger();
  147. const root = new LevelLogger(logger, 'debug');
  148. // Ensure a record of each expected type is being logged
  149. logEachType(root);
  150. expectLogged({ level: 'debug', logger: logger });
  151. });
  152. it("'info' level discards 'debug' and 'trace'", () => {
  153. const logger = new TestLogger();
  154. const root = new LevelLogger(logger, 'info');
  155. // Ensure a record of each expected type is being logged
  156. logEachType(root);
  157. expectLogged({ level: 'info', logger: logger });
  158. });
  159. it("'warn' level discards 'debug', 'trace' and 'info'", () => {
  160. const logger = new TestLogger();
  161. const root = new LevelLogger(logger, 'warn');
  162. // Ensure a record of each expected type is being logged
  163. logEachType(root);
  164. expectLogged({ level: 'warn', logger: logger });
  165. });
  166. it("'error' level discards 'debug', 'trace', 'info' and 'warn'", () => {
  167. const logger = new TestLogger();
  168. const root = new LevelLogger(logger, 'error');
  169. // Ensure a record of each expected type is being logged
  170. logEachType(root);
  171. expectLogged({ level: 'error', logger: logger });
  172. });
  173. });
  174. describe('TagLogger', () => {
  175. it('applies a tag', () => {
  176. const logger = new TestLogger();
  177. const root = new TagLogger(logger, 'tag');
  178. // Ensure a record of each type is being logged with the expected tag
  179. logEachType(root);
  180. expectLogged({ level: 'debug', tag: 'tag', logger: logger });
  181. });
  182. it('applies multiple tags', () => {
  183. const logger = new TestLogger();
  184. const root = new TagLogger(logger, 'tag1', 'tag2');
  185. // Ensure a record of each type is being logged with the expected tag
  186. logEachType(root);
  187. expectLogged({ level: 'debug', tag: ['tag1', 'tag2'], logger: logger });
  188. });
  189. });
  190. describe('UnveilLogger', () => {
  191. it('leaves normal log records untouched', () => {
  192. const logger = new TestLogger();
  193. const root = new UnveilLogger(logger);
  194. // Ensure a record of each expected type is being logged
  195. logEachType(root);
  196. expectLogged({ level: 'debug', logger: logger });
  197. });
  198. it('unveils confidential log record', () => {
  199. const logger = new TestLogger();
  200. const root = new UnveilLogger(logger);
  201. // Ensure a confidential record is being unveiled
  202. root.debug(new TestConfidential());
  203. expect(logger.records).toContain(['debug', 'uncensored']);
  204. });
  205. });
  206. describe('ConsoleLogger', () => {
  207. let backup: Logger = {} as Logger;
  208. let logger: TestLogger;
  209. beforeEach(() => {
  210. logger = new TestLogger();
  211. // Store each log level type method of console that we will override
  212. for (const type of LOG_TYPES) {
  213. backup[type] = console[type];
  214. }
  215. // Overwrite each log level type method of console
  216. for (const type of LOG_TYPES) {
  217. console[type] = logger[type];
  218. }
  219. });
  220. afterEach(() => {
  221. // Restore each log level type method of console that we have
  222. // previously overridden.
  223. for (const type of LOG_TYPES) {
  224. console[type] = backup[type];
  225. }
  226. });
  227. it('forwards log records to the console', () => {
  228. const root = new ConsoleLogger();
  229. // Ensure a record of each expected type is being logged
  230. logEachType(root);
  231. expectLogged({ level: 'debug', logger: logger });
  232. });
  233. it('leaves confidential log record untouched', () => {
  234. const root = new ConsoleLogger();
  235. // Ensure a confidential record is being left untouched
  236. const message = new TestConfidential();
  237. root.debug(message);
  238. expect(logger.records).toContain(['debug', message]);
  239. });
  240. });
  241. describe('MemoryLogger', () => {
  242. it('serialises each log record with a timestamp', () => {
  243. const start = Date.now();
  244. const logger = new MemoryLogger();
  245. // Ensure each log record has a timestamp.
  246. for (let i = 0; i < 10; ++i) {
  247. logger.debug(i);
  248. }
  249. const end = Date.now();
  250. const timestamps = JSON
  251. .parse(JSON.stringify(logger.getRecords(), MemoryLogger.replacer))
  252. .map((entry) => entry[0]);
  253. expect(timestamps.length).toBe(10);
  254. for (const timestamp of timestamps) {
  255. expect(timestamp).toBeGreaterThanOrEqual(start);
  256. expect(timestamp).toBeLessThanOrEqual(end);
  257. }
  258. });
  259. it('stores all arguments of the log record', () => {
  260. const logger = new MemoryLogger();
  261. // Ensure all arguments of the log record are being kept.
  262. // Note: All of these values need to serialised loss-less.
  263. const record = [
  264. 'debug',
  265. null,
  266. true,
  267. 1,
  268. 'a',
  269. [2, 3],
  270. { b: 4 },
  271. ];
  272. logger.debug(...record.slice(1));
  273. const records = JSON
  274. .parse(JSON.stringify(logger.getRecords(), MemoryLogger.replacer))
  275. .map((entry) => entry.slice(1));
  276. expect(records.length).toBe(1);
  277. expect(records[0]).toEqual(record);
  278. });
  279. it("trims the first log record's message (tag)", () => {
  280. const logger = new MemoryLogger();
  281. // Ensure %c CSS style formatting placeholder remains but the tag
  282. // is being trimmed.
  283. const args = [
  284. null,
  285. true,
  286. 1,
  287. 'a',
  288. [2, 3],
  289. { b: 4 },
  290. ];
  291. logger.debug(' te%cst ', 'color: #fff', ...args);
  292. const records = JSON
  293. .parse(JSON.stringify(logger.getRecords(), MemoryLogger.replacer))
  294. .map((entry) => entry.slice(1));
  295. expect(records.length).toBe(1);
  296. expect(records[0]).toEqual((['debug', 'te%cst', 'color: #fff'] as any[]).concat(args));
  297. });
  298. it("ignores style formatting beyond the log record's message (args)", () => {
  299. const logger = new MemoryLogger();
  300. // Ensure %c CSS style formatting placeholder and the following
  301. // argument are not being touched.
  302. const record = [
  303. 'debug',
  304. 'test',
  305. ' me%cow ',
  306. 'color: #fff',
  307. null,
  308. true,
  309. ' ra%cwr ',
  310. 'color: #ffa500',
  311. 1,
  312. 'a',
  313. [2, 3],
  314. { b: 4 },
  315. ];
  316. logger.debug(...record.slice(1));
  317. const records = JSON
  318. .parse(JSON.stringify(logger.getRecords(), MemoryLogger.replacer))
  319. .map((entry) => entry.slice(1));
  320. expect(records.length).toBe(1);
  321. expect(records[0]).toEqual(record);
  322. });
  323. it('serialises standard types', () => {
  324. const logger = new MemoryLogger();
  325. // Ensure 'null', 'boolean', 'number' and 'string' are being
  326. // represented loss-less.
  327. const record = [
  328. 'debug',
  329. null,
  330. false,
  331. 1337,
  332. 'meow?',
  333. ];
  334. logger.debug(...record.slice(1));
  335. const records = JSON
  336. .parse(JSON.stringify(logger.getRecords(), MemoryLogger.replacer))
  337. .map((entry) => entry.slice(1));
  338. expect(records.length).toBe(1);
  339. expect(records[0]).toEqual(record);
  340. });
  341. it('serialises confidential types sanitised', () => {
  342. const logger = new MemoryLogger();
  343. // Ensure 'Confidential' messages are being sanitised.
  344. const confidential = new TestConfidential();
  345. logger.debug(confidential, confidential, confidential);
  346. const records = JSON
  347. .parse(JSON.stringify(logger.getRecords(), MemoryLogger.replacer))
  348. .map((entry) => entry.slice(1));
  349. expect(records.length).toBe(1);
  350. expect(records[0]).toEqual(['debug', 'censored', 'censored', 'censored']);
  351. });
  352. it('serialises exceptions', () => {
  353. const logger = new MemoryLogger();
  354. // Ensure exceptions are being represented (in their lossy string form).
  355. const error = new Error('WTF!');
  356. logger.error(error);
  357. const records = JSON
  358. .parse(JSON.stringify(logger.getRecords(), MemoryLogger.replacer))
  359. .map((entry) => entry.slice(1));
  360. expect(records.length).toBe(1);
  361. expect(records[0]).toEqual(['error', error.toString()]);
  362. });
  363. it('serialises unrepresentable binary types', () => {
  364. const logger = new MemoryLogger();
  365. // Ensure 'ArrayBuffer', 'Uint8Array' and 'Blob' are being
  366. // represented with metadata only.
  367. const buffer = new ArrayBuffer(10);
  368. const array = new Uint8Array(buffer, 2, 6);
  369. const blob = new Blob([JSON.stringify({ a: 10 })], { type: 'application/json'} );
  370. logger.debug(buffer, array, blob);
  371. const records = JSON
  372. .parse(JSON.stringify(logger.getRecords(), MemoryLogger.replacer))
  373. .map((entry) => entry.slice(1));
  374. expect(records.length).toBe(1);
  375. expect(records[0]).toEqual([
  376. 'debug',
  377. '[ArrayBuffer: length=10]',
  378. '[Uint8Array: length=6, offset=2]',
  379. `[Blob: length=${blob.size}, type=application/json]`,
  380. ]);
  381. });
  382. it('serialises class instances', () => {
  383. const logger = new MemoryLogger();
  384. // Ensure instances are being represented with their name.
  385. logger.debug(logger);
  386. const records = JSON
  387. .parse(JSON.stringify(logger.getRecords(), MemoryLogger.replacer))
  388. .map((entry) => entry.slice(1));
  389. expect(records.length).toBe(1);
  390. expect(records[0]).toEqual(['debug', '[MemoryLogger]']);
  391. });
  392. it('serialises objects recursively', () => {
  393. const logger = new MemoryLogger();
  394. // Ensure an object's properties are being serialised recursively.
  395. const object = {
  396. bool: true,
  397. inner: {
  398. number: 4,
  399. },
  400. array: ['a', 'b'],
  401. };
  402. logger.debug(object);
  403. const records = JSON
  404. .parse(JSON.stringify(logger.getRecords(), MemoryLogger.replacer))
  405. .map((entry) => entry.slice(1));
  406. expect(records.length).toBe(1);
  407. expect(records[0]).toEqual(['debug', object]);
  408. });
  409. it('serialises arrays recursively', () => {
  410. const logger = new MemoryLogger();
  411. // Ensure each item of an array is being serialised recursively.
  412. const array = [
  413. false,
  414. { null: null },
  415. ['a', 'b'],
  416. ];
  417. logger.debug(array);
  418. const records = JSON
  419. .parse(JSON.stringify(logger.getRecords(), MemoryLogger.replacer))
  420. .map((entry) => entry.slice(1));
  421. expect(records.length).toBe(1);
  422. expect(records[0]).toEqual(['debug', array]);
  423. });
  424. it('respects the supplied log record limit', () => {
  425. const logger = new MemoryLogger(2);
  426. // Ensure only the last two log records are being kept
  427. for (let i = 0; i < 10; ++i) {
  428. logger.debug(i);
  429. }
  430. const records = JSON
  431. .parse(JSON.stringify(logger.getRecords(), MemoryLogger.replacer))
  432. .map((entry) => entry.slice(1));
  433. expect(records).toEqual([
  434. ['debug', 8],
  435. ['debug', 9],
  436. ]);
  437. });
  438. });
  439. });