248 lines
9.4 KiB
OpenEdge ABL
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');
|
|
}
|
|
} |