From e5ea45a4c5ade75ea4a29bad432cb3f0e7c208cb Mon Sep 17 00:00:00 2001 From: Piotr PG Gajek Date: Sun, 3 Aug 2025 12:39:23 +0200 Subject: [PATCH 1/8] get counters for assertions --- force-app/main/default/classes/HttpMock.cls | 46 ++++++++- .../main/default/classes/HttpMockTest.cls | 93 +++++++++++++++++++ 2 files changed, 138 insertions(+), 1 deletion(-) diff --git a/force-app/main/default/classes/HttpMock.cls b/force-app/main/default/classes/HttpMock.cls index 21fa105..6aea6ea 100644 --- a/force-app/main/default/classes/HttpMock.cls +++ b/force-app/main/default/classes/HttpMock.cls @@ -9,10 +9,18 @@ @SuppressWarnings('PMD.ExcessivePublicCount') public class HttpMock implements HttpMockLib, HttpCalloutMock { /* - new HttpMock() + new HttpMock + .getGetRequestsCount('/api/v1/authorize'); .whenGetOn('/api/v1/authorize').body('{ "token": "aZ3Xb7Qk" }').contentTypeJson().statusCodeOk() .whenPostOn('/api/v1/create').body('{ "success": true, "message": null }').contentTypeJson().statusCodeOk() .mock(); + + Test.startTest(); + // ... + Test.stopTest(); + + Assert.areEqual(1, HttpMock.getGetRequestsCount('/api/v1/authorize'), 'One GET request should be made'); + Assert.areEqual(1, HttpMock.getPostRequestsCount('/api/v1/create'), 'One POST request should be made'); */ public interface HttpMockLib { HttpMock whenGetOn(String endpointToMock); @@ -55,6 +63,42 @@ public class HttpMock implements HttpMockLib, HttpCalloutMock { HttpMock header(String key, String value); // Mock void mock(); + // Request Counts (static) for assertions + Integer getGetRequestsCount(String endpoint); + Integer getPostRequestsCount(String endpoint); + Integer getPutRequestsCount(String endpoint); + Integer getPatchRequestsCount(String endpoint); + Integer getDeleteRequestsCount(String endpoint); + Integer getTraceRequestsCount(String endpoint); + Integer getHeadRequestsCount(String endpoint); + } + + public static Integer getGetRequestsCount(String endpoint) { + return getRequestCount('GET', endpoint); + } + + public static Integer getPostRequestsCount(String endpoint) { + return getRequestCount('POST', endpoint); + } + + public static Integer getPutRequestsCount(String endpoint) { + return getRequestCount('PUT', endpoint); + } + + public static Integer getPatchRequestsCount(String endpoint) { + return getRequestCount('PATCH', endpoint); + } + + public static Integer getDeleteRequestsCount(String endpoint) { + return getRequestCount('DELETE', endpoint); + } + + public static Integer getTraceRequestsCount(String endpoint) { + return getRequestCount('TRACE', endpoint); + } + + public static Integer getHeadRequestsCount(String endpoint) { + return getRequestCount('HEAD', endpoint); } public static Integer getRequestCount(String httpMethod, String endpoint) { diff --git a/force-app/main/default/classes/HttpMockTest.cls b/force-app/main/default/classes/HttpMockTest.cls index fec9c29..460cbb9 100644 --- a/force-app/main/default/classes/HttpMockTest.cls +++ b/force-app/main/default/classes/HttpMockTest.cls @@ -537,6 +537,99 @@ private class HttpMockTest { Assert.areEqual(2, HttpMock.getRequestCount('GET', '/api/v1'), 'Request count should be 2'); } + @IsTest + static void getRequestCountWithMultipleEndpoints() { + new HttpMock() + .whenGetOn('/api/v1').statusCodeOk() + .whenGetOn('/api/v2').statusCodeOk() + .mock(); + + Test.startTest(); + new TestApi().makeCallout('GET', '/api/v1'); + new TestApi().makeCallout('GET', '/api/v2'); + Test.stopTest(); + + Assert.areEqual(1, HttpMock.getRequestCount('GET', '/api/v1'), 'Request count should be 1'); + Assert.areEqual(1, HttpMock.getRequestCount('GET', '/api/v2'), 'Request count should be 1'); + } + + @IsTest + static void getGetRequestsCount() { + new HttpMock().whenGetOn('/api/v1').statusCodeOk().mock(); + + Test.startTest(); + new TestApi().makeCallout('GET', '/api/v1'); + Test.stopTest(); + + Assert.areEqual(1, HttpMock.getGetRequestsCount('/api/v1'), 'Request count should be 1'); + } + + @IsTest + static void getPostRequestsCount() { + new HttpMock().whenPostOn('/api/v1').statusCodeOk().mock(); + + Test.startTest(); + new TestApi().makeCallout('POST', '/api/v1'); + Test.stopTest(); + + Assert.areEqual(1, HttpMock.getPostRequestsCount('/api/v1'), 'Request count should be 1'); + } + + @IsTest + static void getPutRequestsCount() { + new HttpMock().whenPutOn('/api/v1').statusCodeOk().mock(); + + Test.startTest(); + new TestApi().makeCallout('PUT', '/api/v1'); + Test.stopTest(); + + Assert.areEqual(1, HttpMock.getPutRequestsCount('/api/v1'), 'Request count should be 1'); + } + + @IsTest + static void getPatchRequestsCount() { + new HttpMock().whenPatchOn('/api/v1').statusCodeOk().mock(); + + Test.startTest(); + new TestApi().makeCallout('PATCH', '/api/v1'); + Test.stopTest(); + + Assert.areEqual(1, HttpMock.getPatchRequestsCount('/api/v1'), 'Request count should be 1'); + } + + @IsTest + static void getDeleteRequestsCount() { + new HttpMock().whenDeleteOn('/api/v1').statusCodeOk().mock(); + + Test.startTest(); + new TestApi().makeCallout('DELETE', '/api/v1'); + Test.stopTest(); + + Assert.areEqual(1, HttpMock.getDeleteRequestsCount('/api/v1'), 'Request count should be 1'); + } + + @IsTest + static void getTraceRequestsCount() { + new HttpMock().whenTraceOn('/api/v1').statusCodeOk().mock(); + + Test.startTest(); + new TestApi().makeCallout('TRACE', '/api/v1'); + Test.stopTest(); + + Assert.areEqual(1, HttpMock.getTraceRequestsCount('/api/v1'), 'Request count should be 1'); + } + + @IsTest + static void getHeadRequestsCount() { + new HttpMock().whenHeadOn('/api/v1').statusCodeOk().mock(); + + Test.startTest(); + new TestApi().makeCallout('HEAD', '/api/v1'); + Test.stopTest(); + + Assert.areEqual(1, HttpMock.getHeadRequestsCount('/api/v1'), 'Request count should be 1'); + } + @IsTest static void httpMethodNotMocked() { new HttpMock() From 1aa88d61685fb7b7c6fdba87d92d4b353e451adc Mon Sep 17 00:00:00 2001 From: Kai Tribble Date: Mon, 8 Sep 2025 11:49:57 -0400 Subject: [PATCH 2/8] create support for static resources and test results --- force-app/main/default/classes/HttpMock.cls | 14 ++++++ .../main/default/classes/HttpMockTest.cls | 44 +++++++++++++++++++ .../default/staticresources/responseBody.json | 6 +++ .../responseBody.resource-meta.xml | 6 +++ 4 files changed, 70 insertions(+) create mode 100644 force-app/main/default/staticresources/responseBody.json create mode 100644 force-app/main/default/staticresources/responseBody.resource-meta.xml diff --git a/force-app/main/default/classes/HttpMock.cls b/force-app/main/default/classes/HttpMock.cls index 21fa105..34860c7 100644 --- a/force-app/main/default/classes/HttpMock.cls +++ b/force-app/main/default/classes/HttpMock.cls @@ -35,6 +35,8 @@ public class HttpMock implements HttpMockLib, HttpCalloutMock { HttpMock contentTypePdf(); // application/pdf HttpMock contentTypeFormUrlencoded(); // application/x-www-form-urlencoded HttpMock contentType(String contentType); + // Static Resource + HttpMock staticResource(String staticResourceName); // Status Code HttpMock statusCodeOk(); // 200 HttpMock statusCodeCreated(); // 201 @@ -175,6 +177,17 @@ public class HttpMock implements HttpMockLib, HttpCalloutMock { return this; } + public HttpMock staticResource(String staticResourceName) { + StaticResource staticResource = null; + try { + staticResource = [SELECT Body FROM StaticResource WHERE Name = :staticResourceName LIMIT 1]; + } catch (QueryException queryEx) { + throw new StaticResourceNotFoundException('Static Resource "' + staticResourceName + '" not found.'); + } + + return this.body(staticResource.Body); + } + public HttpMock statusCodeOk() { return this.statusCode(200); } @@ -302,4 +315,5 @@ public class HttpMock implements HttpMockLib, HttpCalloutMock { public class HttpMethodNotMockedException extends Exception {} public class HttpEndpointNotMockedException extends Exception {} + public class StaticResourceNotFoundException extends Exception {} } diff --git a/force-app/main/default/classes/HttpMockTest.cls b/force-app/main/default/classes/HttpMockTest.cls index fec9c29..4323eeb 100644 --- a/force-app/main/default/classes/HttpMockTest.cls +++ b/force-app/main/default/classes/HttpMockTest.cls @@ -238,6 +238,50 @@ private class HttpMockTest { Assert.areEqual('text/html', response.getHeader('Content-Type'), 'Content type should be text/html'); } + @IsTest + static void staticResourceBody() { + String staticResourceName = 'responseBody'; + + new HttpMock() + .whenGetOn('/api/v1').staticResource(staticResourceName) + .mock(); + + Test.startTest(); + HttpResponse response = new TestApi().makeCallout('GET', '/api/v1'); + Test.stopTest(); + + SampleResponseBody responseBody = (SampleResponseBody) JSON.deserialize(response.getBody(), SampleResponseBody.class); + Assert.areEqual('responseBody', responseBody.name, 'Name should be responseBody'); + Assert.areEqual(1, responseBody.order, 'Order should be 1'); + Assert.isTrue(responseBody.active, 'Active should be true'); + Assert.areEqual(new List{'This is a sample JSON request body for API testing.'}, responseBody.additionalDetails, 'Additional details list should be the same'); + } + + class SampleResponseBody { + public String name; + public Integer order; + public Boolean active; + public List additionalDetails; + } + + @IsTest + static void staticResourceNotFound() { + String staticResourceName = 'aResourceThatDoesNotExist12345'; + + Test.startTest(); + Exception caughtEx; + try { + new HttpMock() + .whenGetOn('/api/v1').staticResource(staticResourceName) + .mock(); + } catch (Exception ex) { + caughtEx = ex; + } + Test.stopTest(); + + Assert.isInstanceOfType(caughtEx, HttpMock.StaticResourceNotFoundException.class, 'Exception should be StaticResourceNotFoundException'); + } + @IsTest static void statusCodeOk() { new HttpMock() diff --git a/force-app/main/default/staticresources/responseBody.json b/force-app/main/default/staticresources/responseBody.json new file mode 100644 index 0000000..2f1f4d5 --- /dev/null +++ b/force-app/main/default/staticresources/responseBody.json @@ -0,0 +1,6 @@ +{ + "name": "responseBody", + "order": 1, + "active": true, + "additionalDetails": [ "This is a sample JSON request body for API testing." ] +} \ No newline at end of file diff --git a/force-app/main/default/staticresources/responseBody.resource-meta.xml b/force-app/main/default/staticresources/responseBody.resource-meta.xml new file mode 100644 index 0000000..1bf29d0 --- /dev/null +++ b/force-app/main/default/staticresources/responseBody.resource-meta.xml @@ -0,0 +1,6 @@ + + + Private + application/json + A sample response body for testing static resource loading for http mocking + From 6e0f09e5656d5d0f49482adfea37ffefd3828726 Mon Sep 17 00:00:00 2001 From: Piotr PG Gajek Date: Wed, 24 Dec 2025 20:20:34 +0100 Subject: [PATCH 3/8] Refactoring --- force-app/main/default/classes/HttpMock.cls | 5 ++-- .../main/default/classes/HttpMockTest.cls | 23 +++---------------- .../default/staticresources/responseBody.json | 6 ----- .../responseBody.resource-meta.xml | 6 ----- 4 files changed, 6 insertions(+), 34 deletions(-) delete mode 100644 force-app/main/default/staticresources/responseBody.json delete mode 100644 force-app/main/default/staticresources/responseBody.resource-meta.xml diff --git a/force-app/main/default/classes/HttpMock.cls b/force-app/main/default/classes/HttpMock.cls index 305d0a1..49f8a75 100644 --- a/force-app/main/default/classes/HttpMock.cls +++ b/force-app/main/default/classes/HttpMock.cls @@ -34,6 +34,8 @@ public class HttpMock implements HttpMockLib, HttpCalloutMock { HttpMock body(Object body); HttpMock body(String body); HttpMock body(Blob body); + // Static Resource + HttpMock staticResource(String staticResourceName); // Content-Type HttpMock contentTypePlainText(); // text/plain HttpMock contentTypeHtml(); // text/html @@ -43,8 +45,6 @@ public class HttpMock implements HttpMockLib, HttpCalloutMock { HttpMock contentTypePdf(); // application/pdf HttpMock contentTypeFormUrlencoded(); // application/x-www-form-urlencoded HttpMock contentType(String contentType); - // Static Resource - HttpMock staticResource(String staticResourceName); // Status Code HttpMock statusCodeOk(); // 200 HttpMock statusCodeCreated(); // 201 @@ -223,6 +223,7 @@ public class HttpMock implements HttpMockLib, HttpCalloutMock { public HttpMock staticResource(String staticResourceName) { StaticResource staticResource = null; + try { staticResource = [SELECT Body FROM StaticResource WHERE Name = :staticResourceName LIMIT 1]; } catch (QueryException queryEx) { diff --git a/force-app/main/default/classes/HttpMockTest.cls b/force-app/main/default/classes/HttpMockTest.cls index e65d350..c90085f 100644 --- a/force-app/main/default/classes/HttpMockTest.cls +++ b/force-app/main/default/classes/HttpMockTest.cls @@ -238,25 +238,6 @@ private class HttpMockTest { Assert.areEqual('text/html', response.getHeader('Content-Type'), 'Content type should be text/html'); } - @IsTest - static void staticResourceBody() { - String staticResourceName = 'responseBody'; - - new HttpMock() - .whenGetOn('/api/v1').staticResource(staticResourceName) - .mock(); - - Test.startTest(); - HttpResponse response = new TestApi().makeCallout('GET', '/api/v1'); - Test.stopTest(); - - SampleResponseBody responseBody = (SampleResponseBody) JSON.deserialize(response.getBody(), SampleResponseBody.class); - Assert.areEqual('responseBody', responseBody.name, 'Name should be responseBody'); - Assert.areEqual(1, responseBody.order, 'Order should be 1'); - Assert.isTrue(responseBody.active, 'Active should be true'); - Assert.areEqual(new List{'This is a sample JSON request body for API testing.'}, responseBody.additionalDetails, 'Additional details list should be the same'); - } - class SampleResponseBody { public String name; public Integer order; @@ -270,6 +251,7 @@ private class HttpMockTest { Test.startTest(); Exception caughtEx; + try { new HttpMock() .whenGetOn('/api/v1').staticResource(staticResourceName) @@ -279,7 +261,8 @@ private class HttpMockTest { } Test.stopTest(); - Assert.isInstanceOfType(caughtEx, HttpMock.StaticResourceNotFoundException.class, 'Exception should be StaticResourceNotFoundException'); + Assert.isNotNull(caughtEx, 'Exception should be thrown'); + Assert.areEqual('Static Resource "' + staticResourceName + '" not found.', caughtEx.getMessage(), 'Message should be Static Resource "' + staticResourceName + '" not found.'); } @IsTest diff --git a/force-app/main/default/staticresources/responseBody.json b/force-app/main/default/staticresources/responseBody.json deleted file mode 100644 index 2f1f4d5..0000000 --- a/force-app/main/default/staticresources/responseBody.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "responseBody", - "order": 1, - "active": true, - "additionalDetails": [ "This is a sample JSON request body for API testing." ] -} \ No newline at end of file diff --git a/force-app/main/default/staticresources/responseBody.resource-meta.xml b/force-app/main/default/staticresources/responseBody.resource-meta.xml deleted file mode 100644 index 1bf29d0..0000000 --- a/force-app/main/default/staticresources/responseBody.resource-meta.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - Private - application/json - A sample response body for testing static resource loading for http mocking - From 932fd2794795077c3611d924084f299a9ae0bad6 Mon Sep 17 00:00:00 2001 From: Piotr PG Gajek Date: Wed, 24 Dec 2025 21:02:35 +0100 Subject: [PATCH 4/8] Requests count --- force-app/main/default/classes/HttpMock.cls | 181 +++++++++--------- .../default/classes/HttpMock.cls-meta.xml | 2 +- .../main/default/classes/HttpMockTest.cls | 51 ++--- .../default/classes/HttpMockTest.cls-meta.xml | 2 +- package-lock.json | 4 +- package.json | 4 +- pmd/ruleset.xml | 16 ++ sfdx-project.json | 14 +- 8 files changed, 141 insertions(+), 133 deletions(-) create mode 100644 pmd/ruleset.xml diff --git a/force-app/main/default/classes/HttpMock.cls b/force-app/main/default/classes/HttpMock.cls index 49f8a75..bbe588a 100644 --- a/force-app/main/default/classes/HttpMock.cls +++ b/force-app/main/default/classes/HttpMock.cls @@ -7,10 +7,9 @@ **/ @IsTest @SuppressWarnings('PMD.ExcessivePublicCount') -public class HttpMock implements HttpMockLib, HttpCalloutMock { +public class HttpMock implements HttpStubbing, HttpCalloutMock { /* new HttpMock - .getGetRequestsCount('/api/v1/authorize'); .whenGetOn('/api/v1/authorize').body('{ "token": "aZ3Xb7Qk" }').contentTypeJson().statusCodeOk() .whenPostOn('/api/v1/create').body('{ "success": true, "message": null }').contentTypeJson().statusCodeOk() .mock(); @@ -19,87 +18,87 @@ public class HttpMock implements HttpMockLib, HttpCalloutMock { // ... Test.stopTest(); - Assert.areEqual(1, HttpMock.getGetRequestsCount('/api/v1/authorize'), 'One GET request should be made'); - Assert.areEqual(1, HttpMock.getPostRequestsCount('/api/v1/create'), 'One POST request should be made'); + Assert.areEqual(1, HttpMock.requestsTo('/api/v1/authorize').get(), 'One GET request should be made'); + Assert.areEqual(1, HttpMock.requestsTo('/api/v1/create').post(), 'One POST request should be made'); */ - public interface HttpMockLib { - HttpMock whenGetOn(String endpointToMock); - HttpMock whenPostOn(String endpointToMock); - HttpMock whenPutOn(String endpointToMock); - HttpMock whenPatchOn(String endpointToMock); - HttpMock whenDeleteOn(String endpointToMock); - HttpMock whenTraceOn(String endpointToMock); - HttpMock whenHeadOn(String endpointToMock); + public interface HttpStubbing { + HttpStubbing whenGetOn(String endpointToMock); + HttpStubbing whenPostOn(String endpointToMock); + HttpStubbing whenPutOn(String endpointToMock); + HttpStubbing whenPatchOn(String endpointToMock); + HttpStubbing whenDeleteOn(String endpointToMock); + HttpStubbing whenTraceOn(String endpointToMock); + HttpStubbing whenHeadOn(String endpointToMock); // Body - HttpMock body(Object body); - HttpMock body(String body); - HttpMock body(Blob body); + HttpStubbing body(Object body); + HttpStubbing body(String body); + HttpStubbing body(Blob body); // Static Resource - HttpMock staticResource(String staticResourceName); + HttpStubbing staticResource(String staticResourceName); // Content-Type - HttpMock contentTypePlainText(); // text/plain - HttpMock contentTypeHtml(); // text/html - HttpMock contentTypeCsv(); // text/csv - HttpMock contentTypeJson(); // application/json - HttpMock contentTypeXml(); // application/xml - HttpMock contentTypePdf(); // application/pdf - HttpMock contentTypeFormUrlencoded(); // application/x-www-form-urlencoded - HttpMock contentType(String contentType); + HttpStubbing contentTypePlainText(); // text/plain + HttpStubbing contentTypeHtml(); // text/html + HttpStubbing contentTypeCsv(); // text/csv + HttpStubbing contentTypeJson(); // application/json + HttpStubbing contentTypeXml(); // application/xml + HttpStubbing contentTypePdf(); // application/pdf + HttpStubbing contentTypeFormUrlencoded(); // application/x-www-form-urlencoded + HttpStubbing contentType(String contentType); // Status Code - HttpMock statusCodeOk(); // 200 - HttpMock statusCodeCreated(); // 201 - HttpMock statusCodeAccepted(); // 202 - HttpMock statusCodeNoContent(); // 204 - HttpMock statusCodeBadRequest(); // 400 - HttpMock statusCodeUnauthorized(); // 401 - HttpMock statusCodeForbidden(); // 403 - HttpMock statusCodeNotFound(); // 404 - HttpMock statusCodeMethodNotAllowed(); // 405 - HttpMock statusCodeInternalServerError(); // 500 - HttpMock statusCodeNotImplemented(); // 501 - HttpMock statusCodeBadGateway(); // 502 - HttpMock statusCodeServiceUnavailable(); // 503 - HttpMock statusCodeGatewayTimeout(); // 504 - HttpMock statusCode(Integer statusCode); + HttpStubbing statusCodeOk(); // 200 + HttpStubbing statusCodeCreated(); // 201 + HttpStubbing statusCodeAccepted(); // 202 + HttpStubbing statusCodeNoContent(); // 204 + HttpStubbing statusCodeBadRequest(); // 400 + HttpStubbing statusCodeUnauthorized(); // 401 + HttpStubbing statusCodeForbidden(); // 403 + HttpStubbing statusCodeNotFound(); // 404 + HttpStubbing statusCodeMethodNotAllowed(); // 405 + HttpStubbing statusCodeInternalServerError(); // 500 + HttpStubbing statusCodeNotImplemented(); // 501 + HttpStubbing statusCodeBadGateway(); // 502 + HttpStubbing statusCodeServiceUnavailable(); // 503 + HttpStubbing statusCodeGatewayTimeout(); // 504 + HttpStubbing statusCode(Integer statusCode); // Headers - HttpMock header(String key, String value); + HttpStubbing header(String key, String value); // Mock void mock(); // Request Counts (static) for assertions - Integer getGetRequestsCount(String endpoint); - Integer getPostRequestsCount(String endpoint); - Integer getPutRequestsCount(String endpoint); - Integer getPatchRequestsCount(String endpoint); - Integer getDeleteRequestsCount(String endpoint); - Integer getTraceRequestsCount(String endpoint); - Integer getHeadRequestsCount(String endpoint); + Integer getRequestsTo(String endpoint); + Integer postRequestsTo(String endpoint); + Integer putRequestsTo(String endpoint); + Integer patchRequestsTo(String endpoint); + Integer deleteRequestsTo(String endpoint); + Integer traceRequestsTo(String endpoint); + Integer headRequestsTo(String endpoint); } - public static Integer getGetRequestsCount(String endpoint) { + public static Integer getRequestsTo(String endpoint) { return getRequestCount('GET', endpoint); } - public static Integer getPostRequestsCount(String endpoint) { + public static Integer postRequestsTo(String endpoint) { return getRequestCount('POST', endpoint); } - public static Integer getPutRequestsCount(String endpoint) { + public static Integer putRequestsTo(String endpoint) { return getRequestCount('PUT', endpoint); } - public static Integer getPatchRequestsCount(String endpoint) { + public static Integer patchRequestsTo(String endpoint) { return getRequestCount('PATCH', endpoint); } - public static Integer getDeleteRequestsCount(String endpoint) { + public static Integer deleteRequestsTo(String endpoint) { return getRequestCount('DELETE', endpoint); } - public static Integer getTraceRequestsCount(String endpoint) { + public static Integer traceRequestsTo(String endpoint) { return getRequestCount('TRACE', endpoint); } - public static Integer getHeadRequestsCount(String endpoint) { + public static Integer headRequestsTo(String endpoint) { return getRequestCount('HEAD', endpoint); } @@ -108,7 +107,7 @@ public class HttpMock implements HttpMockLib, HttpCalloutMock { return 0; } - return requestCountByMethodAndEndpoint.get(httpMethod).get(endpoint) ?? 0; + return requestCountByMethodAndEndpoint?.get(httpMethod)?.get(endpoint) ?? 0; } // Implementation @@ -118,35 +117,35 @@ public class HttpMock implements HttpMockLib, HttpCalloutMock { private HttpResponse workingHttpResponse = null; - public HttpMock whenGetOn(String endpointToMock) { + public HttpStubbing whenGetOn(String endpointToMock) { return this.add('GET', endpointToMock); } - public HttpMock whenPostOn(String endpointToMock) { + public HttpStubbing whenPostOn(String endpointToMock) { return this.add('POST', endpointToMock); } - public HttpMock whenPutOn(String endpointToMock) { + public HttpStubbing whenPutOn(String endpointToMock) { return this.add('PUT', endpointToMock); } - public HttpMock whenPatchOn(String endpointToMock) { + public HttpStubbing whenPatchOn(String endpointToMock) { return this.add('PATCH', endpointToMock); } - public HttpMock whenDeleteOn(String endpointToMock) { + public HttpStubbing whenDeleteOn(String endpointToMock) { return this.add('DELETE', endpointToMock); } - public HttpMock whenTraceOn(String endpointToMock) { + public HttpStubbing whenTraceOn(String endpointToMock) { return this.add('TRACE', endpointToMock); } - public HttpMock whenHeadOn(String endpointToMock) { + public HttpStubbing whenHeadOn(String endpointToMock) { return this.add('HEAD', endpointToMock); } - private HttpMock add(String httpMethod, String endpointToMock) { + private HttpStubbing add(String httpMethod, String endpointToMock) { this.initWhenEmpty(httpMethod, endpointToMock); this.workingHttpResponse = new HttpResponse(); @@ -170,58 +169,58 @@ public class HttpMock implements HttpMockLib, HttpCalloutMock { } } - public HttpMock body(Object body) { + public HttpStubbing body(Object body) { return this.body(JSON.serialize(body)); } - public HttpMock body(String body) { + public HttpStubbing body(String body) { this.workingHttpResponse.setBody(body); return this; } - public HttpMock body(Blob body) { + public HttpStubbing body(Blob body) { this.workingHttpResponse.setBodyAsBlob(body); return this; } - public HttpMock contentTypePlainText() { + public HttpStubbing contentTypePlainText() { return this.contentType('text/plain'); } - public HttpMock contentTypeHtml() { + public HttpStubbing contentTypeHtml() { return this.contentType('text/html'); } - public HttpMock contentTypeCsv() { + public HttpStubbing contentTypeCsv() { return this.contentType('text/csv'); } - public HttpMock contentTypeJson() { + public HttpStubbing contentTypeJson() { return this.contentType('application/json'); } - public HttpMock contentTypePdf() { + public HttpStubbing contentTypePdf() { return this.contentType('application/pdf'); } - public HttpMock contentTypeXml() { + public HttpStubbing contentTypeXml() { return this.contentType('application/xml'); } - public HttpMock contentTypeFormUrlencoded() { + public HttpStubbing contentTypeFormUrlencoded() { return this.contentType('application/x-www-form-urlencoded'); } - public HttpMock contentType(String contentType) { + public HttpStubbing contentType(String contentType) { return this.header('Content-Type', contentType); } - public HttpMock header(String key, String value) { + public HttpStubbing header(String key, String value) { this.workingHttpResponse.setHeader(key, value); return this; } - public HttpMock staticResource(String staticResourceName) { + public HttpStubbing staticResource(String staticResourceName) { StaticResource staticResource = null; try { @@ -233,63 +232,63 @@ public class HttpMock implements HttpMockLib, HttpCalloutMock { return this.body(staticResource.Body); } - public HttpMock statusCodeOk() { + public HttpStubbing statusCodeOk() { return this.statusCode(200); } - public HttpMock statusCodeCreated() { + public HttpStubbing statusCodeCreated() { return this.statusCode(201); } - public HttpMock statusCodeAccepted() { + public HttpStubbing statusCodeAccepted() { return this.statusCode(202); } - public HttpMock statusCodeNoContent() { + public HttpStubbing statusCodeNoContent() { return this.statusCode(204); } - public HttpMock statusCodeBadRequest() { + public HttpStubbing statusCodeBadRequest() { return this.statusCode(400); } - public HttpMock statusCodeUnauthorized() { + public HttpStubbing statusCodeUnauthorized() { return this.statusCode(401); } - public HttpMock statusCodeForbidden() { + public HttpStubbing statusCodeForbidden() { return this.statusCode(403); } - public HttpMock statusCodeNotFound() { + public HttpStubbing statusCodeNotFound() { return this.statusCode(404); } - public HttpMock statusCodeMethodNotAllowed() { + public HttpStubbing statusCodeMethodNotAllowed() { return this.statusCode(405); } - public HttpMock statusCodeInternalServerError() { + public HttpStubbing statusCodeInternalServerError() { return this.statusCode(500); } - public HttpMock statusCodeNotImplemented() { + public HttpStubbing statusCodeNotImplemented() { return this.statusCode(501); } - public HttpMock statusCodeBadGateway() { + public HttpStubbing statusCodeBadGateway() { return this.statusCode(502); } - public HttpMock statusCodeServiceUnavailable() { + public HttpStubbing statusCodeServiceUnavailable() { return this.statusCode(503); } - public HttpMock statusCodeGatewayTimeout() { + public HttpStubbing statusCodeGatewayTimeout() { return this.statusCode(504); } - public HttpMock statusCode(Integer statusCode) { + public HttpStubbing statusCode(Integer statusCode) { this.workingHttpResponse.setStatusCode(statusCode); return this; } diff --git a/force-app/main/default/classes/HttpMock.cls-meta.xml b/force-app/main/default/classes/HttpMock.cls-meta.xml index 53b88f8..594adb8 100644 --- a/force-app/main/default/classes/HttpMock.cls-meta.xml +++ b/force-app/main/default/classes/HttpMock.cls-meta.xml @@ -1,5 +1,5 @@ - 64.0 + 65.0 Active \ No newline at end of file diff --git a/force-app/main/default/classes/HttpMockTest.cls b/force-app/main/default/classes/HttpMockTest.cls index c90085f..648c580 100644 --- a/force-app/main/default/classes/HttpMockTest.cls +++ b/force-app/main/default/classes/HttpMockTest.cls @@ -2,7 +2,7 @@ * Copyright (c) 2025 Beyond The Cloud Sp. z o.o. (BeyondTheCloud.Dev) * Licensed under the MIT License (https://github.com/beyond-the-cloud-dev/http-mock-lib/blob/main/LICENSE) **/ -@IsTest +@IsTest(IsParallel=true) private class HttpMockTest { @IsTest static void get() { @@ -553,19 +553,7 @@ private class HttpMockTest { } @IsTest - static void getRequestCount() { - new HttpMock().whenGetOn('/api/v1').statusCodeOk().mock(); - - Test.startTest(); - new TestApi().makeCallout('GET', '/api/v1'); - new TestApi().makeCallout('GET', '/api/v1'); - Test.stopTest(); - - Assert.areEqual(2, HttpMock.getRequestCount('GET', '/api/v1'), 'Request count should be 2'); - } - - @IsTest - static void getRequestCountWithMultipleEndpoints() { + static void getRequestsToWithMultipleEndpoints() { new HttpMock() .whenGetOn('/api/v1').statusCodeOk() .whenGetOn('/api/v2').statusCodeOk() @@ -576,85 +564,86 @@ private class HttpMockTest { new TestApi().makeCallout('GET', '/api/v2'); Test.stopTest(); - Assert.areEqual(1, HttpMock.getRequestCount('GET', '/api/v1'), 'Request count should be 1'); - Assert.areEqual(1, HttpMock.getRequestCount('GET', '/api/v2'), 'Request count should be 1'); + Assert.areEqual(1, HttpMock.getRequestsTo('/api/v1'), 'Request count should be 1'); + Assert.areEqual(1, HttpMock.getRequestsTo('/api/v2'), 'Request count should be 1'); } @IsTest - static void getGetRequestsCount() { + static void getRequestsTo() { new HttpMock().whenGetOn('/api/v1').statusCodeOk().mock(); Test.startTest(); new TestApi().makeCallout('GET', '/api/v1'); + new TestApi().makeCallout('GET', '/api/v1'); Test.stopTest(); - Assert.areEqual(1, HttpMock.getGetRequestsCount('/api/v1'), 'Request count should be 1'); - } + Assert.areEqual(2, HttpMock.getRequestsTo('/api/v1'), 'Request count should be 2'); + } @IsTest - static void getPostRequestsCount() { + static void postRequestsTo() { new HttpMock().whenPostOn('/api/v1').statusCodeOk().mock(); Test.startTest(); new TestApi().makeCallout('POST', '/api/v1'); Test.stopTest(); - Assert.areEqual(1, HttpMock.getPostRequestsCount('/api/v1'), 'Request count should be 1'); + Assert.areEqual(1, HttpMock.postRequestsTo('/api/v1'), 'Request count should be 1'); } @IsTest - static void getPutRequestsCount() { + static void putRequestsTo() { new HttpMock().whenPutOn('/api/v1').statusCodeOk().mock(); Test.startTest(); new TestApi().makeCallout('PUT', '/api/v1'); Test.stopTest(); - Assert.areEqual(1, HttpMock.getPutRequestsCount('/api/v1'), 'Request count should be 1'); + Assert.areEqual(1, HttpMock.putRequestsTo('/api/v1'), 'Request count should be 1'); } @IsTest - static void getPatchRequestsCount() { + static void patchRequestsTo() { new HttpMock().whenPatchOn('/api/v1').statusCodeOk().mock(); Test.startTest(); new TestApi().makeCallout('PATCH', '/api/v1'); Test.stopTest(); - Assert.areEqual(1, HttpMock.getPatchRequestsCount('/api/v1'), 'Request count should be 1'); + Assert.areEqual(1, HttpMock.patchRequestsTo('/api/v1'), 'Request count should be 1'); } @IsTest - static void getDeleteRequestsCount() { + static void deleteRequestsTo() { new HttpMock().whenDeleteOn('/api/v1').statusCodeOk().mock(); Test.startTest(); new TestApi().makeCallout('DELETE', '/api/v1'); Test.stopTest(); - Assert.areEqual(1, HttpMock.getDeleteRequestsCount('/api/v1'), 'Request count should be 1'); + Assert.areEqual(1, HttpMock.deleteRequestsTo('/api/v1'), 'Request count should be 1'); } @IsTest - static void getTraceRequestsCount() { + static void traceRequestsTo() { new HttpMock().whenTraceOn('/api/v1').statusCodeOk().mock(); Test.startTest(); new TestApi().makeCallout('TRACE', '/api/v1'); Test.stopTest(); - Assert.areEqual(1, HttpMock.getTraceRequestsCount('/api/v1'), 'Request count should be 1'); + Assert.areEqual(1, HttpMock.traceRequestsTo('/api/v1'), 'Request count should be 1'); } @IsTest - static void getHeadRequestsCount() { + static void headRequestsTo() { new HttpMock().whenHeadOn('/api/v1').statusCodeOk().mock(); Test.startTest(); new TestApi().makeCallout('HEAD', '/api/v1'); Test.stopTest(); - Assert.areEqual(1, HttpMock.getHeadRequestsCount('/api/v1'), 'Request count should be 1'); + Assert.areEqual(1, HttpMock.headRequestsTo('/api/v1'), 'Request count should be 1'); } @IsTest diff --git a/force-app/main/default/classes/HttpMockTest.cls-meta.xml b/force-app/main/default/classes/HttpMockTest.cls-meta.xml index 53b88f8..594adb8 100644 --- a/force-app/main/default/classes/HttpMockTest.cls-meta.xml +++ b/force-app/main/default/classes/HttpMockTest.cls-meta.xml @@ -1,5 +1,5 @@ - 64.0 + 65.0 Active \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 88b946d..1d37c84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "salesforce-repo", + "name": "http-mock-lib", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "salesforce-repo", + "name": "http-mock-lib", "version": "1.0.0", "devDependencies": { "@lwc/eslint-plugin-lwc": "^3.3.0", diff --git a/package.json b/package.json index c4052d4..7e2daaf 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { - "name": "salesforce-repo", + "name": "http-mock-lib", "private": true, "version": "1.0.0", - "description": "Salesforce Repo", + "description": "HTTP Mock Lib", "scripts": { "lint": "eslint **/{aura,lwc}/**", "test": "npm run test:unit", diff --git a/pmd/ruleset.xml b/pmd/ruleset.xml new file mode 100644 index 0000000..9fceb64 --- /dev/null +++ b/pmd/ruleset.xml @@ -0,0 +1,16 @@ + + + Apex PMD ruleset for DML Lib. + + + + + + + + + + + + + \ No newline at end of file diff --git a/sfdx-project.json b/sfdx-project.json index 15bed54..e50573e 100644 --- a/sfdx-project.json +++ b/sfdx-project.json @@ -1,12 +1,16 @@ { "packageDirectories": [ { + "versionName": "HTTP Mock Lib 1.0.0", + "versionNumber": "1.0.0.NEXT", "path": "force-app", - "default": true + "default": true, + "package": "HTTP Mock Lib", + "versionDescription": "" } ], - "name": "BeyondTheCloud", - "namespace": "", + "name": "http-mock-lib", + "namespace": "btcdev", "sfdcLoginUrl": "https://login.salesforce.com", - "sourceApiVersion": "57.0" -} + "sourceApiVersion": "65.0" +} \ No newline at end of file From b3d1b577a0bde988e2658b2aae418bd940e5ea0a Mon Sep 17 00:00:00 2001 From: Piotr PG Gajek Date: Fri, 26 Dec 2025 21:30:49 +0100 Subject: [PATCH 5/8] unlocked package --- force-app/main/default/classes/HttpMock.cls | 141 +++++++++++------- .../main/default/classes/HttpMockTest.cls | 55 +++++-- sfdx-project.json | 11 +- website/installation.md | 121 +++------------ 4 files changed, 164 insertions(+), 164 deletions(-) diff --git a/force-app/main/default/classes/HttpMock.cls b/force-app/main/default/classes/HttpMock.cls index bbe588a..274a0f1 100644 --- a/force-app/main/default/classes/HttpMock.cls +++ b/force-app/main/default/classes/HttpMock.cls @@ -4,14 +4,23 @@ * * PMD False Positives: * - ExcessivePublicCount: It is a library class and exposes all necessary methods to construct a http mock. + * - FieldDeclarationsShouldBeAtStart: It is a library class and important methods are on the top. + * - CognitiveComplexity: It is a library class and the code is complex but it is necessary to keep it this way. + * - CyclomaticComplexity: It is a library class and the code is complex but it is necessary to keep it this way. **/ +@SuppressWarnings('PMD.ExcessivePublicCount,PMD.FieldDeclarationsShouldBeAtStart,PMD.CognitiveComplexity,PMD.CyclomaticComplexity') @IsTest -@SuppressWarnings('PMD.ExcessivePublicCount') -public class HttpMock implements HttpStubbing, HttpCalloutMock { +global class HttpMock implements HttpStubbing, HttpCalloutMock { /* new HttpMock - .whenGetOn('/api/v1/authorize').body('{ "token": "aZ3Xb7Qk" }').contentTypeJson().statusCodeOk() - .whenPostOn('/api/v1/create').body('{ "success": true, "message": null }').contentTypeJson().statusCodeOk() + .whenGetOn('/api/v1/authorize') + .body('{ "token": "aZ3Xb7Qk" }') + .contentTypeJson() + .statusCodeOk() + .whenPostOn('/api/v1/create') + .body('{ "success": true, "message": null }') + .contentTypeJson() + .statusCodeOk() .mock(); Test.startTest(); @@ -21,7 +30,7 @@ public class HttpMock implements HttpStubbing, HttpCalloutMock { Assert.areEqual(1, HttpMock.requestsTo('/api/v1/authorize').get(), 'One GET request should be made'); Assert.areEqual(1, HttpMock.requestsTo('/api/v1/create').post(), 'One POST request should be made'); */ - public interface HttpStubbing { + global interface HttpStubbing { HttpStubbing whenGetOn(String endpointToMock); HttpStubbing whenPostOn(String endpointToMock); HttpStubbing whenPutOn(String endpointToMock); @@ -64,59 +73,32 @@ public class HttpMock implements HttpStubbing, HttpCalloutMock { HttpStubbing header(String key, String value); // Mock void mock(); - // Request Counts (static) for assertions - Integer getRequestsTo(String endpoint); - Integer postRequestsTo(String endpoint); - Integer putRequestsTo(String endpoint); - Integer patchRequestsTo(String endpoint); - Integer deleteRequestsTo(String endpoint); - Integer traceRequestsTo(String endpoint); - Integer headRequestsTo(String endpoint); } - public static Integer getRequestsTo(String endpoint) { - return getRequestCount('GET', endpoint); + // Request Counts for assertions + global static Requests requestsTo(String endpoint) { + return HttpMock.requestsByEndpoint.get(endpoint) ?? new HttpMockRequests(); } - public static Integer postRequestsTo(String endpoint) { - return getRequestCount('POST', endpoint); - } - - public static Integer putRequestsTo(String endpoint) { - return getRequestCount('PUT', endpoint); - } - - public static Integer patchRequestsTo(String endpoint) { - return getRequestCount('PATCH', endpoint); - } - - public static Integer deleteRequestsTo(String endpoint) { - return getRequestCount('DELETE', endpoint); - } - - public static Integer traceRequestsTo(String endpoint) { - return getRequestCount('TRACE', endpoint); - } - - public static Integer headRequestsTo(String endpoint) { - return getRequestCount('HEAD', endpoint); - } - - public static Integer getRequestCount(String httpMethod, String endpoint) { - if (!requestCountByMethodAndEndpoint.containsKey(httpMethod)) { - return 0; - } + global interface Requests { + Integer all(); - return requestCountByMethodAndEndpoint?.get(httpMethod)?.get(endpoint) ?? 0; + Integer get(); + Integer post(); + Integer put(); + Integer patch(); + Integer deletex(); // delete is a reserved keyword + Integer trace(); + Integer head(); } // Implementation private static Map>> mocks = new Map>>(); - private static Map> requestCountByMethodAndEndpoint = new Map>(); + private static Map requestsByEndpoint = new Map(); private HttpResponse workingHttpResponse = null; - + public HttpStubbing whenGetOn(String endpointToMock) { return this.add('GET', endpointToMock); } @@ -298,7 +280,7 @@ public class HttpMock implements HttpStubbing, HttpCalloutMock { } public HttpResponse respond(HttpRequest request) { - String closestMatchingMockedEndpoint = this.findClosestMatchingMockedEndpoint(request); + String closestMatchingMockedEndpoint = this.findClosestMatchingMockedEndpointBasedOnRequest(request); String requestEndpoint = request.getEndpoint(); @@ -314,7 +296,7 @@ public class HttpMock implements HttpStubbing, HttpCalloutMock { throw new HttpEndpointNotMockedException('HTTP Endpoint ' + requestMethod + ' ' + requestEndpoint + ' hasn\'t been mocked.'); } - this.incrementRequestCount(requestMethod, closestMatchingMockedEndpoint); + this.registerRequest(requestMethod, requestEndpoint); if (mockedHttpResponses.size() > 1) { return mockedHttpResponses.remove(0); @@ -323,14 +305,22 @@ public class HttpMock implements HttpStubbing, HttpCalloutMock { return mockedHttpResponses.get(0); } - private String findClosestMatchingMockedEndpoint(HttpRequest httpRequest) { + private void registerRequest(String httpMethod, String endpoint) { + if (!requestsByEndpoint.containsKey(endpoint)) { + requestsByEndpoint.put(endpoint, new HttpMockRequests()); + } + + requestsByEndpoint.get(endpoint).incrementRequestCount(httpMethod); + } + + private String findClosestMatchingMockedEndpointBasedOnRequest(HttpRequest httpRequest) { String httpRequestMethod = httpRequest.getMethod(); + String httpRequestEndpoint = httpRequest.getEndpoint(); if (!mocks.containsKey(httpRequestMethod)) { throw new HttpMethodNotMockedException('HTTP Method ' + httpRequestMethod + ' hasn\'t been mocked.'); } - String httpRequestEndpoint = httpRequest.getEndpoint(); return this.findClosestMatchingMockedEndpoint(httpRequestEndpoint, mocks.get(httpRequestMethod).keySet()); } @@ -348,13 +338,54 @@ public class HttpMock implements HttpStubbing, HttpCalloutMock { return closestMatchingMockedEndpoint; } - private void incrementRequestCount(String httpMethod, String endpoint) { - if (!requestCountByMethodAndEndpoint.containsKey(httpMethod)) { - requestCountByMethodAndEndpoint.put(httpMethod, new Map()); + private class HttpMockRequests implements Requests { + private Map requestCountByMethod = new Map(); + + public void incrementRequestCount(String httpMethod) { + if (!this.requestCountByMethod.containsKey(httpMethod)) { + this.requestCountByMethod.put(httpMethod, 0); + } + + this.requestCountByMethod.put(httpMethod, this.requestCountByMethod.get(httpMethod) + 1); + } + + public Integer all() { + Integer total = 0; + + for (Integer count : this.requestCountByMethod.values()) { + total += count; + } + + return total; + } + + public Integer get() { + return this.requestCountByMethod.get('GET') ?? 0; + } + + public Integer post() { + return this.requestCountByMethod.get('POST') ?? 0; + } + + public Integer put() { + return this.requestCountByMethod.get('PUT') ?? 0; + } + + public Integer patch() { + return this.requestCountByMethod.get('PATCH') ?? 0; + } + + public Integer deletex() { + return this.requestCountByMethod.get('DELETE') ?? 0; } - Integer currentCount = requestCountByMethodAndEndpoint.get(httpMethod).get(endpoint) ?? 0; - requestCountByMethodAndEndpoint.get(httpMethod).put(endpoint, currentCount + 1); + public Integer trace() { + return this.requestCountByMethod.get('TRACE') ?? 0; + } + + public Integer head() { + return this.requestCountByMethod.get('HEAD') ?? 0; + } } public class HttpMethodNotMockedException extends Exception {} diff --git a/force-app/main/default/classes/HttpMockTest.cls b/force-app/main/default/classes/HttpMockTest.cls index 648c580..5111d5f 100644 --- a/force-app/main/default/classes/HttpMockTest.cls +++ b/force-app/main/default/classes/HttpMockTest.cls @@ -1,7 +1,12 @@ /** * Copyright (c) 2025 Beyond The Cloud Sp. z o.o. (BeyondTheCloud.Dev) * Licensed under the MIT License (https://github.com/beyond-the-cloud-dev/http-mock-lib/blob/main/LICENSE) + * + * PMD False Positives: + * - CognitiveComplexity: It is a test class and the code is complex but it is necessary to keep it this way. + * - CyclomaticComplexity: It is a test class and the code is complex but it is necessary to keep it this way. **/ +@SuppressWarnings('PMD.ApexUnitTestClassShouldHaveRunAs,PMD.CognitiveComplexity,PMD.CyclomaticComplexity') @IsTest(IsParallel=true) private class HttpMockTest { @IsTest @@ -564,8 +569,40 @@ private class HttpMockTest { new TestApi().makeCallout('GET', '/api/v2'); Test.stopTest(); - Assert.areEqual(1, HttpMock.getRequestsTo('/api/v1'), 'Request count should be 1'); - Assert.areEqual(1, HttpMock.getRequestsTo('/api/v2'), 'Request count should be 1'); + Assert.areEqual(1, HttpMock.requestsTo('/api/v1').get(), 'Request count should be 1'); + Assert.areEqual(1, HttpMock.requestsTo('/api/v2').get(), 'Request count should be 1'); + } + + @IsTest + static void allRequestsTo() { + new HttpMock() + .whenGetOn('/api/v1').statusCodeOk() + .whenPostOn('/api/v1').statusCodeOk() + .whenPutOn('/api/v1').statusCodeOk() + .whenPatchOn('/api/v1').statusCodeOk() + .whenDeleteOn('/api/v1').statusCodeOk() + .whenTraceOn('/api/v1').statusCodeOk() + .whenHeadOn('/api/v1').statusCodeOk() + .mock(); + + Test.startTest(); + new TestApi().makeCallout('GET', '/api/v1'); + new TestApi().makeCallout('POST', '/api/v1'); + new TestApi().makeCallout('PUT', '/api/v1'); + new TestApi().makeCallout('PATCH', '/api/v1'); + new TestApi().makeCallout('DELETE', '/api/v1'); + new TestApi().makeCallout('TRACE', '/api/v1'); + new TestApi().makeCallout('HEAD', '/api/v1'); + Test.stopTest(); + + Assert.areEqual(7, HttpMock.requestsTo('/api/v1').all(), 'Request count should be 7'); + Assert.areEqual(1, HttpMock.requestsTo('/api/v1').get(), 'Request count should be 1'); + Assert.areEqual(1, HttpMock.requestsTo('/api/v1').post(), 'Request count should be 1'); + Assert.areEqual(1, HttpMock.requestsTo('/api/v1').put(), 'Request count should be 1'); + Assert.areEqual(1, HttpMock.requestsTo('/api/v1').patch(), 'Request count should be 1'); + Assert.areEqual(1, HttpMock.requestsTo('/api/v1').deletex(), 'Request count should be 1'); + Assert.areEqual(1, HttpMock.requestsTo('/api/v1').trace(), 'Request count should be 1'); + Assert.areEqual(1, HttpMock.requestsTo('/api/v1').head(), 'Request count should be 1'); } @IsTest @@ -577,7 +614,7 @@ private class HttpMockTest { new TestApi().makeCallout('GET', '/api/v1'); Test.stopTest(); - Assert.areEqual(2, HttpMock.getRequestsTo('/api/v1'), 'Request count should be 2'); + Assert.areEqual(2, HttpMock.requestsTo('/api/v1').get(), 'Request count should be 2'); } @IsTest @@ -588,7 +625,7 @@ private class HttpMockTest { new TestApi().makeCallout('POST', '/api/v1'); Test.stopTest(); - Assert.areEqual(1, HttpMock.postRequestsTo('/api/v1'), 'Request count should be 1'); + Assert.areEqual(1, HttpMock.requestsTo('/api/v1').post(), 'Request count should be 1'); } @IsTest @@ -599,7 +636,7 @@ private class HttpMockTest { new TestApi().makeCallout('PUT', '/api/v1'); Test.stopTest(); - Assert.areEqual(1, HttpMock.putRequestsTo('/api/v1'), 'Request count should be 1'); + Assert.areEqual(1, HttpMock.requestsTo('/api/v1').put(), 'Request count should be 1'); } @IsTest @@ -610,7 +647,7 @@ private class HttpMockTest { new TestApi().makeCallout('PATCH', '/api/v1'); Test.stopTest(); - Assert.areEqual(1, HttpMock.patchRequestsTo('/api/v1'), 'Request count should be 1'); + Assert.areEqual(1, HttpMock.requestsTo('/api/v1').patch(), 'Request count should be 1'); } @IsTest @@ -621,7 +658,7 @@ private class HttpMockTest { new TestApi().makeCallout('DELETE', '/api/v1'); Test.stopTest(); - Assert.areEqual(1, HttpMock.deleteRequestsTo('/api/v1'), 'Request count should be 1'); + Assert.areEqual(1, HttpMock.requestsTo('/api/v1').deletex(), 'Request count should be 1'); } @IsTest @@ -632,7 +669,7 @@ private class HttpMockTest { new TestApi().makeCallout('TRACE', '/api/v1'); Test.stopTest(); - Assert.areEqual(1, HttpMock.traceRequestsTo('/api/v1'), 'Request count should be 1'); + Assert.areEqual(1, HttpMock.requestsTo('/api/v1').trace(), 'Request count should be 1'); } @IsTest @@ -643,7 +680,7 @@ private class HttpMockTest { new TestApi().makeCallout('HEAD', '/api/v1'); Test.stopTest(); - Assert.areEqual(1, HttpMock.headRequestsTo('/api/v1'), 'Request count should be 1'); + Assert.areEqual(1, HttpMock.requestsTo('/api/v1').head(), 'Request count should be 1'); } @IsTest diff --git a/sfdx-project.json b/sfdx-project.json index e50573e..7cc45f2 100644 --- a/sfdx-project.json +++ b/sfdx-project.json @@ -1,8 +1,8 @@ { "packageDirectories": [ { - "versionName": "HTTP Mock Lib 1.0.0", - "versionNumber": "1.0.0.NEXT", + "versionName": "ver 1.3.0", + "versionNumber": "1.3.0.NEXT", "path": "force-app", "default": true, "package": "HTTP Mock Lib", @@ -12,5 +12,10 @@ "name": "http-mock-lib", "namespace": "btcdev", "sfdcLoginUrl": "https://login.salesforce.com", - "sourceApiVersion": "65.0" + "sourceApiVersion": "65.0", + "packageAliases": { + "HTTP Mock Lib": "0HoP600000001KHKAY", + "HTTP Mock Lib@1.1.0-1": "04tP6000002EJ3FIAW", + "HTTP Mock Lib@1.2.0-1": "04tP6000002EJBJIA4" + } } \ No newline at end of file diff --git a/website/installation.md b/website/installation.md index d980d9b..3cf7b11 100644 --- a/website/installation.md +++ b/website/installation.md @@ -1,116 +1,43 @@ # Installation -This guide covers the different ways to install HTTP Mock Lib in your Salesforce org. +## Install via Unlocked Package -## Using Salesforce CLI + -### 1. Install as an Unlocked Package +Install the HTTP Mock Lib unlocked package with `btcdev` namespace to your Salesforce environment: -```bash -sf package install --package 0HoRG00000000XXXXXXX --target-org your-org-alias --wait 10 -``` +`/packaging/installPackage.apexp?p0=04tP6000002EJBJIA4` -::: tip -Replace `your-org-alias` with your org alias or username. -::: +[Install on Sandbox](https://test.salesforce.com/packaging/installPackage.apexp?p0=04tP6000002EJBJIA4) -### 2. Deploy Source Code +[Install on Production](https://login.salesforce.com/packaging/installPackage.apexp?p0=04tP6000002EJBJIA4) -Clone the repository and deploy directly: + -1. Download the source code from [GitHub](https://github.com/beyond-the-cloud-dev/http-mock-lib) -2. Navigate to [Workbench](https://workbench.developerforce.com/) -3. Login to your org -4. Go to **Migration** → **Deploy** -5. Select the `force-app` folder and deploy +## Deploy via Button -### Using Setup UI +Click the button below to deploy HTTP Mock Lib to your environment. -1. Copy the class code from [HttpMock.cls](https://github.com/beyond-the-cloud-dev/http-mock-lib/blob/main/force-app/main/default/classes/HttpMock.cls) -2. In your Salesforce org, go to **Setup** → **Apex Classes** -3. Click **New** -4. Paste the code and click **Save** + + Deploy to Salesforce + -## Dependencies +## Copy and Deploy -HTTP Mock Lib has **zero dependencies**. It's a standalone library that works out of the box. +**Apex** -## API Version - -HTTP Mock Lib requires Salesforce API version **64.0** or higher. - -## Verification - -To verify the installation, create a simple test: - -```apex -@IsTest -private class HttpMockVerificationTest { - @IsTest - static void verifyInstallation() { - new HttpMock() - .whenGetOn('/test') - .body('{"test": true}') - .statusCodeOk() - .mock(); - - // If this compiles and runs, installation is successful - Assert.isTrue(true); - } -} -``` - -Run the test: - -```bash -sf apex run test --class-names HttpMockVerificationTest --target-org your-org-alias -``` - -If the test passes, HTTP Mock Lib is installed correctly! ✅ - -## Next Steps - -Now that you have HTTP Mock Lib installed, check out: - -- [Getting Started Guide](/getting-started) -- [API Reference](/api/) -- [Examples](/examples/basic) - -## Troubleshooting - -### API Version Error - -**Error:** `Invalid API version specified` - -**Solution:** Ensure your org supports API version 64.0 or higher. You can check this in Setup → API. - -### Deployment Fails - -**Error:** `Deployment failed` - -**Solution:** -- Verify you have sufficient permissions -- Check that your org is not at maximum capacity for Apex classes -- Review any error messages for specific issues - -### Need Help? - -If you encounter any issues: -- Check [GitHub Issues](https://github.com/beyond-the-cloud-dev/http-mock-lib/issues) -- Create a new issue if your problem isn't listed -- Contact [Beyond the Cloud](https://beyondthecloud.dev) +- [`HttpMock.cls`](https://github.com/beyond-the-cloud-dev/http-mock-lib/blob/main/force-app/main/default/classes/HttpMock.cls) +- [`HttpMockTest.cls`](https://github.com/beyond-the-cloud-dev/http-mock-lib/blob/main/force-app/main/default/classes/HttpMockTest.cls) From 58f7bf780286b7c19025183b9b130078d2712864 Mon Sep 17 00:00:00 2001 From: Piotr PG Gajek Date: Fri, 26 Dec 2025 22:01:05 +0100 Subject: [PATCH 6/8] documentation update --- website/.vitepress/config.mts | 23 +- website/api/content-types.md | 245 +++------------- website/api/headers.md | 203 ++----------- website/api/http-methods.md | 109 ++----- website/api/index.md | 14 - website/api/response-body.md | 197 ++----------- website/api/status-codes.md | 209 ++++---------- website/examples/basic.md | 262 ----------------- website/examples/custom-headers.md | 347 ---------------------- website/examples/error-handling.md | 384 ------------------------- website/examples/multiple-endpoints.md | 277 ------------------ website/index.md | 65 +---- 12 files changed, 174 insertions(+), 2161 deletions(-) delete mode 100644 website/examples/basic.md delete mode 100644 website/examples/custom-headers.md delete mode 100644 website/examples/error-handling.md delete mode 100644 website/examples/multiple-endpoints.md diff --git a/website/.vitepress/config.mts b/website/.vitepress/config.mts index cc01c80..a163646 100644 --- a/website/.vitepress/config.mts +++ b/website/.vitepress/config.mts @@ -9,8 +9,7 @@ export default defineConfig({ nav: [ { text: 'Home', link: '/' }, - { text: 'Guide', link: '/getting-started' }, - { text: 'API', link: '/api/' } + { text: 'Documentation', link: '/api/' } ], sidebar: [ @@ -31,27 +30,19 @@ export default defineConfig({ { text: 'Status Codes', link: '/api/status-codes' }, { text: 'Headers', link: '/api/headers' } ] - }, - { - text: 'Examples', - items: [ - { text: 'Basic Usage', link: '/examples/basic' }, - { text: 'Multiple Endpoints', link: '/examples/multiple-endpoints' }, - { text: 'Custom Headers', link: '/examples/custom-headers' }, - { text: 'Error Handling', link: '/examples/error-handling' } - ] } ], - socialLinks: [ - { icon: 'github', link: 'https://github.com/beyond-the-cloud-dev/http-mock-lib' } + { icon: 'github', link: 'https://github.com/beyond-the-cloud-dev/http-mock-lib' }, + { + icon: 'linkedin', + link: 'https://www.linkedin.com/company/beyondtheclouddev' + } ], - footer: { message: 'Part of Apex Fluently', - copyright: 'Copyright © 2024 Beyond the Cloud' + copyright: 'Copyright © 2025 Beyond the Cloud' }, - search: { provider: 'local' } diff --git a/website/api/content-types.md b/website/api/content-types.md index 636e689..b63cabf 100644 --- a/website/api/content-types.md +++ b/website/api/content-types.md @@ -2,17 +2,17 @@ Set the Content-Type header for your mocked responses. -## Built-in Content Types - -HTTP Mock Lib provides semantic methods for common content types. - -### contentTypeJson() - ```apex -HttpMock contentTypeJson() // application/json +new HttpMock() + .whenGetOn('/api/users') + .body('{"users": []}') + .contentTypeJson() + .mock(); ``` -Default content type. Used for JSON responses. +## JSON + +`application/json` - Default content type. ```apex new HttpMock() @@ -22,13 +22,9 @@ new HttpMock() .mock(); ``` -### contentTypePlainText() +## Plain Text -```apex -HttpMock contentTypePlainText() // text/plain -``` - -Plain text responses. +`text/plain` ```apex new HttpMock() @@ -38,13 +34,9 @@ new HttpMock() .mock(); ``` -### contentTypeHtml() +## HTML -```apex -HttpMock contentTypeHtml() // text/html -``` - -HTML responses. +`text/html` ```apex new HttpMock() @@ -54,30 +46,21 @@ new HttpMock() .mock(); ``` -### contentTypeCsv() - -```apex -HttpMock contentTypeCsv() // text/csv -``` +## CSV -CSV data responses. +`text/csv` ```apex new HttpMock() .whenGetOn('/api/export/users.csv') - .body('id,name,email\n1,John,john@example.com\n2,Jane,jane@example.com') + .body('id,name,email\n1,John,john@example.com') .contentTypeCsv() - .header('Content-Disposition', 'attachment; filename="users.csv"') .mock(); ``` -### contentTypeXml() - -```apex -HttpMock contentTypeXml() // application/xml -``` +## XML -XML responses. +`application/xml` ```apex new HttpMock() @@ -87,13 +70,9 @@ new HttpMock() .mock(); ``` -### contentTypePdf() +## PDF -```apex -HttpMock contentTypePdf() // application/pdf -``` - -PDF document responses. +`application/pdf` ```apex Blob pdfData = generatePdfContent(); @@ -105,13 +84,9 @@ new HttpMock() .mock(); ``` -### contentTypeFormUrlencoded() - -```apex -HttpMock contentTypeFormUrlencoded() // application/x-www-form-urlencoded -``` +## Form URL Encoded -Form-encoded data. +`application/x-www-form-urlencoded` ```apex new HttpMock() @@ -121,177 +96,27 @@ new HttpMock() .mock(); ``` -## Custom Content Type +## Custom -For content types not covered by built-in methods: +For content types not covered by built-in methods. ```apex -HttpMock contentType(String contentType) -``` - -**Examples:** - -```apex -// YAML new HttpMock() .whenGetOn('/api/config') - .body('key: value\nlist:\n - item1\n - item2') + .body('key: value') .contentType('application/x-yaml') .mock(); - -// Protocol Buffers -new HttpMock() - .whenGetOn('/api/data') - .body(protobufBlob) - .contentType('application/protobuf') - .mock(); - -// Custom vendor type -new HttpMock() - .whenGetOn('/api/custom') - .body('{"data": {}}') - .contentType('application/vnd.mycompany.v1+json') - .mock(); -``` - -## Default Content Type - -If not specified, HTTP Mock Lib uses `application/json`: - -```apex -// These are equivalent: -new HttpMock() - .whenGetOn('/api/users') - .body('{"users": []}') - .mock(); - -new HttpMock() - .whenGetOn('/api/users') - .body('{"users": []}') - .contentTypeJson() // Explicitly set - .mock(); -``` - -## Content Type Reference - -| Content Type | Method | Usage | -|-------------|--------|-------| -| `application/json` | `contentTypeJson()` | JSON data (default) | -| `text/plain` | `contentTypePlainText()` | Plain text | -| `text/html` | `contentTypeHtml()` | HTML documents | -| `text/csv` | `contentTypeCsv()` | CSV data | -| `application/xml` | `contentTypeXml()` | XML data | -| `application/pdf` | `contentTypePdf()` | PDF documents | -| `application/x-www-form-urlencoded` | `contentTypeFormUrlencoded()` | Form data | -| Custom | `contentType(String)` | Any MIME type | - -## Examples - -### JSON API Response - -```apex -new HttpMock() - .whenGetOn('/api/v1/users') - .body('{"users": [], "total": 0}') - .contentTypeJson() - .statusCodeOk() - .mock(); -``` - -### CSV Export - -```apex -String csvData = 'Name,Email,Status\n' + - 'John Doe,john@example.com,Active\n' + - 'Jane Smith,jane@example.com,Active'; - -new HttpMock() - .whenGetOn('/api/export') - .body(csvData) - .contentTypeCsv() - .header('Content-Disposition', 'attachment; filename="export.csv"') - .statusCodeOk() - .mock(); -``` - -### XML SOAP Response - -```apex -String soapResponse = - '' + - '' + - '' + - 'Success' + - '' + - ''; - -new HttpMock() - .whenPostOn('/soap/endpoint') - .body(soapResponse) - .contentType('application/soap+xml') - .statusCodeOk() - .mock(); -``` - -### Binary File Download - -```apex -Blob fileContent = Blob.valueOf('File content'); - -new HttpMock() - .whenGetOn('/api/download/document.pdf') - .body(fileContent) - .contentTypePdf() - .header('Content-Length', String.valueOf(fileContent.size())) - .header('Content-Disposition', 'attachment; filename="document.pdf"') - .statusCodeOk() - .mock(); -``` - -## Best Practices - -1. **Match Real APIs** - Use the same content type as the actual API - -2. **Set Explicitly** - Even though JSON is default, explicitly set content type for clarity - -3. **Use with Headers** - Combine with other headers like `Content-Disposition` for downloads - -4. **Validate Format** - Ensure your body format matches the content type - -## Common Patterns - -### API with Multiple Formats - -```apex -// JSON endpoint -new HttpMock() - .whenGetOn('/api/data?format=json') - .body('{"data": []}') - .contentTypeJson() - .mock(); - -// XML endpoint -new HttpMock() - .whenGetOn('/api/data?format=xml') - .body('') - .contentTypeXml() - .mock(); -``` - -### File Upload Response - -```apex -new HttpMock() - .whenPostOn('/api/upload') - .body('{"fileId": "abc123", "url": "https://cdn.example.com/abc123"}') - .contentTypeJson() - .statusCodeCreated() - .header('Location', '/api/files/abc123') - .mock(); ``` -## See Also +## Reference -- [Response Body →](/api/response-body) -- [Headers →](/api/headers) -- [Examples →](/examples/basic) +| Content Type | Method | +|-------------|--------| +| `application/json` | `contentTypeJson()` | +| `text/plain` | `contentTypePlainText()` | +| `text/html` | `contentTypeHtml()` | +| `text/csv` | `contentTypeCsv()` | +| `application/xml` | `contentTypeXml()` | +| `application/pdf` | `contentTypePdf()` | +| `application/x-www-form-urlencoded` | `contentTypeFormUrlencoded()` | +| Custom | `contentType(String)` | diff --git a/website/api/headers.md b/website/api/headers.md index 84e26bd..1131d6d 100644 --- a/website/api/headers.md +++ b/website/api/headers.md @@ -2,30 +2,18 @@ Add custom HTTP headers to your mocked responses. -## API - -### header() - -Add a custom header to the response. - -```apex -HttpMock header(String key, String value) -``` - -**Example:** ```apex new HttpMock() .whenGetOn('/api/users') .body('{"users": []}') .header('X-Total-Count', '42') .header('X-Page-Number', '1') - .statusCodeOk() .mock(); ``` ## Multiple Headers -You can chain multiple `.header()` calls to add multiple headers: +Chain multiple `.header()` calls. ```apex new HttpMock() @@ -34,14 +22,10 @@ new HttpMock() .header('Cache-Control', 'no-cache') .header('X-Request-ID', 'abc-123-def') .header('X-API-Version', 'v1') - .header('X-RateLimit-Remaining', '99') - .statusCodeOk() .mock(); ``` -## Common Headers - -### Cache Control +## Cache Control ```apex new HttpMock() @@ -49,39 +33,23 @@ new HttpMock() .body('{"data": "cached"}') .header('Cache-Control', 'max-age=3600') .header('ETag', '"abc123"') - .statusCodeOk() .mock(); ``` -### Content Disposition +## Content Disposition -For file downloads: +For file downloads. ```apex new HttpMock() .whenGetOn('/api/report.pdf') .body(pdfBlob) .contentTypePdf() - .header('Content-Disposition', 'attachment; filename="monthly-report.pdf"') - .header('Content-Length', String.valueOf(pdfBlob.size())) - .statusCodeOk() - .mock(); -``` - -### CORS Headers - -```apex -new HttpMock() - .whenGetOn('/api/public/data') - .body('{"data": []}') - .header('Access-Control-Allow-Origin', '*') - .header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE') - .header('Access-Control-Allow-Headers', 'Content-Type, Authorization') - .statusCodeOk() + .header('Content-Disposition', 'attachment; filename="report.pdf"') .mock(); ``` -### Rate Limiting +## Rate Limiting ```apex new HttpMock() @@ -90,11 +58,10 @@ new HttpMock() .header('X-RateLimit-Limit', '100') .header('X-RateLimit-Remaining', '95') .header('X-RateLimit-Reset', '1640000000') - .statusCodeOk() .mock(); ``` -### Authentication +## Authentication ```apex new HttpMock() @@ -102,26 +69,12 @@ new HttpMock() .body('{"token": "xyz789"}') .header('Set-Cookie', 'session=abc123; HttpOnly; Secure') .header('X-Auth-Token', 'xyz789') - .statusCodeOk() - .mock(); -``` - -### Custom API Headers - -```apex -new HttpMock() - .whenGetOn('/api/v2/data') - .body('{"data": []}') - .header('X-API-Version', 'v2.1.0') - .header('X-Server-ID', 'server-42') - .header('X-Request-Duration', '127ms') - .statusCodeOk() .mock(); ``` -### Location Header +## Location -For redirects and created resources: +For redirects and created resources. ```apex new HttpMock() @@ -132,131 +85,15 @@ new HttpMock() .mock(); ``` -## Default Headers - -HTTP Mock Lib automatically sets: -- `Content-Type` (based on your content type method, default: `application/json`) - -You can override the Content-Type using `.header()`: - -```apex -new HttpMock() - .whenGetOn('/api/custom') - .body('{"data": {}}') - .header('Content-Type', 'application/vnd.api+json') // Override - .statusCodeOk() - .mock(); -``` - -## Testing Headers - -Test that your code properly handles response headers: - -```apex -@IsTest -static void testRateLimitHeaders() { - // Arrange - new HttpMock() - .whenGetOn('/api/data') - .body('{"data": []}') - .header('X-RateLimit-Remaining', '0') - .statusCodeOk() - .mock(); - - // Act - Test.startTest(); - ApiService service = new ApiService(); - service.getData(); - Test.stopTest(); - - // Assert - Assert.isTrue(service.rateLimitReached); -} -``` - -## Best Practices - -1. **Use Standard Headers** - Prefer standard HTTP headers (Cache-Control, Content-Type, etc.) - -2. **Match Real API** - Include the same headers that the real API returns - -3. **Test Header Handling** - Verify your code properly processes important headers - -4. **Document Custom Headers** - If using custom `X-` headers, document their purpose - -5. **Case Sensitivity** - HTTP headers are case-insensitive, but use standard casing (e.g., `Content-Type` not `content-type`) - -## Common Use Cases - -### Pagination Metadata - -```apex -new HttpMock() - .whenGetOn('/api/users?page=2') - .body('{"users": [...]}') - .header('X-Total-Count', '1000') - .header('X-Page-Number', '2') - .header('X-Page-Size', '50') - .header('X-Total-Pages', '20') - .header('Link', '; rel="next"') - .statusCodeOk() - .mock(); -``` - -### Error Tracking - -```apex -new HttpMock() - .whenGetOn('/api/error-prone') - .body('{"error": "Internal error"}') - .header('X-Request-ID', 'req-123-456') - .header('X-Error-Code', 'ERR_500') - .statusCodeInternalServerError() - .mock(); -``` - -### API Versioning - -```apex -new HttpMock() - .whenGetOn('/api/resource') - .body('{"data": {}}') - .header('X-API-Version', '2.0') - .header('X-Deprecated-In-Version', '3.0') - .header('Sunset', 'Sat, 31 Dec 2024 23:59:59 GMT') - .statusCodeOk() - .mock(); -``` - -### Content Negotiation - -```apex -new HttpMock() - .whenGetOn('/api/data') - .body('{"data": []}') - .contentTypeJson() - .header('Content-Language', 'en-US') - .header('Vary', 'Accept, Accept-Language') - .statusCodeOk() - .mock(); -``` - -## Header Reference - -| Header | Purpose | Example Value | -|--------|---------|---------------| -| `Cache-Control` | Caching directives | `no-cache`, `max-age=3600` | -| `Content-Disposition` | File download info | `attachment; filename="file.pdf"` | -| `Content-Length` | Response size | `1234` | -| `ETag` | Cache validation | `"abc123"` | -| `Location` | Redirect/resource URL | `/api/users/123` | -| `X-RateLimit-*` | Rate limiting info | `100`, `95`, `1640000000` | -| `X-Request-ID` | Request tracking | `abc-123-def-456` | -| `X-API-Version` | API version | `v2.1.0` | -| `Set-Cookie` | Set cookies | `session=abc; HttpOnly` | - -## See Also +## Reference -- [Content Types →](/api/content-types) -- [Response Body →](/api/response-body) -- [Examples →](/examples/custom-headers) +| Header | Purpose | +|--------|---------| +| `Cache-Control` | Caching directives | +| `Content-Disposition` | File download info | +| `Content-Length` | Response size | +| `ETag` | Cache validation | +| `Location` | Redirect/resource URL | +| `X-RateLimit-*` | Rate limiting info | +| `X-Request-ID` | Request tracking | +| `Set-Cookie` | Set cookies | diff --git a/website/api/http-methods.md b/website/api/http-methods.md index dc2de60..bf1d64f 100644 --- a/website/api/http-methods.md +++ b/website/api/http-methods.md @@ -2,29 +2,17 @@ Mock different HTTP methods for your endpoints. -## Supported Methods - -HTTP Mock Lib supports all standard HTTP methods: - -- `GET` - Retrieve data -- `POST` - Create resources -- `PUT` - Update/replace resources -- `PATCH` - Partially update resources -- `DELETE` - Remove resources -- `HEAD` - Get headers only -- `TRACE` - Debug/diagnostic method - -## API - -### whenGetOn() - -Mock a GET request. - ```apex -HttpMock whenGetOn(String endpointToMock) +new HttpMock() + .whenGetOn('/api/v1/users/123') + .whenPostOn('/api/v1/comments/') + .mock(); ``` -**Example:** +## GET + +Retrieve data. + ```apex new HttpMock() .whenGetOn('/api/v1/users/123') @@ -33,15 +21,10 @@ new HttpMock() .mock(); ``` -### whenPostOn() - -Mock a POST request. +## POST -```apex -HttpMock whenPostOn(String endpointToMock) -``` +Create resources. -**Example:** ```apex new HttpMock() .whenPostOn('/api/v1/users') @@ -50,15 +33,10 @@ new HttpMock() .mock(); ``` -### whenPutOn() +## PUT -Mock a PUT request. +Update/replace resources. -```apex -HttpMock whenPutOn(String endpointToMock) -``` - -**Example:** ```apex new HttpMock() .whenPutOn('/api/v1/users/123') @@ -67,15 +45,10 @@ new HttpMock() .mock(); ``` -### whenPatchOn() - -Mock a PATCH request. +## PATCH -```apex -HttpMock whenPatchOn(String endpointToMock) -``` +Partially update resources. -**Example:** ```apex new HttpMock() .whenPatchOn('/api/v1/users/123') @@ -84,15 +57,10 @@ new HttpMock() .mock(); ``` -### whenDeleteOn() - -Mock a DELETE request. +## DELETE -```apex -HttpMock whenDeleteOn(String endpointToMock) -``` +Remove resources. -**Example:** ```apex new HttpMock() .whenDeleteOn('/api/v1/users/123') @@ -100,15 +68,10 @@ new HttpMock() .mock(); ``` -### whenHeadOn() +## HEAD -Mock a HEAD request. +Get headers only. -```apex -HttpMock whenHeadOn(String endpointToMock) -``` - -**Example:** ```apex new HttpMock() .whenHeadOn('/api/v1/users/123') @@ -117,15 +80,10 @@ new HttpMock() .mock(); ``` -### whenTraceOn() - -Mock a TRACE request. +## TRACE -```apex -HttpMock whenTraceOn(String endpointToMock) -``` +Debug/diagnostic method. -**Example:** ```apex new HttpMock() .whenTraceOn('/api/v1/debug') @@ -142,25 +100,21 @@ You can mock multiple HTTP methods in a single test: @IsTest static void testCrudOperations() { new HttpMock() - // Create .whenPostOn('/api/v1/users') .body('{"id": "123"}') .statusCodeCreated() - // Read .whenGetOn('/api/v1/users/123') .body('{"id": "123", "name": "John"}') .statusCodeOk() - // Update .whenPutOn('/api/v1/users/123') .body('{"updated": true}') .statusCodeOk() - // Delete .whenDeleteOn('/api/v1/users/123') .statusCodeNoContent() .mock(); Test.startTest(); - // Your CRUD operations here + // Your callout here Test.stopTest(); } ``` @@ -185,24 +139,3 @@ new HttpMock() .whenGetOn('https://api.example.com/v1/users') // ❌ Wrong - includes domain .mock(); ``` - -## Best Practices - -1. **Use Full Paths** - Include API version in the path: `/api/v1/users` instead of `/users` - -2. **Match HTTP Semantics** - Use the correct method for the operation: - - `GET` for retrieval - - `POST` for creation - - `PUT` for full updates - - `PATCH` for partial updates - - `DELETE` for removal - -3. **Test All Methods** - If your service uses multiple HTTP methods, test them all - -4. **RESTful Patterns** - Follow REST conventions in your mocks to match real APIs - -## See Also - -- [Status Codes →](/api/status-codes) -- [Response Body →](/api/response-body) -- [Examples →](/examples/basic) diff --git a/website/api/index.md b/website/api/index.md index 677629a..521c261 100644 --- a/website/api/index.md +++ b/website/api/index.md @@ -142,17 +142,3 @@ If not specified, HTTP Mock Lib uses these defaults: ## Thread Safety HTTP Mock Lib uses Salesforce's built-in `Test.setMock()` mechanism, which is thread-safe within test context. - -## Best Practices - -1. **Call `.mock()` last** - Always call `.mock()` as the final method to activate the mock -2. **One mock per test** - Create a new HttpMock for each test method -3. **Clear endpoint paths** - Use full paths like `/api/v1/users` instead of `/users` -4. **Meaningful status codes** - Use appropriate status codes to test error handling -5. **Realistic responses** - Use actual response formats from your APIs - -## See Also - -- [HTTP Methods →](/api/http-methods) -- [Status Codes →](/api/status-codes) -- [Examples →](/examples/basic) diff --git a/website/api/response-body.md b/website/api/response-body.md index f3ee7b4..f185116 100644 --- a/website/api/response-body.md +++ b/website/api/response-body.md @@ -2,41 +2,29 @@ Configure the response body for your mocked HTTP calls. -## Overview - -HTTP Mock Lib supports three types of response bodies: -- **String** - Raw string data -- **Object** - Apex objects (automatically serialized to JSON) -- **Blob** - Binary data - -## API - -### body(String) - -Set a string response body. - -```apex -HttpMock body(String body) -``` - -**Example:** ```apex new HttpMock() .whenGetOn('/api/users') .body('{"id": "123", "name": "John Doe"}') - .statusCodeOk() .mock(); ``` -### body(Object) +## String -Set an object response body. The object will be JSON-serialized automatically. +Raw string data (JSON, XML, plain text, etc.). ```apex -HttpMock body(Object body) +new HttpMock() + .whenGetOn('/api/v1/token') + .body('{"access_token": "abc123", "expires_in": 3600}') + .statusCodeOk() + .mock(); ``` -**Example:** +## Object + +Apex objects are automatically serialized to JSON. + ```apex Map response = new Map{ 'id' => '123', @@ -51,15 +39,10 @@ new HttpMock() .mock(); ``` -### body(Blob) - -Set a binary response body. +## Blob -```apex -HttpMock body(Blob body) -``` +Binary data for files. -**Example:** ```apex Blob pdfData = Blob.valueOf('PDF content here'); @@ -71,71 +54,19 @@ new HttpMock() .mock(); ``` -## Examples - -### JSON String Response - -```apex -new HttpMock() - .whenGetOn('/api/v1/token') - .body('{"access_token": "abc123", "expires_in": 3600}') - .contentTypeJson() - .statusCodeOk() - .mock(); -``` - -### Map Response - -```apex -Map tokenResponse = new Map{ - 'access_token' => 'abc123', - 'expires_in' => '3600' -}; - -new HttpMock() - .whenGetOn('/api/v1/token') - .body(tokenResponse) - .statusCodeOk() - .mock(); -``` +## Static Resource -### List Response +Load response body from a Static Resource. ```apex -List> users = new List>{ - new Map{ 'id' => '1', 'name' => 'Alice' }, - new Map{ 'id' => '2', 'name' => 'Bob' } -}; - new HttpMock() .whenGetOn('/api/users') - .body(users) + .staticResource('UsersResponseMock') .statusCodeOk() .mock(); ``` -### Custom Class Response - -```apex -public class UserResponse { - public String id; - public String name; - public String email; -} - -UserResponse user = new UserResponse(); -user.id = '123'; -user.name = 'John Doe'; -user.email = 'john@example.com'; - -new HttpMock() - .whenGetOn('/api/users/123') - .body(user) - .statusCodeOk() - .mock(); -``` - -### XML Response +## XML Response ```apex String xmlResponse = '' + @@ -152,7 +83,7 @@ new HttpMock() .mock(); ``` -### Empty Response +## Empty Response For responses with no body (like DELETE operations): @@ -161,104 +92,16 @@ new HttpMock() .whenDeleteOn('/api/users/123') .statusCodeNoContent() .mock(); -// No .body() call needed -``` - -### Binary Response (PDF) - -```apex -Blob pdfContent = generatePdfContent(); - -new HttpMock() - .whenGetOn('/api/reports/monthly.pdf') - .body(pdfContent) - .contentTypePdf() - .header('Content-Disposition', 'attachment; filename="report.pdf"') - .statusCodeOk() - .mock(); -``` - -## Object Serialization - -When using `body(Object)`, HTTP Mock Lib uses `JSON.serialize()` to convert your object: - -```apex -// This code: -new HttpMock() - .body(new Map{ 'key' => 'value' }) - .mock(); - -// Is equivalent to: -new HttpMock() - .body('{"key":"value"}') - .mock(); ``` ## Default Behavior -If you don't call `.body()`, the response body will be an empty string: +If you don't call `.body()`, the response body defaults to `{}`: ```apex new HttpMock() .whenGetOn('/api/ping') .statusCodeOk() .mock(); -// Response body: "" +// Response body: "{}" ``` - -## Best Practices - -1. **Use Objects for Complex Data** - For complex responses, use Maps or custom classes instead of building JSON strings - -2. **Match Real Responses** - Use response formats that match your actual API - -3. **Set Appropriate Content-Type** - Always set the correct content type for your response body - -4. **Validate JSON** - If using string bodies, ensure your JSON is valid - -5. **Test Edge Cases** - Mock empty responses, large responses, and malformed responses - -## Common Patterns - -### Paginated Response - -```apex -Map paginatedResponse = new Map{ - 'data' => new List>{ - new Map{ 'id' => '1' }, - new Map{ 'id' => '2' } - }, - 'page' => 1, - 'total_pages' => 5, - 'total_count' => 50 -}; - -new HttpMock() - .whenGetOn('/api/users?page=1') - .body(paginatedResponse) - .statusCodeOk() - .mock(); -``` - -### Error Response - -```apex -Map errorResponse = new Map{ - 'error' => new Map{ - 'code' => 'INVALID_REQUEST', - 'message' => 'Email is required' - } -}; - -new HttpMock() - .whenPostOn('/api/users') - .body(errorResponse) - .statusCodeBadRequest() - .mock(); -``` - -## See Also - -- [Content Types →](/api/content-types) -- [HTTP Methods →](/api/http-methods) -- [Examples →](/examples/basic) diff --git a/website/api/status-codes.md b/website/api/status-codes.md index 7dcd132..baafa06 100644 --- a/website/api/status-codes.md +++ b/website/api/status-codes.md @@ -2,16 +2,16 @@ Set HTTP status codes for your mocked responses. -## Built-in Status Codes - -HTTP Mock Lib provides semantic methods for common HTTP status codes. - -### Success Codes (2xx) - -#### statusCodeOk() ```apex -HttpMock statusCodeOk() // 200 +new HttpMock() + .whenGetOn('/api/users') + .body('{"users": []}') + .statusCodeOk() + .mock(); ``` + +## 200 OK + Standard success response. ```apex @@ -22,10 +22,8 @@ new HttpMock() .mock(); ``` -#### statusCodeCreated() -```apex -HttpMock statusCodeCreated() // 201 -``` +## 201 Created + Resource successfully created. ```apex @@ -36,10 +34,8 @@ new HttpMock() .mock(); ``` -#### statusCodeAccepted() -```apex -HttpMock statusCodeAccepted() // 202 -``` +## 202 Accepted + Request accepted for processing. ```apex @@ -50,10 +46,8 @@ new HttpMock() .mock(); ``` -#### statusCodeNoContent() -```apex -HttpMock statusCodeNoContent() // 204 -``` +## 204 No Content + Success with no response body. ```apex @@ -63,12 +57,8 @@ new HttpMock() .mock(); ``` -### Client Error Codes (4xx) +## 400 Bad Request -#### statusCodeBadRequest() -```apex -HttpMock statusCodeBadRequest() // 400 -``` Invalid request. ```apex @@ -79,10 +69,8 @@ new HttpMock() .mock(); ``` -#### statusCodeUnauthorized() -```apex -HttpMock statusCodeUnauthorized() // 401 -``` +## 401 Unauthorized + Authentication required. ```apex @@ -93,10 +81,8 @@ new HttpMock() .mock(); ``` -#### statusCodeForbidden() -```apex -HttpMock statusCodeForbidden() // 403 -``` +## 403 Forbidden + Access denied. ```apex @@ -107,10 +93,8 @@ new HttpMock() .mock(); ``` -#### statusCodeNotFound() -```apex -HttpMock statusCodeNotFound() // 404 -``` +## 404 Not Found + Resource not found. ```apex @@ -121,10 +105,8 @@ new HttpMock() .mock(); ``` -#### statusCodeMethodNotAllowed() -```apex -HttpMock statusCodeMethodNotAllowed() // 405 -``` +## 405 Method Not Allowed + HTTP method not supported. ```apex @@ -135,12 +117,8 @@ new HttpMock() .mock(); ``` -### Server Error Codes (5xx) +## 500 Internal Server Error -#### statusCodeInternalServerError() -```apex -HttpMock statusCodeInternalServerError() // 500 -``` Server error. ```apex @@ -151,10 +129,8 @@ new HttpMock() .mock(); ``` -#### statusCodeNotImplemented() -```apex -HttpMock statusCodeNotImplemented() // 501 -``` +## 501 Not Implemented + Functionality not implemented. ```apex @@ -165,10 +141,8 @@ new HttpMock() .mock(); ``` -#### statusCodeBadGateway() -```apex -HttpMock statusCodeBadGateway() // 502 -``` +## 502 Bad Gateway + Invalid response from upstream server. ```apex @@ -179,10 +153,8 @@ new HttpMock() .mock(); ``` -#### statusCodeServiceUnavailable() -```apex -HttpMock statusCodeServiceUnavailable() // 503 -``` +## 503 Service Unavailable + Service temporarily unavailable. ```apex @@ -193,10 +165,8 @@ new HttpMock() .mock(); ``` -#### statusCodeGatewayTimeout() -```apex -HttpMock statusCodeGatewayTimeout() // 504 -``` +## 504 Gateway Timeout + Gateway timeout. ```apex @@ -207,99 +177,34 @@ new HttpMock() .mock(); ``` -## Custom Status Codes - -For status codes not covered by built-in methods, use `statusCode()`: +## Custom -```apex -HttpMock statusCode(Integer statusCode) -``` +For status codes not covered by built-in methods. -**Example:** ```apex new HttpMock() .whenGetOn('/api/users') .body('{"error": "Too many requests"}') - .statusCode(429) // Custom: Too Many Requests - .mock(); -``` - -## Default Status Code - -If no status code is specified, HTTP Mock Lib uses **200 (OK)** by default: - -```apex -// These are equivalent: -new HttpMock() - .whenGetOn('/api/users') - .body('{"users": []}') - .mock(); - -new HttpMock() - .whenGetOn('/api/users') - .body('{"users": []}') - .statusCodeOk() // Explicitly set - .mock(); -``` - -## Testing Error Handling - -Use error status codes to test how your code handles failures: - -```apex -@IsTest -static void testUnauthorizedError() { - // Arrange - new HttpMock() - .whenGetOn('/api/secure-data') - .body('{"error": "Unauthorized"}') - .statusCodeUnauthorized() - .mock(); - - // Act & Assert - Test.startTest(); - try { - new ApiService().getSecureData(); - Assert.fail('Expected CalloutException'); - } catch (CalloutException e) { - Assert.isTrue(e.getMessage().contains('Unauthorized')); - } - Test.stopTest(); -} -``` - -## Complete Status Code Reference - -| Code | Method | Description | -|------|--------|-------------| -| 200 | `statusCodeOk()` | Success | -| 201 | `statusCodeCreated()` | Resource created | -| 202 | `statusCodeAccepted()` | Request accepted | -| 204 | `statusCodeNoContent()` | Success, no content | -| 400 | `statusCodeBadRequest()` | Bad request | -| 401 | `statusCodeUnauthorized()` | Unauthorized | -| 403 | `statusCodeForbidden()` | Forbidden | -| 404 | `statusCodeNotFound()` | Not found | -| 405 | `statusCodeMethodNotAllowed()` | Method not allowed | -| 500 | `statusCodeInternalServerError()` | Server error | -| 501 | `statusCodeNotImplemented()` | Not implemented | -| 502 | `statusCodeBadGateway()` | Bad gateway | -| 503 | `statusCodeServiceUnavailable()` | Service unavailable | -| 504 | `statusCodeGatewayTimeout()` | Gateway timeout | -| Custom | `statusCode(Integer)` | Any status code | - -## Best Practices - -1. **Use Semantic Methods** - Prefer `statusCodeOk()` over `statusCode(200)` for readability - -2. **Test Error Paths** - Don't just test success cases; mock error responses too - -3. **Match Real APIs** - Use status codes that match what the real API returns - -4. **Document Exceptions** - When testing error cases, document why you expect them - -## See Also - -- [HTTP Methods →](/api/http-methods) -- [Error Handling Examples →](/examples/error-handling) -- [Headers →](/api/headers) + .statusCode(429) + .mock(); +``` + +## Reference + +| Code | Method | +|------|--------| +| 200 | `statusCodeOk()` | +| 201 | `statusCodeCreated()` | +| 202 | `statusCodeAccepted()` | +| 204 | `statusCodeNoContent()` | +| 400 | `statusCodeBadRequest()` | +| 401 | `statusCodeUnauthorized()` | +| 403 | `statusCodeForbidden()` | +| 404 | `statusCodeNotFound()` | +| 405 | `statusCodeMethodNotAllowed()` | +| 500 | `statusCodeInternalServerError()` | +| 501 | `statusCodeNotImplemented()` | +| 502 | `statusCodeBadGateway()` | +| 503 | `statusCodeServiceUnavailable()` | +| 504 | `statusCodeGatewayTimeout()` | +| Custom | `statusCode(Integer)` | diff --git a/website/examples/basic.md b/website/examples/basic.md deleted file mode 100644 index 23a3e55..0000000 --- a/website/examples/basic.md +++ /dev/null @@ -1,262 +0,0 @@ -# Basic Examples - -Learn HTTP Mock Lib fundamentals through practical examples. - -## Simple GET Request - -The most basic use case - mocking a single GET endpoint: - -```apex -@IsTest -static void testSimpleGet() { - // Arrange - new HttpMock() - .whenGetOn('/api/users') - .body('{"id": "123", "name": "John Doe"}') - .statusCodeOk() - .mock(); - - // Act - Test.startTest(); - UserService service = new UserService(); - User result = service.getUser(); - Test.stopTest(); - - // Assert - Assert.areEqual('John Doe', result.name); -} -``` - -## POST Request - -Mock a POST request that creates a resource: - -```apex -@IsTest -static void testCreateUser() { - // Arrange - new HttpMock() - .whenPostOn('/api/users') - .body('{"id": "456", "created": true}') - .statusCodeCreated() - .mock(); - - // Act - Test.startTest(); - UserService service = new UserService(); - String userId = service.createUser('John Doe'); - Test.stopTest(); - - // Assert - Assert.areEqual('456', userId); -} -``` - -## Using Objects as Bodies - -Instead of JSON strings, you can pass Apex objects: - -```apex -@IsTest -static void testWithObjectBody() { - // Arrange - Map responseData = new Map{ - 'users' => new List>{ - new Map{ 'id' => '1', 'name' => 'Alice' }, - new Map{ 'id' => '2', 'name' => 'Bob' } - }, - 'total' => 2 - }; - - new HttpMock() - .whenGetOn('/api/users') - .body(responseData) - .statusCodeOk() - .mock(); - - // Act - Test.startTest(); - List users = new UserService().getUsers(); - Test.stopTest(); - - // Assert - Assert.areEqual(2, users.size()); -} -``` - -## Multiple Endpoints - -Mock several endpoints in one test: - -```apex -@IsTest -static void testMultipleEndpoints() { - // Arrange - new HttpMock() - .whenGetOn('/api/auth/token') - .body('{"token": "xyz123"}') - .statusCodeOk() - .whenPostOn('/api/users') - .body('{"id": "789"}') - .statusCodeCreated() - .whenDeleteOn('/api/users/789') - .statusCodeNoContent() - .mock(); - - // Act - Test.startTest(); - UserService service = new UserService(); - service.authenticate(); - String userId = service.createUser('Test User'); - service.deleteUser(userId); - Test.stopTest(); - - // Assert - Assert.isNotNull(userId); -} -``` - -## Custom Headers - -Add custom headers to your response: - -```apex -@IsTest -static void testWithCustomHeaders() { - // Arrange - new HttpMock() - .whenGetOn('/api/users') - .body('{"users": []}') - .header('X-Total-Count', '100') - .header('X-Page-Number', '1') - .header('Cache-Control', 'no-cache') - .statusCodeOk() - .mock(); - - // Act - Test.startTest(); - ApiResponse response = new UserService().getUsersWithMetadata(); - Test.stopTest(); - - // Assert - Assert.areEqual(100, response.totalCount); -} -``` - -## Different Content Types - -### XML Response - -```apex -@IsTest -static void testXmlResponse() { - // Arrange - String xmlData = '' + - '' + - '123' + - 'John Doe' + - ''; - - new HttpMock() - .whenGetOn('/api/user.xml') - .body(xmlData) - .contentTypeXml() - .statusCodeOk() - .mock(); - - // Act - Test.startTest(); - User user = new UserService().getUserAsXml(); - Test.stopTest(); - - // Assert - Assert.areEqual('123', user.id); -} -``` - -### CSV Response - -```apex -@IsTest -static void testCsvExport() { - // Arrange - String csvData = 'id,name,email\n' + - '1,John,john@example.com\n' + - '2,Jane,jane@example.com'; - - new HttpMock() - .whenGetOn('/api/users/export') - .body(csvData) - .contentTypeCsv() - .header('Content-Disposition', 'attachment; filename="users.csv"') - .statusCodeOk() - .mock(); - - // Act - Test.startTest(); - String csvContent = new UserService().exportUsers(); - Test.stopTest(); - - // Assert - Assert.isTrue(csvContent.contains('John')); -} -``` - -## Default Values - -If you don't specify certain properties, HTTP Mock uses sensible defaults: - -```apex -@IsTest -static void testDefaults() { - // This mock uses all defaults: - // - Status Code: 200 (OK) - // - Content-Type: application/json - // - Body: {} - new HttpMock() - .whenGetOn('/api/ping') - .mock(); - - // Act - Test.startTest(); - Boolean pong = new ApiService().ping(); - Test.stopTest(); - - // Assert - Assert.isTrue(pong); -} -``` - -## Minimal Example - -The absolute minimum needed for a mock: - -```apex -@IsTest -static void testMinimalMock() { - new HttpMock() - .whenGetOn('/api/status') - .mock(); - - Test.startTest(); - new ApiService().checkStatus(); - Test.stopTest(); -} -``` - -## Tips - -1. **Call `.mock()` last** - Always end with `.mock()` to activate your configuration - -2. **One mock per test** - Create a fresh HttpMock for each test method - -3. **Match real endpoints** - Use the same paths your code actually calls - -4. **Use meaningful data** - Test with realistic response data - -## Next Steps - -- [Multiple Endpoints Examples](/examples/multiple-endpoints) -- [Custom Headers Examples](/examples/custom-headers) -- [Error Handling Examples](/examples/error-handling) -- [API Reference](/api/) diff --git a/website/examples/custom-headers.md b/website/examples/custom-headers.md deleted file mode 100644 index 2ec9c17..0000000 --- a/website/examples/custom-headers.md +++ /dev/null @@ -1,347 +0,0 @@ -# Custom Headers - -Add and test custom HTTP headers in your mocked responses. - -## Basic Header Usage - -Add a single custom header: - -```apex -@IsTest -static void testSingleHeader() { - new HttpMock() - .whenGetOn('/api/users') - .body('{"users": []}') - .header('X-Total-Count', '100') - .statusCodeOk() - .mock(); - - Test.startTest(); - ApiResponse response = new UserService().getUsers(); - Test.stopTest(); - - Assert.areEqual(100, response.totalCount); -} -``` - -## Multiple Headers - -Chain multiple `.header()` calls: - -```apex -@IsTest -static void testMultipleHeaders() { - new HttpMock() - .whenGetOn('/api/data') - .body('{"data": []}') - .header('X-Request-ID', 'abc-123') - .header('X-API-Version', 'v2') - .header('Cache-Control', 'no-cache') - .header('X-RateLimit-Remaining', '99') - .statusCodeOk() - .mock(); - - Test.startTest(); - ApiResponse response = new ApiService().getData(); - Test.stopTest(); - - Assert.areEqual('abc-123', response.requestId); - Assert.areEqual('v2', response.apiVersion); -} -``` - -## Pagination Headers - -Use headers to communicate pagination metadata: - -```apex -@IsTest -static void testPaginationHeaders() { - new HttpMock() - .whenGetOn('/api/users?page=2') - .body('{"users": [...]}') - .header('X-Total-Count', '1000') - .header('X-Page-Number', '2') - .header('X-Page-Size', '50') - .header('X-Total-Pages', '20') - .header('Link', '; rel="next", ; rel="prev"') - .statusCodeOk() - .mock(); - - Test.startTest(); - PaginatedResponse response = new UserService().getUsers(2); - Test.stopTest(); - - Assert.areEqual(1000, response.totalCount); - Assert.areEqual(2, response.currentPage); - Assert.areEqual(20, response.totalPages); -} -``` - -## Cache Headers - -Test cache-related headers: - -```apex -@IsTest -static void testCacheHeaders() { - new HttpMock() - .whenGetOn('/api/static-data') - .body('{"data": "cached content"}') - .header('Cache-Control', 'public, max-age=3600') - .header('ETag', '"abc123"') - .header('Last-Modified', 'Wed, 21 Oct 2023 07:28:00 GMT') - .header('Expires', 'Wed, 21 Oct 2023 08:28:00 GMT') - .statusCodeOk() - .mock(); - - Test.startTest(); - CachedResponse response = new ApiService().getCachedData(); - Test.stopTest(); - - Assert.isTrue(response.isCacheable); - Assert.areEqual(3600, response.maxAge); -} -``` - -## Rate Limiting Headers - -Mock rate limit information: - -```apex -@IsTest -static void testRateLimitHeaders() { - new HttpMock() - .whenGetOn('/api/limited') - .body('{"data": {}}') - .header('X-RateLimit-Limit', '100') - .header('X-RateLimit-Remaining', '95') - .header('X-RateLimit-Reset', '1640000000') - .statusCodeOk() - .mock(); - - Test.startTest(); - ApiResponse response = new ApiService().callLimitedEndpoint(); - Test.stopTest(); - - Assert.areEqual(100, response.rateLimit); - Assert.areEqual(95, response.rateLimitRemaining); - Assert.isFalse(response.rateLimitExceeded); -} -``` - -## File Download Headers - -Test file download with Content-Disposition: - -```apex -@IsTest -static void testFileDownload() { - Blob pdfContent = Blob.valueOf('PDF content here'); - - new HttpMock() - .whenGetOn('/api/reports/monthly.pdf') - .body(pdfContent) - .contentTypePdf() - .header('Content-Disposition', 'attachment; filename="monthly-report.pdf"') - .header('Content-Length', String.valueOf(pdfContent.size())) - .statusCodeOk() - .mock(); - - Test.startTest(); - FileDownload download = new ReportService().downloadMonthlyReport(); - Test.stopTest(); - - Assert.areEqual('monthly-report.pdf', download.filename); - Assert.isNotNull(download.content); -} -``` - -## CORS Headers - -Mock CORS headers for cross-origin requests: - -```apex -@IsTest -static void testCorsHeaders() { - new HttpMock() - .whenGetOn('/api/public/data') - .body('{"data": []}') - .header('Access-Control-Allow-Origin', '*') - .header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE') - .header('Access-Control-Allow-Headers', 'Content-Type, Authorization') - .header('Access-Control-Max-Age', '3600') - .statusCodeOk() - .mock(); - - Test.startTest(); - ApiResponse response = new ApiService().getPublicData(); - Test.stopTest(); - - Assert.isNotNull(response); -} -``` - -## Authentication Headers - -Test authentication token headers: - -```apex -@IsTest -static void testAuthHeaders() { - new HttpMock() - .whenPostOn('/api/login') - .body('{"userId": "123"}') - .header('X-Auth-Token', 'xyz789') - .header('Set-Cookie', 'session=abc123; HttpOnly; Secure; SameSite=Strict') - .statusCodeOk() - .mock(); - - Test.startTest(); - AuthResponse response = new AuthService().login('user', 'pass'); - Test.stopTest(); - - Assert.areEqual('xyz789', response.authToken); - Assert.isNotNull(response.sessionCookie); -} -``` - -## Custom Business Headers - -Use headers for custom business logic: - -```apex -@IsTest -static void testBusinessHeaders() { - new HttpMock() - .whenGetOn('/api/orders') - .body('{"orders": []}') - .header('X-Tenant-ID', 'tenant-123') - .header('X-Feature-Flags', 'feature1,feature2') - .header('X-Correlation-ID', 'corr-456') - .header('X-Server-Time', '2023-10-21T12:00:00Z') - .statusCodeOk() - .mock(); - - Test.startTest(); - ApiResponse response = new OrderService().getOrders(); - Test.stopTest(); - - Assert.areEqual('tenant-123', response.tenantId); - Assert.isTrue(response.hasFeature('feature1')); -} -``` - -## Override Content-Type - -You can override the default Content-Type using `.header()`: - -```apex -@IsTest -static void testCustomContentType() { - new HttpMock() - .whenGetOn('/api/custom') - .body('{"data": {}}') - .header('Content-Type', 'application/vnd.api+json; charset=utf-8') - .statusCodeOk() - .mock(); - - Test.startTest(); - ApiResponse response = new ApiService().getCustomFormat(); - Test.stopTest(); - - Assert.isNotNull(response); -} -``` - -## Debugging Headers - -Add debugging information via headers: - -```apex -@IsTest -static void testDebugHeaders() { - new HttpMock() - .whenGetOn('/api/debug') - .body('{"data": {}}') - .header('X-Request-ID', 'req-123-456') - .header('X-Server-ID', 'server-42') - .header('X-Request-Duration', '127ms') - .header('X-Cache-Status', 'MISS') - .statusCodeOk() - .mock(); - - Test.startTest(); - ApiResponse response = new ApiService().callWithDebug(); - Test.stopTest(); - - Assert.areEqual('req-123-456', response.requestId); - Assert.areEqual(127, response.durationMs); -} -``` - -## Location Header - -Test redirect and created resource headers: - -```apex -@IsTest -static void testLocationHeader() { - new HttpMock() - .whenPostOn('/api/users') - .body('{"id": "123"}') - .header('Location', '/api/users/123') - .statusCodeCreated() - .mock(); - - Test.startTest(); - CreateResponse response = new UserService().createUser('John Doe'); - Test.stopTest(); - - Assert.areEqual('123', response.id); - Assert.areEqual('/api/users/123', response.location); -} -``` - -## Deprecation Headers - -Communicate API deprecation: - -```apex -@IsTest -static void testDeprecationHeaders() { - new HttpMock() - .whenGetOn('/api/v1/old-endpoint') - .body('{"data": {}}') - .header('Deprecation', 'true') - .header('Sunset', 'Sat, 31 Dec 2024 23:59:59 GMT') - .header('Link', '; rel="alternate"') - .statusCodeOk() - .mock(); - - Test.startTest(); - ApiResponse response = new ApiService().callOldEndpoint(); - Test.stopTest(); - - Assert.isTrue(response.isDeprecated); - Assert.isNotNull(response.sunsetDate); -} -``` - -## Best Practices - -1. **Use Standard Headers** - Prefer standard HTTP headers when possible - -2. **Namespace Custom Headers** - Use `X-` prefix for custom headers (though this is deprecated in RFC 6648, it's still common) - -3. **Test Header Parsing** - Verify your code correctly reads and uses headers - -4. **Match Real APIs** - Include the same headers the actual API returns - -5. **Document Header Purpose** - Comment what each custom header represents - -## See Also - -- [Headers API](/api/headers) -- [Basic Examples](/examples/basic) -- [Content Types](/api/content-types) diff --git a/website/examples/error-handling.md b/website/examples/error-handling.md deleted file mode 100644 index 8f8515f..0000000 --- a/website/examples/error-handling.md +++ /dev/null @@ -1,384 +0,0 @@ -# Error Handling - -Test how your code handles HTTP errors and edge cases. - -## HTTP 400 - Bad Request - -Test invalid request handling: - -```apex -@IsTest -static void testBadRequest() { - // Arrange - new HttpMock() - .whenPostOn('/api/users') - .body('{"error": "Email is required"}') - .statusCodeBadRequest() - .mock(); - - // Act & Assert - Test.startTest(); - try { - new UserService().createUser(''); // Empty email - Assert.fail('Expected CalloutException'); - } catch (CalloutException e) { - Assert.isTrue(e.getMessage().contains('Email is required')); - } - Test.stopTest(); -} -``` - -## HTTP 401 - Unauthorized - -Test authentication failures: - -```apex -@IsTest -static void testUnauthorized() { - // Arrange - new HttpMock() - .whenGetOn('/api/protected/data') - .body('{"error": "Invalid token"}') - .statusCodeUnauthorized() - .mock(); - - // Act & Assert - Test.startTest(); - try { - new ApiService().getProtectedData('invalid-token'); - Assert.fail('Expected exception'); - } catch (CalloutException e) { - Assert.isTrue(e.getMessage().contains('Unauthorized')); - } - Test.stopTest(); -} -``` - -## HTTP 404 - Not Found - -Test resource not found scenarios: - -```apex -@IsTest -static void testNotFound() { - // Arrange - new HttpMock() - .whenGetOn('/api/users/999') - .body('{"error": "User not found"}') - .statusCodeNotFound() - .mock(); - - // Act - Test.startTest(); - User user = new UserService().getUser('999'); - Test.stopTest(); - - // Assert - Assert.isNull(user, 'User should be null for 404 response'); -} -``` - -## HTTP 500 - Internal Server Error - -Test server error handling: - -```apex -@IsTest -static void testServerError() { - // Arrange - new HttpMock() - .whenGetOn('/api/users') - .body('{"error": "Internal server error"}') - .statusCodeInternalServerError() - .mock(); - - // Act & Assert - Test.startTest(); - try { - new UserService().getUsers(); - Assert.fail('Expected CalloutException'); - } catch (CalloutException e) { - Assert.isTrue(e.getMessage().contains('500')); - } - Test.stopTest(); -} -``` - -## HTTP 503 - Service Unavailable - -Test service downtime: - -```apex -@IsTest -static void testServiceUnavailable() { - // Arrange - new HttpMock() - .whenGetOn('/api/users') - .body('{"error": "Service temporarily unavailable"}') - .statusCodeServiceUnavailable() - .header('Retry-After', '120') - .mock(); - - // Act - Test.startTest(); - ApiResponse response = new UserService().getUsersWithRetry(); - Test.stopTest(); - - // Assert - Assert.isTrue(response.shouldRetry); - Assert.areEqual(120, response.retryAfter); -} -``` - -## Retry Logic - -Test automatic retry mechanisms: - -```apex -@IsTest -static void testRetryMechanism() { - // Arrange - first two calls fail, third succeeds - new HttpMock() - .whenGetOn('/api/unstable') - .body('{"error": "Timeout"}') - .statusCodeGatewayTimeout() - .whenGetOn('/api/unstable') - .body('{"error": "Timeout"}') - .statusCodeGatewayTimeout() - .whenGetOn('/api/unstable') - .body('{"success": true}') - .statusCodeOk() - .mock(); - - // Act - Test.startTest(); - ApiResponse response = new ApiService().callWithRetry(); - Test.stopTest(); - - // Assert - Assert.isTrue(response.success); - Assert.areEqual(3, HttpMock.getRequestCount('GET', '/api/unstable')); -} -``` - -## Empty Response Body - -Test handling of responses with no body: - -```apex -@IsTest -static void testEmptyResponse() { - // Arrange - new HttpMock() - .whenDeleteOn('/api/users/123') - .statusCodeNoContent() - .mock(); - - // Act - Test.startTest(); - Boolean deleted = new UserService().deleteUser('123'); - Test.stopTest(); - - // Assert - Assert.isTrue(deleted); -} -``` - -## Malformed JSON - -Test handling of invalid response data: - -```apex -@IsTest -static void testMalformedJson() { - // Arrange - new HttpMock() - .whenGetOn('/api/broken') - .body('{"invalid": json}') // Invalid JSON - .statusCodeOk() - .mock(); - - // Act & Assert - Test.startTest(); - try { - new ApiService().parseResponse(); - Assert.fail('Expected JSON parsing exception'); - } catch (Exception e) { - Assert.isTrue(e instanceof JSONException); - } - Test.stopTest(); -} -``` - -## Rate Limiting - -Test rate limit handling: - -```apex -@IsTest -static void testRateLimit() { - // Arrange - new HttpMock() - .whenGetOn('/api/users') - .body('{"error": "Rate limit exceeded"}') - .statusCode(429) // Too Many Requests - .header('X-RateLimit-Remaining', '0') - .header('X-RateLimit-Reset', '1640000000') - .mock(); - - // Act - Test.startTest(); - ApiResponse response = new UserService().getUsers(); - Test.stopTest(); - - // Assert - Assert.isTrue(response.rateLimitExceeded); - Assert.areEqual(1640000000, response.rateLimitReset); -} -``` - -## Timeout Simulation - -Combine with Test.stopTest() timing to test timeouts: - -```apex -@IsTest -static void testTimeout() { - // Arrange - new HttpMock() - .whenGetOn('/api/slow') - .body('{"data": "finally"}') - .statusCodeGatewayTimeout() - .mock(); - - // Act - Test.startTest(); - ApiService service = new ApiService(); - service.setTimeout(1000); // 1 second timeout - - try { - service.callSlowEndpoint(); - Assert.fail('Expected timeout exception'); - } catch (Exception e) { - Assert.isTrue(e.getMessage().contains('timeout')); - } - Test.stopTest(); -} -``` - -## Fallback Behavior - -Test fallback to cached or default data: - -```apex -@IsTest -static void testFallbackOnError() { - // Arrange - new HttpMock() - .whenGetOn('/api/users') - .body('{"error": "Service unavailable"}') - .statusCodeServiceUnavailable() - .mock(); - - // Act - Test.startTest(); - List users = new UserService().getUsersWithFallback(); - Test.stopTest(); - - // Assert - Assert.isNotNull(users); - Assert.areEqual(0, users.size(), 'Should return empty list as fallback'); -} -``` - -## Multiple Error Scenarios - -Test different error responses from the same endpoint: - -```apex -@IsTest -static void testMultipleErrorTypes() { - // Arrange - new HttpMock() - .whenPostOn('/api/users') - .body('{"error": "Bad Request"}') - .statusCodeBadRequest() - .whenPostOn('/api/users') - .body('{"error": "Unauthorized"}') - .statusCodeUnauthorized() - .whenPostOn('/api/users') - .body('{"success": true}') - .statusCodeCreated() - .mock(); - - // Act - Test.startTest(); - UserService service = new UserService(); - - // First call - bad request - try { - service.createUser('invalid-data'); - Assert.fail('Expected exception'); - } catch (CalloutException e) { - Assert.isTrue(e.getMessage().contains('Bad Request')); - } - - // Second call - unauthorized - try { - service.createUser('no-auth'); - Assert.fail('Expected exception'); - } catch (CalloutException e) { - Assert.isTrue(e.getMessage().contains('Unauthorized')); - } - - // Third call - success - String userId = service.createUser('valid-data'); - Assert.isNotNull(userId); - Test.stopTest(); -} -``` - -## Custom Error Codes - -Test custom HTTP status codes: - -```apex -@IsTest -static void testCustomStatusCode() { - // Arrange - new HttpMock() - .whenGetOn('/api/deprecated') - .body('{"message": "API endpoint deprecated"}') - .statusCode(410) // Gone - .header('Sunset', 'Sat, 31 Dec 2024 23:59:59 GMT') - .mock(); - - // Act & Assert - Test.startTest(); - try { - new ApiService().callDeprecatedEndpoint(); - Assert.fail('Expected exception for deprecated endpoint'); - } catch (Exception e) { - Assert.isTrue(e.getMessage().contains('deprecated')); - } - Test.stopTest(); -} -``` - -## Best Practices - -1. **Test All Error Codes** - Don't just test the happy path; verify error handling - -2. **Use Realistic Errors** - Mock error responses that match what the real API returns - -3. **Verify Error Messages** - Check that your code properly parses and handles error details - -4. **Test Recovery** - Verify retry logic, fallbacks, and graceful degradation - -5. **Document Edge Cases** - Comment why you expect certain exceptions - -## See Also - -- [Status Codes API](/api/status-codes) -- [Multiple Endpoints](/examples/multiple-endpoints) -- [Basic Examples](/examples/basic) diff --git a/website/examples/multiple-endpoints.md b/website/examples/multiple-endpoints.md deleted file mode 100644 index 01a9823..0000000 --- a/website/examples/multiple-endpoints.md +++ /dev/null @@ -1,277 +0,0 @@ -# Multiple Endpoints - -Mock multiple HTTP endpoints in a single test. - -## Basic Pattern - -Chain multiple endpoint definitions before calling `.mock()`: - -```apex -@IsTest -static void testFullWorkflow() { - new HttpMock() - .whenGetOn('/api/auth') - .body('{"token": "xyz"}') - .statusCodeOk() - .whenPostOn('/api/data') - .body('{"success": true}') - .statusCodeCreated() - .whenDeleteOn('/api/data/1') - .statusCodeNoContent() - .mock(); - - Test.startTest(); - // Your code that calls all three endpoints - Test.stopTest(); -} -``` - -## CRUD Operations - -Test all CRUD operations in one go: - -```apex -@IsTest -static void testCrudOperations() { - new HttpMock() - // Create - .whenPostOn('/api/users') - .body('{"id": "123"}') - .statusCodeCreated() - // Read - .whenGetOn('/api/users/123') - .body('{"id": "123", "name": "John"}') - .statusCodeOk() - // Update - .whenPutOn('/api/users/123') - .body('{"updated": true}') - .statusCodeOk() - // Delete - .whenDeleteOn('/api/users/123') - .statusCodeNoContent() - .mock(); - - Test.startTest(); - UserService service = new UserService(); - String userId = service.createUser('John'); - User user = service.getUser(userId); - service.updateUser(userId, 'John Updated'); - service.deleteUser(userId); - Test.stopTest(); -} -``` - -## Multiple Responses for Same Endpoint - -Return different responses for subsequent calls to the same endpoint: - -```apex -@IsTest -static void testPollingEndpoint() { - new HttpMock() - .whenGetOn('/api/job/status') - .body('{"status": "pending"}') - .statusCodeOk() - .whenGetOn('/api/job/status') - .body('{"status": "processing"}') - .statusCodeOk() - .whenGetOn('/api/job/status') - .body('{"status": "complete"}') - .statusCodeOk() - .mock(); - - Test.startTest(); - JobService service = new JobService(); - - // First call returns "pending" - String status1 = service.checkJobStatus(); - Assert.areEqual('pending', status1); - - // Second call returns "processing" - String status2 = service.checkJobStatus(); - Assert.areEqual('processing', status2); - - // Third call returns "complete" - String status3 = service.checkJobStatus(); - Assert.areEqual('complete', status3); - Test.stopTest(); -} -``` - -::: tip -When you define multiple responses for the same endpoint, each call removes the first response. The last response stays and is reused for all subsequent calls. -::: - -## Verify Request Count - -Use `getRequestCount()` to verify how many times an endpoint was called: - -```apex -@IsTest -static void testRequestCount() { - new HttpMock() - .whenGetOn('/api/users') - .body('{"users": []}') - .statusCodeOk() - .mock(); - - Test.startTest(); - UserService service = new UserService(); - service.getUsers(); - service.getUsers(); - service.getUsers(); - Test.stopTest(); - - // Verify the endpoint was called exactly 3 times - Integer count = HttpMock.getRequestCount('GET', '/api/users'); - Assert.areEqual(3, count); -} -``` - -## Authentication Flow - -Mock a complete authentication and API call flow: - -```apex -@IsTest -static void testAuthenticatedApiCall() { - new HttpMock() - // Step 1: Get token - .whenPostOn('/oauth/token') - .body('{"access_token": "abc123", "expires_in": 3600}') - .statusCodeOk() - // Step 2: Use token to fetch data - .whenGetOn('/api/v1/protected/data') - .body('{"data": "secret information"}') - .statusCodeOk() - .mock(); - - Test.startTest(); - ApiService service = new ApiService(); - String token = service.authenticate(); - String data = service.getProtectedData(token); - Test.stopTest(); - - Assert.isNotNull(token); - Assert.isNotNull(data); -} -``` - -## Pagination - -Mock paginated API responses: - -```apex -@IsTest -static void testPagination() { - new HttpMock() - // Page 1 - .whenGetOn('/api/users?page=1') - .body('{"users": [{"id": "1"}, {"id": "2"}], "hasMore": true}') - .header('X-Total-Count', '100') - .statusCodeOk() - // Page 2 - .whenGetOn('/api/users?page=2') - .body('{"users": [{"id": "3"}, {"id": "4"}], "hasMore": true}') - .header('X-Total-Count', '100') - .statusCodeOk() - // Page 3 - .whenGetOn('/api/users?page=3') - .body('{"users": [{"id": "5"}], "hasMore": false}') - .header('X-Total-Count', '100') - .statusCodeOk() - .mock(); - - Test.startTest(); - UserService service = new UserService(); - List allUsers = service.getAllUsersPaginated(); - Test.stopTest(); - - Assert.areEqual(5, allUsers.size()); -} -``` - -## Microservices Communication - -Mock calls to multiple microservices: - -```apex -@IsTest -static void testMicroservicesIntegration() { - new HttpMock() - // User Service - .whenGetOn('/user-service/api/users/123') - .body('{"id": "123", "name": "John"}') - .statusCodeOk() - // Order Service - .whenGetOn('/order-service/api/orders?userId=123') - .body('{"orders": [{"id": "order-1"}]}') - .statusCodeOk() - // Payment Service - .whenGetOn('/payment-service/api/payments?orderId=order-1') - .body('{"status": "paid"}') - .statusCodeOk() - .mock(); - - Test.startTest(); - OrderSummaryService service = new OrderSummaryService(); - OrderSummary summary = service.getOrderSummary('123'); - Test.stopTest(); - - Assert.areEqual('John', summary.userName); - Assert.areEqual(1, summary.orders.size()); -} -``` - -## Error and Success Mix - -Mix successful and error responses: - -```apex -@IsTest -static void testMixedResponses() { - new HttpMock() - .whenGetOn('/api/users') - .body('{"users": []}') - .statusCodeOk() - .whenPostOn('/api/invalid') - .body('{"error": "Bad Request"}') - .statusCodeBadRequest() - .whenGetOn('/api/notfound') - .body('{"error": "Not Found"}') - .statusCodeNotFound() - .mock(); - - Test.startTest(); - ApiService service = new ApiService(); - - // This should succeed - List users = service.getUsers(); - Assert.isNotNull(users); - - // This should fail - try { - service.postInvalid(); - Assert.fail('Expected exception'); - } catch (Exception e) { - Assert.isTrue(e.getMessage().contains('Bad Request')); - } - Test.stopTest(); -} -``` - -## Best Practices - -1. **Group Related Endpoints** - Mock all endpoints needed for one workflow together - -2. **Maintain Call Order** - Ensure your mocks are defined in the order they'll be called - -3. **Use Request Count** - Verify endpoints were called the expected number of times - -4. **Test Edge Cases** - Include both success and error scenarios - -## See Also - -- [Basic Examples](/examples/basic) -- [Error Handling](/examples/error-handling) -- [API Reference](/api/) diff --git a/website/index.md b/website/index.md index 8baf188..8cb9d25 100644 --- a/website/index.md +++ b/website/index.md @@ -19,27 +19,27 @@ hero: features: - icon: 🎯 title: Fluent API - details: Write clean, readable test mocks with a chainable, intuitive interface that makes your tests easy to understand. + details: Write clean, readable test mocks with a chainable, intuitive interface. - - icon: 🚀 - title: Production Ready - details: Battle-tested in production environments. Part of Apex Fluently suite of enterprise-grade Salesforce libraries. + - icon: 🌐 + title: All HTTP Methods + details: Mock GET, POST, PUT, PATCH, DELETE, HEAD, TRACE in one test. - - icon: 📝 - title: Type Safe - details: Strongly typed methods prevent runtime errors and provide excellent IDE autocomplete support. + - icon: 📦 + title: Flexible Responses + details: Return String, Object, or Blob bodies with JSON, XML, CSV, PDF, or custom content types. - - icon: 🔧 - title: Flexible - details: Mock multiple endpoints, HTTP methods, status codes, headers, and content types all in one test. + - icon: 🔢 + title: Built-in Status Codes + details: Use semantic methods like statusCodeOk(), statusCodeNotFound(), statusCodeCreated(). - icon: ⚡ - title: Lightweight - details: Zero dependencies, minimal footprint. Just one class to mock all your HTTP callouts. + title: Zero Dependencies + details: Lightweight, minimal footprint. Just one class to mock all your HTTP callouts. - icon: 🎓 title: Easy to Learn - details: Straightforward API inspired by modern testing frameworks. Get started in minutes. + details: Get started in minutes with a straightforward, modern API. --- ## Why HTTP Mock Lib? @@ -49,7 +49,7 @@ Traditional Salesforce HTTP mocking requires creating verbose mock classes for e ::: code-group ```apex [Before ❌] -@isTest +@IsTest global class MockHttpResponseGenerator implements HttpCalloutMock { global HTTPResponse respond(HTTPRequest req) { HttpResponse res = new HttpResponse(); @@ -73,43 +73,6 @@ new HttpMock() ::: -## Quick Example - -```apex -@IsTest -private class CalloutServiceTest { - @IsTest - static void testMultipleEndpoints() { - // Arrange - new HttpMock() - .whenGetOn('/api/v1/authorize') - .body('{ "token": "aZ3Xb7Qk" }') - .statusCodeOk() - .whenPostOn('/api/v1/create') - .body('{ "success": true, "message": null }') - .statusCodeCreated() - .mock(); - - // Act - Test.startTest(); - CalloutResult result = new CalloutService().makeCallout(); - Test.stopTest(); - - // Assert - Assert.isTrue(result.success); - } -} -``` - -## Features at a Glance - -- ✅ **Mock Multiple Endpoints** - Handle GET, POST, PUT, PATCH, DELETE, HEAD, TRACE in one test -- ✅ **Flexible Response Bodies** - Return String, Object, or Blob responses -- ✅ **Built-in Status Codes** - Use semantic methods like `statusCodeOk()`, `statusCodeNotFound()` -- ✅ **Content Type Support** - JSON, XML, CSV, PDF, and custom content types -- ✅ **Custom Headers** - Add any headers your callout needs -- ✅ **Zero Configuration** - Works out of the box, no setup required - ## Part of Apex Fluently HTTP Mock Lib is part of [Apex Fluently](https://apexfluently.beyondthecloud.dev/), a suite of production-ready Salesforce libraries by [Beyond the Cloud](https://beyondthecloud.dev). From 52d43152db385f496d239c5e6b029fc341acca2e Mon Sep 17 00:00:00 2001 From: Piotr PG Gajek Date: Fri, 26 Dec 2025 22:05:21 +0100 Subject: [PATCH 7/8] documentation --- website/getting-started.md | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/website/getting-started.md b/website/getting-started.md index 32b0451..6167aa1 100644 --- a/website/getting-started.md +++ b/website/getting-started.md @@ -129,17 +129,4 @@ try { Assert.isTrue(e.getMessage().contains('Unauthorized')); } Test.stopTest(); -``` - -## Next Steps - -- Learn about [HTTP Methods](/api/http-methods) -- Explore [Status Codes](/api/status-codes) -- See [Complete Examples](/examples/basic) -- Review the [Full API Reference](/api/) - -## Need Help? - -- Check the [API Reference](/api/) -- Browse [Examples](/examples/basic) -- Report issues on [GitHub](https://github.com/beyond-the-cloud-dev/http-mock-lib/issues) +``` \ No newline at end of file From 0dcbb03dab0a4acad3aa325d5119562f2943d8ce Mon Sep 17 00:00:00 2001 From: Piotr PG Gajek Date: Fri, 26 Dec 2025 22:14:31 +0100 Subject: [PATCH 8/8] readme --- README.md | 101 +++++---------------------------------- website/release.md | 115 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 89 deletions(-) create mode 100644 website/release.md diff --git a/README.md b/README.md index 2de5f09..fce69cb 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,14 @@

HTTP Mock Lib

Beyond The Cloud logo -API version +API version License [![CI](https://github.com/beyond-the-cloud-dev/http-mock-lib/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/beyond-the-cloud-dev/http-mock-lib/actions/workflows/ci.yml) -[![codecov](https://codecov.io/gh/beyond-the-cloud-dev/http-mock-lib/branch/main/graph/badge.svg)](https://codecov.io/gh/beyond-the-cloud-dev/http-mock-lib) +# Getting Started + HTTP Mock inspired by Robert Sösemann’s [Apex Http Mock](https://github.com/rsoesemann/apex-httpmock). HTTP Mock Lib is part of [Apex Fluently](https://apexfluently.beyondthecloud.dev/), a suite of production-ready Salesforce libraries by Beyond the Cloud. @@ -113,95 +114,17 @@ public interface HttpMockLib { } ``` -## Features - -### Mock different HTTP methods - -Mock different HTTP methods in the same test method. - -```java -new HttpMock() - .whenGetOn('/api/v1/authorize').body('{ "token": "aZ3Xb7Qk" }').statusCodeOk() - .whenPostOn('/api/v1/create').body('{ "success": true, "message": null }').statusCodeOk() - .mock(); -``` - -Supported methods: -- GET -- POST -- PUT -- PATCH -- DELETE -- TRACE -- HEAD +## Documentation -### Return different body +Visit the [documentation](https://httpmock.beyondthecloud.dev/) to view the full documentation. -```java -new HttpMock() - .whenGetOn('/api/v1').body(new Map{ 'token' => 'aZ3Xb7Qk' }).statusCodeOk() - .mock(); -``` - -Mock HTTP response body by using the following methods: +## Contributors -```java -HttpMock body(Object body); -HttpMock body(String body); -HttpMock body(Blob body); -``` + + + -### Use built-in Content Types +## License notes: -Use different content types. By default, the content type is set to `application/json`. - -```java -HttpMock contentTypePlainText(); // text/plain -HttpMock contentTypeHtml(); // text/html -HttpMock contentTypeCsv(); // text/csv -HttpMock contentTypeJson(); // application/json -HttpMock contentTypeXml(); // application/xml -HttpMock contentTypePdf(); // application/pdf -HttpMock contentTypeFormUrlencoded(); // application/x-www-form-urlencoded - -HttpMock contentType(String contentType); -``` - -Use `contentType(String contentType)` to set your own content type. - -### Use built-in Status Codes - -Use different status codes. By default, the status code is set to 200 (OK). - -Available status codes: - -```java -HttpMock statusCodeOk(); // 200 -HttpMock statusCodeCreated(); // 201 -HttpMock statusCodeAccepted(); // 202 -HttpMock statusCodeNoContent(); // 204 -HttpMock statusCodeBadRequest(); // 400 -HttpMock statusCodeUnauthorized(); // 401 -HttpMock statusCodeForbidden(); // 403 -HttpMock statusCodeNotFound(); // 404 -HttpMock statusCodeMethodNotAllowed(); // 405 -HttpMock statusCodeInternalServerError(); // 500 -HttpMock statusCodeNotImplemented(); // 501 -HttpMock statusCodeBadGateway(); // 502 -HttpMock statusCodeServiceUnavailable(); // 503 -HttpMock statusCodeGatewayTimeout(); // 504 - -HttpMock statusCode(Integer statusCode); -``` - -Use `statusCode(Integer statusCode)` to set your own status code. - -### Set custom headers - -Set response headers using the `header(String key, String value)` method. - -```java -new HttpMock() - .whenGetOn('/api/v1').body('{ "token": "aZ3Xb7Qk" }').header('Cache-Control', 'no-cache') - .mock(); -``` +- For proper license management each repository should contain LICENSE file similar to this one. +- each original class should contain copyright mark: © Copyright 2025, Beyond The Cloud Sp. z o.o. (BeyondTheCloud.Dev) diff --git a/website/release.md b/website/release.md new file mode 100644 index 0000000..684c24b --- /dev/null +++ b/website/release.md @@ -0,0 +1,115 @@ +# v1.2.0 - 26-December-2025 + +**Scope** + +- New Features: Static Resource Support, Enhanced Request Assertions API +- Improvements: Global Access Modifier, Parallel Test Execution, Unlocked Package Support + +`HttpMock` + +- Added `staticResource()` method for loading response body from Static Resources +- Added `requestsTo()` method with fluent assertion API for request counting +- Changed class access from `public` to `global` for managed package support +- Added unlocked package distribution with `btcdev` namespace + +## New Features + +### Static Resource Support + +New `staticResource()` method allows loading response body directly from a Salesforce Static Resource. This is ideal for large or complex response payloads that are difficult to maintain inline. + +**Example: Mock Using Static Resource** + +```apex +new HttpMock() + .whenGetOn('/api/v1/users') + .staticResource('UsersResponseMock') + .statusCodeOk() + .mock(); +``` + +If the Static Resource doesn't exist, a `StaticResourceNotFoundException` is thrown with a clear error message. + +### Enhanced Request Assertions API + +New `requestsTo()` method provides a fluent API for asserting the number of HTTP requests made during a test. This replaces the previous `getRequestCount()` method with a more intuitive interface. + +**Example: Assert Request Counts** + +```apex +new HttpMock() + .whenGetOn('/api/v1/authorize') + .statusCodeOk() + .whenPostOn('/api/v1/create') + .statusCodeOk() + .mock(); + +Test.startTest(); +// Make callouts... +Test.stopTest(); + +Assert.areEqual(1, HttpMock.requestsTo('/api/v1/authorize').get(), 'One GET request should be made'); +Assert.areEqual(1, HttpMock.requestsTo('/api/v1/create').post(), 'One POST request should be made'); +``` + +**Supported Assertion Methods** + +```apex +HttpMock.requestsTo('/endpoint').all(); // Total requests (all methods) +HttpMock.requestsTo('/endpoint').get(); // GET requests +HttpMock.requestsTo('/endpoint').post(); // POST requests +HttpMock.requestsTo('/endpoint').put(); // PUT requests +HttpMock.requestsTo('/endpoint').patch(); // PATCH requests +HttpMock.requestsTo('/endpoint').deletex(); // DELETE requests (x suffix due to reserved keyword) +HttpMock.requestsTo('/endpoint').trace(); // TRACE requests +HttpMock.requestsTo('/endpoint').head(); // HEAD requests +``` + +## Improvements + +### Global Access Modifier + +The `HttpMock` class is now declared as `global` instead of `public`, enabling usage in managed and unlocked packages. + +### Parallel Test Execution + +Test class now includes `@IsTest(IsParallel=true)` for faster test execution. + +### Unlocked Package Support + +HTTP Mock Lib is now available as an unlocked package with the `btcdev` namespace. See the [Installation Guide](/installation) for package installation instructions. + +### API Version Update + +Updated Salesforce API version from 57.0 to 65.0. + +## Internal Refactoring + +- Renamed interface from `HttpMockLib` to `HttpStubbing` +- All fluent methods now return `HttpStubbing` interface type +- Introduced `HttpMockRequests` inner class for improved request counting +- Refactored request tracking from method-based to endpoint-based storage +- Added PMD suppressions for `FieldDeclarationsShouldBeAtStart`, `CognitiveComplexity`, `CyclomaticComplexity` + +## 🚨 Breaking Changes 🚨 + +### Request Count API Change + +The `getRequestCount()` static method has been replaced with the new `requestsTo()` API. + +**Before (v1.1.x):** + +```apex +Assert.areEqual(2, HttpMock.getRequestCount('GET', '/api/v1')); +``` + +**After (v1.2.0):** + +```apex +Assert.areEqual(2, HttpMock.requestsTo('/api/v1').get()); +``` + +### Interface Rename + +The public interface has been renamed from `HttpMockLib` to `HttpStubbing`. This only affects code that explicitly references the interface type (uncommon). +