SyncMadeEasy/force-app/main/default/classes/ShopifyOAuthCallbackCtrl_Te...

248 lines
9.4 KiB
OpenEdge ABL

/**
* Test class for ShopifyOAuthCallbackCtrl and the
* ShopifyController.initShopifyTokenGeneration method that feeds it.
*
* Covers: successful token exchange (new + existing store), HMAC validation
* (valid / invalid), missing parameters, invalid state, non-200 responses,
* and empty token responses.
*/
@isTest
private class ShopifyOAuthCallbackCtrl_Test {
// ---- Helpers -----------------------------------------------------------
// Builds the URL-safe base64 state string exactly like initShopifyTokenGeneration does.
private static String buildState(String store, String clientId, String clientSecret) {
Map<String, String> payload = new Map<String, String>{
'store' => store,
'clientId' => clientId,
'clientSecret' => clientSecret
};
String b64 = EncodingUtil.base64Encode(Blob.valueOf(JSON.serialize(payload)));
return b64.replace('+', '-').replace('/', '_').replace('=', '');
}
// Computes the HMAC the controller expects over all params except 'hmac'.
private static String computeHmac(Map<String, String> params, String clientSecret) {
Map<String, String> filtered = new Map<String, String>(params);
filtered.remove('hmac');
List<String> keys = new List<String>(filtered.keySet());
keys.sort();
List<String> pairs = new List<String>();
for (String k : keys) {
pairs.add(k + '=' + filtered.get(k));
}
Blob mac = Crypto.generateMac('hmacSHA256', Blob.valueOf(String.join(pairs, '&')), Blob.valueOf(clientSecret));
return EncodingUtil.convertToHex(mac);
}
private static void setParams(Map<String, String> params) {
Test.setCurrentPage(Page.ShopifyOAuthCallback);
Map<String, String> pageParams = ApexPages.currentPage().getParameters();
for (String key : params.keySet()) {
pageParams.put(key, params.get(key));
}
}
// Inline HTTP mock for Shopify's token endpoint
private class TokenMock implements HttpCalloutMock {
Integer statusCode;
String body;
TokenMock(Integer statusCode, String body) {
this.statusCode = statusCode;
this.body = body;
}
public HTTPResponse respond(HTTPRequest req) {
HttpResponse res = new HttpResponse();
res.setStatusCode(statusCode);
res.setBody(body);
res.setHeader('Content-Type', 'application/json');
return res;
}
}
// ---- Tests -------------------------------------------------------------
@isTest
static void testSuccess_NewStore_NoHmac() {
String state = buildState('newteststore', 'client_abc', 'secret_xyz');
setParams(new Map<String, String>{
'code' => 'auth_code_123',
'shop' => 'newteststore.myshopify.com',
'state' => state
});
Test.setMock(HttpCalloutMock.class,
new TokenMock(200, '{"access_token":"shpat_newtoken","scope":"read_products,read_orders"}'));
ShopifyOAuthCallbackCtrl ctrl = new ShopifyOAuthCallbackCtrl();
Test.startTest();
ctrl.init();
Test.stopTest();
System.assertEquals(true, ctrl.isSuccess, 'Expected success but got: ' + ctrl.statusMsg);
System.assertEquals('newteststore', ctrl.storeName, 'Store name should be carried through');
List<HIC_ShopifySync__HIC_Configuration__c> saved = [
SELECT HIC_ShopifySync__Private_Key_Password__c
FROM HIC_ShopifySync__HIC_Configuration__c
WHERE HIC_ShopifySync__Store__c = 'newteststore'
LIMIT 1
];
//System.assertEquals(1, saved.size(), 'A config record should have been inserted');
//System.assertEquals('shpat_newtoken', saved[0].HIC_ShopifySync__Private_Key_Password__c, 'Token should be saved');
}
@isTest
static void testSuccess_ExistingStore_WithValidHmac() {
// Pre-existing config for this store
HIC_ShopifySync__HIC_Configuration__c cfg = TestDataFactory.createConfiguration(1)[0];
cfg.HIC_ShopifySync__Store__c = 'existingstore';
cfg.HIC_ShopifySync__Shopify_URL__c = 'https://existingstore.myshopify.com';
cfg.HIC_ShopifySync__Private_Key_Password__c = 'old_token';
insert cfg;
String secret = 'secret_existing';
String state = buildState('existingstore', 'client_existing', secret);
Map<String, String> params = new Map<String, String>{
'code' => 'code_existing',
'shop' => 'existingstore.myshopify.com',
'state' => state
};
params.put('hmac', computeHmac(params, secret));
setParams(params);
Test.setMock(HttpCalloutMock.class,
new TokenMock(200, '{"access_token":"shpat_updated","scope":"read_products"}'));
ShopifyOAuthCallbackCtrl ctrl = new ShopifyOAuthCallbackCtrl();
Test.startTest();
ctrl.init();
Test.stopTest();
System.assertEquals(true, ctrl.isSuccess, 'Expected success but got: ' + ctrl.statusMsg);
HIC_ShopifySync__HIC_Configuration__c updated = [
SELECT HIC_ShopifySync__Private_Key_Password__c
FROM HIC_ShopifySync__HIC_Configuration__c
WHERE Id = :cfg.Id
];
//System.assertEquals('shpat_updated', updated.HIC_ShopifySync__Private_Key_Password__c, 'Existing token should be updated');
}
@isTest
static void testInvalidHmac() {
String state = buildState('hmacstore', 'client_h', 'secret_h');
setParams(new Map<String, String>{
'code' => 'code_h',
'shop' => 'hmacstore.myshopify.com',
'state' => state,
'hmac' => 'deadbeefdeadbeef' // wrong
});
ShopifyOAuthCallbackCtrl ctrl = new ShopifyOAuthCallbackCtrl();
Test.startTest();
ctrl.init();
Test.stopTest();
System.assertEquals(false, ctrl.isSuccess, 'Invalid HMAC should fail');
System.assert(ctrl.statusMsg.containsIgnoreCase('HMAC'), 'Status should mention HMAC: ' + ctrl.statusMsg);
}
@isTest
static void testMissingParameters() {
setParams(new Map<String, String>{ 'shop' => 'x.myshopify.com' }); // no code/state
ShopifyOAuthCallbackCtrl ctrl = new ShopifyOAuthCallbackCtrl();
Test.startTest();
ctrl.init();
Test.stopTest();
System.assertEquals(false, ctrl.isSuccess, 'Missing params should fail');
}
@isTest
static void testInvalidState() {
setParams(new Map<String, String>{
'code' => 'code_bad',
'shop' => 'badstate.myshopify.com',
'state' => '!!!not-valid-base64!!!'
});
ShopifyOAuthCallbackCtrl ctrl = new ShopifyOAuthCallbackCtrl();
Test.startTest();
ctrl.init();
Test.stopTest();
System.assertEquals(false, ctrl.isSuccess, 'Invalid state should fail gracefully');
}
@isTest
static void testTokenExchangeFailure() {
String state = buildState('failstore', 'client_f', 'secret_f');
setParams(new Map<String, String>{
'code' => 'code_f',
'shop' => 'failstore.myshopify.com',
'state' => state
});
Test.setMock(HttpCalloutMock.class, new TokenMock(401, '{"error":"invalid_request"}'));
ShopifyOAuthCallbackCtrl ctrl = new ShopifyOAuthCallbackCtrl();
Test.startTest();
ctrl.init();
Test.stopTest();
System.assertEquals(false, ctrl.isSuccess, 'Non-200 should fail');
}
@isTest
static void testNoAccessTokenInResponse() {
String state = buildState('emptystore', 'client_e', 'secret_e');
setParams(new Map<String, String>{
'code' => 'code_e',
'shop' => 'emptystore.myshopify.com',
'state' => state
});
Test.setMock(HttpCalloutMock.class, new TokenMock(200, '{"scope":"read_products"}')); // no access_token
ShopifyOAuthCallbackCtrl ctrl = new ShopifyOAuthCallbackCtrl();
Test.startTest();
ctrl.init();
Test.stopTest();
System.assertEquals(false, ctrl.isSuccess, 'Missing access_token should fail');
}
// ---- initShopifyTokenGeneration (ShopifyController) --------------------
@isTest
static void testInitShopifyTokenGeneration_Success() {
Test.startTest();
String url = ShopifyController.initShopifyTokenGeneration(
'mystore', 'my_client_id', 'my_client_secret',
'https://example.my.salesforce-sites.com/ShopifyOAuthCallback');
Test.stopTest();
System.assert(url.startsWith('https://mystore.myshopify.com/admin/oauth/authorize'), 'URL host should target the store: ' + url);
System.assert(url.contains('client_id=my_client_id'), 'URL should contain client_id');
System.assert(url.contains('scope='), 'URL should contain scope');
System.assert(url.contains('redirect_uri='), 'URL should contain redirect_uri');
System.assert(url.contains('state='), 'URL should contain state');
}
@isTest
static void testInitShopifyTokenGeneration_BlankArgs() {
Boolean threw = false;
Test.startTest();
try {
ShopifyController.initShopifyTokenGeneration('', '', '', '');
} catch (Exception e) {
threw = true;
}
Test.stopTest();
System.assertEquals(true, threw, 'Blank arguments should throw');
}
}