All files / src/app/core/models/http-error http-error.helper.ts

100% Statements 20/20
100% Branches 16/16
100% Functions 13/13
100% Lines 16/16

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 5154x       54x               14x 14x             11x       17x     9x   26x       15x       26x 12x   2x   26x 18x     10x 9x      
import { escape, escapeRegExp, uniq } from 'lodash-es';
 
import { ErrorFeedback, HttpError } from './http-error.model';
 
export class HttpErrorHelper {
  /**
   * Formats all messages of an HTTP error for display: the main messages and their causes are rendered as separate lines.
   * ICM messages are trusted and may contain HTML, but the values of their parameters (e.g. a SKU entered by the user) are HTML-escaped.
   *
   * The result has to be rendered as sanitized HTML, e.g. in a toast or with the DomSanitizer.
   */
  static formatMessage(error: Partial<HttpError>): string {
    const lines = error?.errors?.flatMap(feedback => HttpErrorHelper.getLines(feedback)) ?? [];
    return lines.length ? HttpErrorHelper.joinLines(lines) : error?.message;
  }
 
  /**
   * Formats the message of a single error feedback and its causes, see {@link formatMessage}.
   */
  static formatFeedback(feedback: ErrorFeedback): string {
    return HttpErrorHelper.joinLines(HttpErrorHelper.getLines(feedback));
  }
 
  private static getLines(feedback: ErrorFeedback): string[] {
    return [
      HttpErrorHelper.escapeParameters(feedback?.message, feedback?.parameters),
      ...(feedback?.causes ?? []).map(cause =>
        HttpErrorHelper.escapeParameters(cause.message, { ...feedback.parameters, ...cause.parameters })
      ),
    ].filter(line => !!line);
  }
 
  private static joinLines(lines: string[]): string {
    return lines.length > 1 ? lines.map(line => `<div>${line}</div>`).join('') : lines[0];
  }
 
  private static escapeParameters(message: string, parameters: Record<string, unknown>): string {
    const values = uniq(Object.values(parameters ?? {}))
      .filter((value): value is string => typeof value === 'string' && !!value)
      // match longer values first, so that values contained in other values are escaped completely
      .sort((a, b) => b.length - a.length);
 
    if (!message || !values.length) {
      return message;
    }
    // escape all values in one pass with a callback, so replacement tokens like '$&' in values are not interpreted and nothing is escaped twice
    const pattern = new RegExp(values.map(value => escapeRegExp(value)).join('|'), 'g');
    return message.replace(pattern, match => escape(match));
  }
}