Merge pull request #50 from wimvelzeboer/feature/uuid

Add Uuid and HttpException. Logger now includes Uuid's
This commit is contained in:
William Velzeboer
2023-08-01 14:00:20 +01:00
committed by GitHub
19 changed files with 956 additions and 12 deletions
@@ -0,0 +1,138 @@
global virtual class fflib_HttpException extends Exception
{
protected fflib_HttpProblem problems;
global fflib_HttpException(Integer statusCode)
{
this(statusCode, fflib_HttpStatusCode.getTitle(statusCode));
}
global fflib_HttpException(Integer statusCode, String title)
{
this(statusCode, title, null);
}
global fflib_HttpException(Integer statusCode, String title, String detail)
{
this.problems =
new fflib_HttpProblem()
.setStatus(statusCode)
.setTitle(title)
.setDetail(detail);
addMessage(
String.valueOf(statusCode)
+ (String.isNotBlank(title) ? + ' - ' + title : '')
+ (String.isNotBlank(detail) ? ', ' + detail : '')
);
}
global fflib_HttpException addProblem(Integer statusCode)
{
return this.addProblem(statusCode, fflib_HttpStatusCode.getTitle(statusCode));
}
global fflib_HttpException addProblem(Integer statusCode, String title)
{
return this.addProblem(statusCode, title, null);
}
global fflib_HttpException addProblem(Integer statusCode, String title, String detail)
{
this.problems.addNextProblem()
.setStatus(statusCode)
.setTitle(title)
.setDetail(detail);
addMessage(
String.valueOf(statusCode)
+ (String.isNotBlank(title) ? + ' - ' + title : '')
+ (String.isNotBlank(detail) ? ': ' + detail : '')
);
return this;
}
global List<fflib_HttpProblemDetail> getProblemDetails()
{
return this.problems.getProblemDetails();
}
global String getUuid()
{
return this.problems.getUuid();
}
/**
* Set a URI reference that identifies the specific occurrence of the problem.
*
* @param instance URI reference to the problem location
*
* @return Instance of itself to allow for chaining
*/
global fflib_HttpException setInstance(String instance)
{
this.problems.setInstance(instance);
return this;
}
/**
* Sets the URI reference [RFC3986] that identifies the problem type.
* This specification encourages that, when dereferenced,
* it provide human-readable documentation for the problem type
* (e.g., using HTML [W3C.REC-html5-20141028]).
* When this member is not present, its value is assumed to be "about:blank".
*
* @param type URI to human-readable documentation of the occurred problem
*
* @return Instance of itself to allow for chaining
*/
global fflib_HttpException setType(String type)
{
this.problems.setType(type);
return this;
}
/**
* @return Returns a RFC-7807 Json object
*/
global String toJson()
{
return JSON.serialize(this.problems);
}
global String toPrettyJson()
{
return JSON.serializePretty(this.problems);
}
global Blob toJsonBlob()
{
return Blob.valueOf(toJson());
}
global void addToRestResponse()
{
RestResponse response = RestContext.response;
if (response == null)
{
response = new RestResponse();
RestContext.response = response;
}
response.statusCode = this.problems.getProblemDetails().get(0)?.status;
response.responseBody = this.toJsonBlob();
}
protected void addMessage(String message)
{
if (this.problems.getProblemDetails().size() > 1)
{
setMessage(getMessage() + ', ' + message);
}
else
{
setMessage(message);
}
}
}
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>57.0</apiVersion>
<status>Active</status>
</ApexClass>
@@ -0,0 +1,104 @@
global virtual class fflib_HttpProblem
{
protected List<fflib_HttpProblemDetail> errors;
// A uuid that can only be set once
protected final String uuid { get; set; }
global fflib_HttpProblem()
{
this.errors = new List<fflib_HttpProblemDetail> {new fflib_HttpProblemDetail()};
this.uuid = new fflib_Uuid().getValue();
}
global fflib_HttpProblem addNextProblem()
{
this.errors.add(new fflib_HttpProblemDetail());
return this;
}
global List<fflib_HttpProblemDetail> getProblemDetails()
{
return this.errors;
}
global String getUuid()
{
return this.uuid;
}
/**
* Set a human-readable explanation specific to this occurrence of the problem.
*
* @param message Detailed problem description
*
* @return Instance of itself to allow for chaining
*/
global fflib_HttpProblem setDetail(String message)
{
getLatestProblem().detail = message;
return this;
}
/**
* Set a URI reference that identifies the specific occurrence of the problem.
*
* @param instance URI reference to the problem location
*
* @return Instance of itself to allow for chaining
*/
global fflib_HttpProblem setInstance(String instance)
{
getLatestProblem().instance = instance;
return this;
}
/**
* Sets a short, human-readable summary of the problem.
*
* @param title Short summary of the problem
*
* @return Instance of itself to allow for chaining
*/
global fflib_HttpProblem setTitle(String title)
{
getLatestProblem().title = title;
return this;
}
/**
* Sets the URI reference [RFC3986] that identifies the problem type.
* This specification encourages that, when dereferenced,
* it provide human-readable documentation for the problem type
* (e.g., using HTML [W3C.REC-html5-20141028]).
* When this member is not present, its value is assumed to be "about:blank".
*
* @param type URI to human-readable documentation of the occurred problem
*
* @return Instance of itself to allow for chaining
*/
global fflib_HttpProblem setType(String type)
{
getLatestProblem().type = type;
return this;
}
/**
* Sets the HTTP status code of the problem.
*
* @param httpResponseCode A HTTP status code (https://developer.mozilla.org/en-US/docs/Web/HTTP/Status)
*
* @return Instance of itself to allow for chaining
*/
global fflib_HttpProblem setStatus(Integer httpResponseCode)
{
getLatestProblem().status = httpResponseCode;
return this;
}
protected fflib_HttpProblemDetail getLatestProblem()
{
return this.errors.get(this.errors.size() - 1);
}
}
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>57.0</apiVersion>
<status>Active</status>
</ApexClass>
@@ -0,0 +1,37 @@
/**
* A RRC 7807 structured Problem Detail Object
*/
global virtual class fflib_HttpProblemDetail
{
/**
* A human-readable explanation specific to this occurrence of the problem.
*/
global String detail { get; set; }
/**
* A URI reference that identifies the specific occurrence of the problem.
* It may or may not yield further information if de-referenced.
*/
global String instance { get; set; }
/**
* A short, human-readable summary of the problem type.
* It SHOULD NOT change from occurrence to occurrence of the
* problem, except for purposes of localization (e.g., using
* proactive content negotiation; see [RFC7231], Section 3.4).
*/
global String title { get; set; }
/**
* A URI reference that identifies the problem type.
* This specification encourages that, when problem type (e.g., using HTML [W3C.REC-html5-20141028]).
*/
global String type { get; set; }
/**
* The HTTP status code
*/
global Integer status { get; set; }
global fflib_HttpProblemDetail() {}
}
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>57.0</apiVersion>
<status>Active</status>
</ApexClass>
@@ -0,0 +1,128 @@
public class fflib_HttpStatusCode
{
// --- 1xx Informational ---
public static final Integer INFO_CONTINUE = 100;
public static final Integer INFO_SWITCHING_PROTOCOLS = 101;
public static final Integer INFO_PROCESSING = 102;
// --- 2xx Success ---
public static final Integer SUCCESS_OK = 200;
public static final Integer SUCCESS_CREATED = 201;
public static final Integer SUCCESS_ACCEPTED = 202;
public static final Integer SUCCESS_NON_AUTHORITATIVE_INFORMATION = 203;
public static final Integer SUCCESS_NO_CONTENT = 204;
public static final Integer SUCCESS_RESET_CONTENT = 205;
public static final Integer SUCCESS_PARTIAL_CONTENT = 206;
public static final Integer SUCCESS_MULTI_STATUS = 207;
// --- 3xx Redirection ---
public static final Integer REDIRECT_MULTIPLE_CHOICES = 300;
public static final Integer REDIRECT_MOVED_PERMANENTLY = 301;
public static final Integer REDIRECT_MOVED_TEMPORARILY = 302;
public static final Integer REDIRECT_SEE_OTHER = 303;
public static final Integer REDIRECT_NOT_MODIFIED = 304;
public static final Integer REDIRECT_USE_PROXY = 305;
public static final Integer REDIRECT_TEMPORARY_REDIRECT = 307;
// --- 4xx Client Error ---
public static final Integer CLIENT_ERROR_BAD_REQUEST = 400;
public static final Integer CLIENT_ERROR_UNAUTHORIZED = 401;
public static final Integer CLIENT_ERROR_PAYMENT_REQUIRED = 402;
public static final Integer CLIENT_ERROR_FORBIDDEN = 403;
public static final Integer CLIENT_ERROR_NOT_FOUND = 404;
public static final Integer CLIENT_ERROR_METHOD_NOT_ALLOWED = 405;
public static final Integer CLIENT_ERROR_NOT_ACCEPTABLE = 406;
public static final Integer CLIENT_ERROR_PROXY_AUTHENTICATION_REQUIRED = 407;
public static final Integer CLIENT_ERROR_REQUEST_TIMEOUT = 408;
public static final Integer CLIENT_ERROR_CONFLICT = 409;
public static final Integer CLIENT_ERROR_GONE = 410;
public static final Integer CLIENT_ERROR_LENGTH_REQUIRED = 411;
public static final Integer CLIENT_ERROR_PRECONDITION_FAILED = 412;
public static final Integer CLIENT_ERROR_REQUEST_TOO_LONG = 413;
public static final Integer CLIENT_ERROR_REQUEST_URI_TOO_LONG = 414;
public static final Integer CLIENT_ERROR_UNSUPPORTED_MEDIA_TYPE = 415;
public static final Integer CLIENT_ERROR_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
public static final Integer CLIENT_ERROR_EXPECTATION_FAILED = 417;
public static final Integer CLIENT_ERROR_INSUFFICIENT_SPACE_ON_RESOURCE = 419;
public static final Integer CLIENT_ERROR_METHOD_FAILURE = 420;
public static final Integer CLIENT_ERROR_UNPROCESSABLE_ENTITY = 422;
public static final Integer CLIENT_ERROR_LOCKED = 423;
public static final Integer CLIENT_ERROR_FAILED_DEPENDENCY = 424;
// --- 5xx Server Error ---
public static final Integer SERVER_ERROR_INTERNAL_SERVER_ERROR = 500;
public static final Integer SERVER_ERROR_NOT_IMPLEMENTED = 501;
public static final Integer SERVER_ERROR_BAD_GATEWAY = 502;
public static final Integer SERVER_ERROR_SERVICE_UNAVAILABLE = 503;
public static final Integer SERVER_ERROR_GATEWAY_TIMEOUT = 504;
public static final Integer SERVER_ERROR_HTTP_VERSION_NOT_SUPPORTED = 505;
public static final Integer SERVER_ERROR_INSUFFICIENT_STORAGE = 507;
public static final Map<Integer, String> MESSAGE_BY_STATUS_CODE = new Map<Integer, String>
{
100 => 'Continue',
101 => 'Switching Protocols',
102 => 'Processing',
200 => 'Ok',
201 => 'Created',
202 => 'Accepted',
203 => 'Non authoritative information',
204 => 'No content',
205 => 'Reset content',
206 => 'Partial content',
207 => 'Multi status',
300 => 'Multiple choices',
301 => 'Moved permanently',
302 => 'Moved temporarily',
303 => 'See other',
304 => 'Not modified',
305 => 'Use proxy',
307 => 'Temporary redirect',
400 => 'Bad request',
401 => 'Unauthorized',
402 => 'Payment required',
403 => 'Forbidden',
404 => 'Not found',
405 => 'Method not allowed',
406 => 'Not acceptable',
407 => 'Proxy authentication required',
408 => 'Request timeout',
409 => 'Conflict',
410 => 'Gone',
411 => 'Length required',
412 => 'Precondition failed',
413 => 'Request too long',
414 => 'Request uri too long',
415 => 'Unsupported media type',
416 => 'Requested range not satisfiable',
417 => 'Expectation failed',
419 => 'Insufficient space on resource',
420 => 'Method failure',
422 => 'Unprocessable entity',
423 => 'Locked',
424 => 'Failed dependency',
500 => 'Internal server error',
501 => 'Not implemented',
502 => 'Bad gateway',
503 => 'Service unavailable',
504 => 'Gateway timeout',
505 => 'Http version not supported',
507 => 'Insufficient storage'
};
public static String getTitle(Integer statusCode)
{
if (MESSAGE_BY_STATUS_CODE.containsKey(statusCode))
{
return MESSAGE_BY_STATUS_CODE.get(statusCode);
}
else
{
return null;
}
}
}
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>57.0</apiVersion>
<status>Active</status>
</ApexClass>
@@ -0,0 +1,166 @@
/******************************************************************************************
* This file is part of the Apex UUID project, released under the MIT License. *
* See LICENSE file or go to https://github.com/jongpie/ApexUuid for full license details. *
******************************************************************************************/
/**
* Changes made:
* - Renamed class into fflib_Uuid
* - Renamed some parameters to resolve dictionary warnings
* - Added the fflib Apex styling
*/
public without sharing class fflib_Uuid
{
private static final Integer HEX_BASE = HEX_CHARACTERS.length();
private static final String HEX_CHARACTERS = '0123456789abcdef';
private static final String HEX_PREFIX = '0x';
private static final List<String> HEX_CHARACTER_LIST = HEX_CHARACTERS.split('');
private static final Integer UUID_V4_LENGTH = 36;
private static final String UUID_V4_REGEX = '[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}';
private static final String ERROR_MESSAGE_INVALID_UUID = '{0} is not a valid UUID value';
public static String formatValue(String unformattedValue)
{
// Remove any non-alphanumeric characters
String nonformattedValue = unformattedValue.replaceAll('[^a-zA-Z0-9]', '');
// If the nonformatted value isn't even the right length to be valid, then throw an exception
// Subtract 4 because the UUID_V4_LENGTH includes 4 '-' characters in the UUID pattern
if (nonformattedValue.length() != (UUID_V4_LENGTH - 4))
{
throw new UuidException(String.format(ERROR_MESSAGE_INVALID_UUID, new List<String> {unformattedValue}));
}
// UUID Pattern: 8-4-4-4-12
String formattedValue =
nonformattedValue.substring(0, 8) +
'-' +
nonformattedValue.substring(8, 12) +
'-' +
nonformattedValue.substring(12, 16) +
'-' +
nonformattedValue.substring(16, 20) +
'-' +
nonformattedValue.substring(20);
formattedValue = formattedValue.toLowerCase();
if (!fflib_Uuid.isValid(formattedValue))
{
throw new UuidException(String.format(ERROR_MESSAGE_INVALID_UUID, new List<String> {nonformattedValue}));
}
return formattedValue;
}
public static Boolean isValid(String uuidValue)
{
if (String.isBlank(uuidValue) || uuidValue.length() != UUID_V4_LENGTH)
{
return false;
}
Pattern uuidPattern = Pattern.compile(UUID_V4_REGEX.toLowerCase());
Matcher uuidMatcher = uuidPattern.matcher(uuidValue.toLowerCase());
return uuidMatcher.matches();
}
public static fflib_Uuid valueOf(String uuidValue)
{
return new fflib_Uuid(uuidValue);
}
private final String value;
public fflib_Uuid() {
this.value = this.generateValue();
}
private fflib_Uuid(String uuidValue) {
this.value = fflib_Uuid.formatValue(uuidValue);
}
public String getValue()
{
return this.value;
}
private String generateValue()
{
String hexValue = EncodingUtil.convertToHex(Crypto.generateAesKey(128));
// Version Calculation: (i & 0x0f) | 0x40
// Version Format: Always begins with 4
String versionShiftedHexBits = this.getShiftedHexBits(
hexValue.substring(14, 16),
this.convertHexToInteger('0x0f'),
this.convertHexToInteger('0x40')
);
// Variant Calculation: (i & 0x3f) | 0x80
// Variant Format: Always begins with 8, 9, A or B
String variantShiftedHexBits = this.getShiftedHexBits(
hexValue.substring(18, 20),
this.convertHexToInteger('0x3f'),
this.convertHexToInteger('0x80')
);
String uuidValue =
hexValue.substring(0, 8) + // time-low
hexValue.substring(8, 12) + // time-mid
versionShiftedHexBits +
hexValue.substring(14, 16) + // time-high-and-version
variantShiftedHexBits +
hexValue.substring(18, 20) + // clock-seq-and-reserved + clock-seq-low
hexValue.substring(20); // node
return fflib_Uuid.formatValue(uuidValue);
}
private String getShiftedHexBits(
String hexSubstring,
Integer lowerThreshold,
Integer upperThreshold
)
{
Integer shiftedIntegerBits =
(this.convertHexToInteger(hexSubstring) & lowerThreshold) |
upperThreshold;
return this.convertIntegerToHex(shiftedIntegerBits);
}
private Integer convertHexToInteger(String hexValue)
{
hexValue = hexValue.toLowerCase();
if (hexValue.startsWith(HEX_PREFIX))
{
hexValue = hexValue.substringAfter(HEX_PREFIX);
}
Integer integerValue = 0;
for (String hexCharacter : hexValue.split(''))
{
Integer hexCharacterIndex = HEX_CHARACTERS.indexOf(hexCharacter);
integerValue = HEX_BASE * integerValue + hexCharacterIndex;
}
return integerValue;
}
private String convertIntegerToHex(Integer integerValue)
{
String hexValue = '';
while (integerValue > 0)
{
Integer hexCharacterIndex = Math.mod(integerValue, HEX_BASE);
hexValue = HEX_CHARACTER_LIST[hexCharacterIndex] + hexValue;
integerValue = integerValue / HEX_BASE;
}
return hexValue;
}
private class UuidException extends Exception {}
}
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>58.0</apiVersion>
<status>Active</status>
</ApexClass>
@@ -29,6 +29,7 @@
*/
public with sharing class fflib_LogEntry
{
public String uuid { get; set; }
public String className { get; set; }
public String executionContext { get; set; }
public Decimal lineNumber { get; set; }
@@ -61,6 +62,7 @@ public with sharing class fflib_LogEntry
this.methodName = logEvent.MethodName__c;
this.userId = logEvent.CreatedById;
this.createdDatetime = logEvent.CreatedDate;
this.uuid = String.isBlank(logEvent.Uuid__c) ? logEvent.EventUuid : logEvent.Uuid__c;
}
public fflib_LogEvent__e toPlatformEvent()
@@ -71,7 +73,8 @@ public with sharing class fflib_LogEntry
LineNumber__c = this.lineNumber,
LogLevel__c = this.logLevel.name(),
Message__c = this.message,
MethodName__c = this.methodName
MethodName__c = this.methodName,
Uuid__c = this.uuid
);
}
}
@@ -54,7 +54,12 @@ public with sharing class fflib_Logger implements fflib_ILogger
public void add(LoggingLevel loggingLevel, Exception e)
{
add(new fflib_LogEntry(loggingLevel, messageFormatter.format(e)));
fflib_LogEntry logEntry = new fflib_LogEntry(loggingLevel, messageFormatter.format(e));
if (e instanceof fflib_HttpException)
{
logEntry.uuid = ((fflib_HttpException) e).getUuid();
}
add(logEntry);
}
public void add(LoggingLevel loggingLevel, String message)
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Uuid__c</fullName>
<externalId>false</externalId>
<isFilteringDisabled>false</isFilteringDisabled>
<isNameField>false</isNameField>
<isSortingDisabled>false</isSortingDisabled>
<label>UUID</label>
<description>A universally unique identifier (UUID) for the error. Its useful to expose this UUID in the UI/API when errors occur so that the log entry for that issue can easily be found</description>
<inlineHelpText>A universally unique identifier (UUID) for the error</inlineHelpText>
<length>36</length>
<required>false</required>
<type>Text</type>
<unique>false</unique>
</CustomField>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Uuid__c</fullName>
<externalId>false</externalId>
<isFilteringDisabled>false</isFilteringDisabled>
<isNameField>false</isNameField>
<isSortingDisabled>false</isSortingDisabled>
<label>UUID</label>
<description>A universally unique identifier (UUID) for the error. Its useful to expose this UUID in the UI/API when errors occur so that the log entry for that issue can easily be found</description>
<inlineHelpText>A universally unique identifier (UUID) for the error</inlineHelpText>
<length>36</length>
<required>false</required>
<type>Text</type>
<unique>false</unique>
</CustomField>
@@ -0,0 +1,180 @@
@IsTest
private class fflib_HttpExceptionTest
{
@IsTest
static void itShouldThrowExceptionWithOnlyHttpSResponseStatusCode()
{
try
{
throw new fflib_HttpException(fflib_HttpStatusCode.CLIENT_ERROR_BAD_REQUEST);
}
catch (fflib_HttpException e)
{
final List<fflib_HttpProblemDetail> problemDetails = e.getProblemDetails();
System.Assert.areEqual(1, problemDetails.size(), 'Expected only one problem');
final fflib_HttpProblemDetail problem = problemDetails.get(0);
System.Assert.areEqual(400, problem.status, 'Incorrect returned http response status code');
System.Assert.areEqual(
fflib_HttpStatusCode.getTitle(400),
problem.title,
'Incorrect returned http response status title');
System.Assert.isNull(problem.detail, 'Expected problem detail to be blank');
System.Assert.isNull(problem.instance, 'Expected problem instance to be blank');
System.Assert.isNull(problem.type, 'Expected problem type to be blank');
System.Assert.areEqual(
'400 - ' + fflib_HttpStatusCode.getTitle(400),
e.getMessage(),
'Incorrect returned exception message'
);
System.Assert.isTrue(fflib_Uuid.isValid(e.getUuid()), 'Invalid Uuid');
}
}
@IsTest
static void itShouldThrowExceptionWithStatusCodeAndCustomTitle()
{
final String myTitle = 'My Error message Title';
try
{
throw new fflib_HttpException(fflib_HttpStatusCode.CLIENT_ERROR_BAD_REQUEST, myTitle);
}
catch (fflib_HttpException e)
{
final List<fflib_HttpProblemDetail> problemDetails = e.getProblemDetails();
System.Assert.areEqual(1, problemDetails.size(), 'Expected only one problem');
final fflib_HttpProblemDetail problem = problemDetails.get(0);
System.Assert.areEqual(400, problem.status, 'Incorrect returned http response status code');
System.Assert.areEqual(
myTitle,
problem.title,
'Incorrect returned http response status title');
System.Assert.isNull(problem.detail, 'Expected problem detail to be blank');
System.Assert.isNull(problem.instance, 'Expected problem instance to be blank');
System.Assert.isNull(problem.type, 'Expected problem type to be blank');
System.Assert.areEqual(
'400 - ' + myTitle,
e.getMessage(),
'Incorrect returned exception message'
);
}
}
@IsTest
static void itShouldThrowExceptionWithStatusCodeAndCustomTitleAndDetail()
{
final String myTitle = 'My Error message Title';
final String myDetail = 'Very lengthy description of the error';
try
{
throw new fflib_HttpException(fflib_HttpStatusCode.CLIENT_ERROR_BAD_REQUEST, myTitle, myDetail);
}
catch (fflib_HttpException e)
{
final List<fflib_HttpProblemDetail> problemDetails = e.getProblemDetails();
System.Assert.areEqual(1, problemDetails.size(), 'Expected only one problem');
final fflib_HttpProblemDetail problem = problemDetails.get(0);
System.Assert.areEqual(400, problem.status, 'Incorrect returned http response status code');
System.Assert.areEqual(
myTitle,
problem.title,
'Incorrect returned http response status title');
System.Assert.areEqual(
myDetail,
problem.detail,
'Incorrect returned problem detail');
System.Assert.isNull(problem.instance, 'Expected problem instance to be blank');
System.Assert.isNull(problem.type, 'Expected problem type to be blank');
System.Assert.areEqual(
'400 - ' + myTitle + ', ' + myDetail,
e.getMessage(),
'Incorrect returned exception message'
);
}
}
@IsTest
static void itShouldThrowExceptionWithInstance()
{
final String instanceUri = '/somewhere/over/the/rainbow';
try
{
throw new fflib_HttpException(fflib_HttpStatusCode.CLIENT_ERROR_BAD_REQUEST)
.setInstance(instanceUri);
}
catch (fflib_HttpException e)
{
final List<fflib_HttpProblemDetail> problemDetails = e.getProblemDetails();
System.Assert.areEqual(1, problemDetails.size(), 'Expected only one problem');
final fflib_HttpProblemDetail problem = problemDetails.get(0);
System.Assert.areEqual(400, problem.status, 'Incorrect returned http response status code');
System.Assert.areEqual(
fflib_HttpStatusCode.getTitle(400),
problem.title,
'Incorrect returned http response status title');
System.Assert.isNull(problem.detail, 'Expected problem instance to be blank');
System.Assert.areEqual(instanceUri, problem.instance, 'Incorrect returned problem instance');
System.Assert.isNull(problem.type, 'Expected problem type to be blank');
System.Assert.areEqual(
'400 - ' + fflib_HttpStatusCode.getTitle(400),
e.getMessage(),
'Incorrect returned exception message'
);
}
}
@IsTest
static void itShouldThrowException()
{
final String myTitle = 'My Error message Title';
final String myDetail = 'Very lengthy description of the error';
final String instanceUri = '/somewhere/over/the/rainbow';
final String typeUri = '/somewhere/very/knowledgeable';
try
{
throw new fflib_HttpException(fflib_HttpStatusCode.CLIENT_ERROR_BAD_REQUEST, myTitle, myDetail)
.setInstance(instanceUri)
.setType(typeUri);
}
catch (fflib_HttpException e)
{
final List<fflib_HttpProblemDetail> problemDetails = e.getProblemDetails();
System.Assert.areEqual(1, problemDetails.size(), 'Expected only one problem');
final fflib_HttpProblemDetail problem = problemDetails.get(0);
System.Assert.areEqual(400, problem.status, 'Incorrect returned http response status code');
System.Assert.areEqual(myTitle, problem.title, 'Incorrect returned http response status title');
System.Assert.areEqual(myDetail, problem.detail, 'Incorrect returned problem detail');
System.Assert.areEqual(instanceUri, problem.instance, 'Incorrect returned problem instance');
System.Assert.areEqual(typeUri, problem.type, 'Incorrect returned problem type');
System.Assert.areEqual(
'400 - ' + myTitle + ', ' + myDetail,
e.getMessage(),
'Incorrect returned exception message'
);
}
}
@IsTest
static void itShouldThrowExceptionWithMultipleProblems()
{
try
{
throw new fflib_HttpException(fflib_HttpStatusCode.CLIENT_ERROR_BAD_REQUEST)
.addProblem(fflib_HttpStatusCode.CLIENT_ERROR_METHOD_NOT_ALLOWED);
}
catch (fflib_HttpException e)
{
List<fflib_HttpProblemDetail> problemDetails = e.getProblemDetails();
System.Assert.areEqual(2, problemDetails.size(), 'Expected only one firstProblem');
final fflib_HttpProblemDetail firstProblem = problemDetails.get(0);
System.Assert.areEqual(400, firstProblem.status, 'Incorrect returned http response status code for first problem');
final fflib_HttpProblemDetail secondProblem = problemDetails.get(1);
System.Assert.areEqual(405, secondProblem.status, 'Incorrect returned http response status code for second problem');
System.Assert.areEqual(
'400 - ' + fflib_HttpStatusCode.getTitle(400) +
', 405 - ' + fflib_HttpStatusCode.getTitle(405),
e.getMessage(),
'Incorrect returned exception message'
);
}
}
}
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>57.0</apiVersion>
<status>Active</status>
</ApexClass>
@@ -41,7 +41,7 @@ private class fflib_LoggerTest
{
'LogTestMockException:[]:',
ERROR_MESSAGE,
'()'
'Class.fflib_LoggerTest.itShouldAppendDebugException:'
}
);
private static final String ERROR_MESSAGE_FORMATTED_WITH_ARGS =
@@ -83,9 +83,6 @@ private class fflib_LoggerTest
);
}
/**
* todo : For some reason this method fails when run by Jenkins deployment user during a build, need to investigate why and re-enable it
@IsTest
static void itShouldAppendDebugException()
{
@@ -97,13 +94,30 @@ private class fflib_LoggerTest
log.debug(e);
// THEN - It should be appended to the logAppender
System.Assert.isTrue(
APPENDER_MOCK
.getAppendedMessages(System.LoggingLevel.DEBUG)
.contains(ERROR_MESSAGE_FORMATTED_WITH_EXCEPTION)
);
final List<String> result = APPENDER_MOCK
.getAppendedMessages(System.LoggingLevel.DEBUG);
System.debug(result);
System.Assert.areEqual(1, result.size(), 'Expected only one error');
System.Assert.isTrue(result.get(0).startsWith(ERROR_MESSAGE_FORMATTED_WITH_EXCEPTION), 'Incorrect returned error message');
}
*/
@IsTest
static void itShouldAppendDebugExceptionWithUuid()
{
// GIVEN - An error message and a configured logger
fflib_ILogger log = generatedLoggerWithMockAppender();
final fflib_HttpException e = new fflib_HttpException(400);
final String uuid = e.getUuid();
// WHEN - When the error is logged
log.debug(e);
// THEN - It should be appended to the logAppender
final List<fflib_LogEntry> result = APPENDER_MOCK.getEntries();
System.Assert.areEqual(1, result.size(), 'Expected only one log entry');
System.Assert.areEqual(uuid, result.get(0).uuid, 'Incorrect UUID on the LogEntry');
}
@IsTest
static void itShouldAppendDebugMessageWithArguments()
@@ -255,6 +269,12 @@ private class fflib_LoggerTest
{
this.appendedEntries.add(logEntry);
}
public List<fflib_LogEntry> getEntries()
{
return this.appendedEntries;
}
public List<String> getAppendedMessages(System.LoggingLevel loggingLevel)
{
List<String> result = new List<String>();
@@ -0,0 +1,98 @@
@IsTest
private class fflib_UuidTest
{
@IsTest
static void it_should_create_several_valid_uuids()
{
String generatedUuid = new fflib_Uuid().getValue();
System.assertEquals(36, generatedUuid.length());
Pattern pattern = Pattern.compile(
'[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}'
);
for (Integer i = 0; i < 10; i++)
{
fflib_Uuid uuid = new fflib_Uuid();
Matcher matcher = pattern.matcher(uuid.getValue());
System.assert(matcher.matches(), 'Generated UUID=' + uuid.getValue());
}
}
@IsTest
static void it_should_reuse_a_uuid_on_subsequent_calls()
{
fflib_Uuid uuid = new fflib_Uuid();
String originalValue = uuid.getValue();
for (Integer i = 0; i < 5; i++)
{
System.assertEquals(originalValue, uuid.getValue());
}
}
@IsTest
static void it_should_verify_that_a_uuid_is_a_uuid()
{
String generatedUuid = new fflib_Uuid().getValue();
System.assert(fflib_Uuid.isValid(generatedUuid));
}
@IsTest
static void it_should_not_consider_a_blank_string_a_uuid()
{
System.assertEquals(false, fflib_Uuid.isValid(''));
}
@IsTest
static void it_should_not_consider_null_a_uuid()
{
System.assertEquals(false, fflib_Uuid.isValid(null));
}
@IsTest
static void it_should_validate_a_uuid_in_upper_case()
{
String exampleUuid = 'f3665813-1a60-4924-ad9b-23a9cef17d80'.toUpperCase();
System.assertEquals(true, fflib_Uuid.isValid(exampleUuid));
}
@IsTest
static void it_should_validate_a_uuid_in_lower_case()
{
String exampleUuid = 'f3665813-1a60-4924-ad9b-23a9cef17d80'.toLowerCase();
System.assertEquals(true, fflib_Uuid.isValid(exampleUuid));
}
@IsTest
static void it_should_convert_a_valid_string_to_a_uuid()
{
String uuidValue = new fflib_Uuid().getValue();
Test.startTest();
fflib_Uuid convertedUuid = fflib_Uuid.valueOf(uuidValue);
Test.stopTest();
System.assertEquals(uuidValue, convertedUuid.getValue());
}
@IsTest
static void it_should_not_convert_an_invalid_string_to_a_uuid()
{
String invalidUuidValue = 'this-is-not-a-valid-uuid';
Test.startTest();
try
{
fflib_Uuid.valueOf(invalidUuidValue);
System.assert(false, 'Error expected here');
}
catch (Exception ex)
{
String expectedError = invalidUuidValue + ' is not a valid UUID value';
System.Assert.areEqual(expectedError, ex.getMessage());
}
Test.stopTest();
}
}
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>58.0</apiVersion>
<status>Active</status>
</ApexClass>