/** * @description Test class for DocusignAPIService * @author Paul Huliganga * @date 2026-02-23 */ @isTest private class DocusignAPIServiceTest { /** * @description Mock HTTP callout */ private class DocusignMock implements HttpCalloutMock { private Integer statusCode; private String responseBody; private Integer callCount = 0; public DocusignMock(Integer statusCode, String responseBody) { this.statusCode = statusCode; this.responseBody = responseBody; } public HTTPResponse respond(HTTPRequest req) { callCount++; HttpResponse res = new HttpResponse(); res.setStatusCode(this.statusCode); res.setBody(this.responseBody); res.setStatus(this.statusCode == 201 ? 'Created' : 'Error'); return res; } } /** * @description Mock that fails first time, succeeds second (for retry testing) */ private class RetryMock implements HttpCalloutMock { private Integer callCount = 0; public HTTPResponse respond(HTTPRequest req) { callCount++; HttpResponse res = new HttpResponse(); if (callCount == 1) { // First call fails with 500 res.setStatusCode(500); res.setBody('{"errorCode":"INTERNAL_ERROR","message":"Server error"}'); res.setStatus('Internal Server Error'); } else { // Second call succeeds res.setStatusCode(201); res.setBody('{"envelopeId":"envelope-retry-success","status":"sent"}'); res.setStatus('Created'); } return res; } } /** * @description Test successful envelope creation */ @isTest static void testSuccessfulEnvelopeCreation() { // Arrange String envelopeId = 'envelope-test-12345'; String mockResponse = '{"envelopeId":"' + envelopeId + '","status":"sent"}'; Test.setMock(HttpCalloutMock.class, new DocusignMock(201, mockResponse)); DocusignCredentials.setTestCredentials('test-account-id', 'callout:DocusignAPI', 'test-token'); DocusignCredentials creds = DocusignCredentials.getInstance(); String envelopeJSON = '{"status":"sent","compositeTemplates":[]}'; // Act Test.startTest(); String resultEnvelopeId = DocusignAPIService.createCompositeEnvelope(envelopeJSON, creds); Test.stopTest(); // Assert System.assertEquals(envelopeId, resultEnvelopeId, 'Should return envelope ID'); } /** * @description Test parseEnvelopeId method */ @isTest static void testParseEnvelopeId() { // Arrange HttpResponse res = new HttpResponse(); res.setStatusCode(201); res.setBody('{"envelopeId":"parsed-envelope-123","uri":"/envelopes/parsed-envelope-123"}'); // Act Test.startTest(); String envelopeId = DocusignAPIService.parseEnvelopeId(res); Test.stopTest(); // Assert System.assertEquals('parsed-envelope-123', envelopeId, 'Should parse envelope ID correctly'); } /** * @description Test parseEnvelopeId with missing ID */ @isTest static void testParseEnvelopeIdMissing() { // Arrange HttpResponse res = new HttpResponse(); res.setStatusCode(201); res.setBody('{"status":"sent"}'); // No envelopeId field // Act & Assert Test.startTest(); try { DocusignAPIService.parseEnvelopeId(res); System.assert(false, 'Should have thrown exception'); } catch (DocusignAPIService.CalloutException e) { System.assert(e.getMessage().contains('Envelope ID not found'), 'Should mention missing ID'); } Test.stopTest(); } /** * @description Test handleAPIError for 400 Bad Request */ @isTest static void testHandleAPIError400() { // Arrange HttpResponse res = new HttpResponse(); res.setStatusCode(400); res.setBody('{"errorCode":"INVALID_REQUEST_PARAMETER","message":"Invalid template ID"}'); // Act & Assert Test.startTest(); try { DocusignAPIService.handleAPIError(res); System.assert(false, 'Should have thrown exception'); } catch (DocusignAPIService.CalloutException e) { System.assert(e.getMessage().contains('400'), 'Should include status code'); System.assert(e.getMessage().contains('Invalid template'), 'Should include error message'); System.assert(e.getMessage().contains('template IDs'), 'Should include guidance'); } Test.stopTest(); } /** * @description Test handleAPIError for 401 Unauthorized */ @isTest static void testHandleAPIError401() { // Arrange HttpResponse res = new HttpResponse(); res.setStatusCode(401); res.setBody('{"errorCode":"USER_AUTHENTICATION_FAILED","message":"Invalid access token"}'); // Act & Assert Test.startTest(); try { DocusignAPIService.handleAPIError(res); System.assert(false, 'Should have thrown exception'); } catch (DocusignAPIService.CalloutException e) { System.assert(e.getMessage().contains('401'), 'Should include status code'); System.assert(e.getMessage().contains('Authentication'), 'Should mention authentication'); } Test.stopTest(); } /** * @description Test handleAPIError for 403 Forbidden */ @isTest static void testHandleAPIError403() { // Arrange HttpResponse res = new HttpResponse(); res.setStatusCode(403); res.setBody('{"errorCode":"USER_LACKS_PERMISSIONS","message":"User cannot send envelopes"}'); // Act & Assert Test.startTest(); try { DocusignAPIService.handleAPIError(res); System.assert(false, 'Should have thrown exception'); } catch (DocusignAPIService.CalloutException e) { System.assert(e.getMessage().contains('403'), 'Should include status code'); System.assert(e.getMessage().contains('permission'), 'Should mention permission'); } Test.stopTest(); } /** * @description Test handleAPIError for 404 Not Found */ @isTest static void testHandleAPIError404() { // Arrange HttpResponse res = new HttpResponse(); res.setStatusCode(404); res.setBody('{"errorCode":"RESOURCE_NOT_FOUND","message":"Template not found"}'); // Act & Assert Test.startTest(); try { DocusignAPIService.handleAPIError(res); System.assert(false, 'Should have thrown exception'); } catch (DocusignAPIService.CalloutException e) { System.assert(e.getMessage().contains('404'), 'Should include status code'); System.assert(e.getMessage().contains('not found'), 'Should mention template not found'); } Test.stopTest(); } /** * @description Test handleAPIError for 429 Rate Limit */ @isTest static void testHandleAPIError429() { // Arrange HttpResponse res = new HttpResponse(); res.setStatusCode(429); res.setBody('{"errorCode":"HOURLY_APIINVOCATION_LIMIT_EXCEEDED","message":"Rate limit exceeded"}'); // Act & Assert Test.startTest(); try { DocusignAPIService.handleAPIError(res); System.assert(false, 'Should have thrown exception'); } catch (DocusignAPIService.CalloutException e) { System.assert(e.getMessage().contains('429'), 'Should include status code'); System.assert(e.getMessage().contains('rate limit'), 'Should mention rate limit'); } Test.stopTest(); } /** * @description Test handleAPIError for 500 Server Error */ @isTest static void testHandleAPIError500() { // Arrange HttpResponse res = new HttpResponse(); res.setStatusCode(500); res.setBody('{"errorCode":"INTERNAL_ERROR","message":"Server error"}'); // Act & Assert Test.startTest(); try { DocusignAPIService.handleAPIError(res); System.assert(false, 'Should have thrown exception'); } catch (DocusignAPIService.CalloutException e) { System.assert(e.getMessage().contains('500'), 'Should include status code'); System.assert(e.getMessage().contains('server error'), 'Should mention server error'); } Test.stopTest(); } /** * @description Test retry logic with transient failure */ @isTest static void testRetryLogic() { // Arrange Test.setMock(HttpCalloutMock.class, new RetryMock()); DocusignCredentials.setTestCredentials('test-account-id', 'callout:DocusignAPI', 'test-token'); DocusignCredentials creds = DocusignCredentials.getInstance(); String envelopeJSON = '{"status":"sent","compositeTemplates":[]}'; // Act Test.startTest(); String envelopeId = DocusignAPIService.createCompositeEnvelope(envelopeJSON, creds); Test.stopTest(); // Assert System.assertEquals('envelope-retry-success', envelopeId, 'Should succeed after retry'); } /** * @description Test buildCreateEnvelopeRequest */ @isTest static void testBuildCreateEnvelopeRequest() { // Arrange DocusignCredentials.setTestCredentials('test-account-id', 'callout:DocusignAPI', 'test-token'); DocusignCredentials creds = DocusignCredentials.getInstance(); String envelopeJSON = '{"status":"sent"}'; // Act Test.startTest(); HttpRequest req = DocusignAPIService.buildCreateEnvelopeRequest(envelopeJSON, creds); Test.stopTest(); // Assert System.assertEquals('POST', req.getMethod(), 'Should use POST method'); System.assert(req.getEndpoint().contains('/envelopes'), 'Should have envelopes endpoint'); System.assert(req.getHeader('Authorization').contains('Bearer'), 'Should have Bearer token'); System.assertEquals('application/json', req.getHeader('Content-Type'), 'Should set JSON content type'); System.assertEquals(envelopeJSON, req.getBody(), 'Should set body'); } /** * @description Test executeWithRetry with non-retryable error */ @isTest static void testExecuteWithRetryNonRetryable() { // Arrange String mockResponse = '{"errorCode":"INVALID_REQUEST_PARAMETER","message":"Bad request"}'; Test.setMock(HttpCalloutMock.class, new DocusignMock(400, mockResponse)); DocusignCredentials.setTestCredentials('test-account-id', 'callout:DocusignAPI', 'test-token'); DocusignCredentials creds = DocusignCredentials.getInstance(); HttpRequest req = DocusignAPIService.buildCreateEnvelopeRequest('{}', creds); // Act Test.startTest(); HttpResponse res = DocusignAPIService.executeWithRetry(req, 2); Test.stopTest(); // Assert System.assertEquals(400, res.getStatusCode(), 'Should return 400 without retry'); } }