mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-28 11:17:07 +00:00
fix(license): reject activation when LS response is missing instance.id (#867)
LicenseService.activate() previously stored data.instance?.id || '' on success. If LS ever returned activated:true without an instance.id (malformed response, API change, transient bug), the user saw "Activated successfully" but every subsequent validate() and deactivate() short-circuited on the empty license_instance_id with a generic "no active license" error. The activation appeared to succeed while leaving the install in a broken state. After the existing catalog-id guard, also require a non-empty data.instance.id and reject up front with a retry-friendly message if missing. The check costs nothing on the happy path (LS has historically always returned instance.id on success) and turns a silent state divergence into a clear, actionable error. Adds two tests covering instance object absent and instance.id empty string. Both assert mockSetSystemState was never called, which catches any future code that accidentally writes state above the guard. Adds a block comment above initialize() explaining the dual-name relationship that confused the original audit: instance_id is the local UUID we pass to LS as instance_name, license_instance_id is the LS-issued activation id we pass back as instance_id on validate and deactivate. Same area, swapped names, no overlap.
This commit is contained in:
@@ -152,6 +152,51 @@ describe('LicenseService.activate() - catalog ID guard', () => {
|
||||
expect(mockSetSystemState).not.toHaveBeenCalledWith('license_status', 'active');
|
||||
});
|
||||
|
||||
it('rejects activation when the LS response is missing the instance object', async () => {
|
||||
// Storing an empty license_instance_id would silently break later
|
||||
// validate() and deactivate() calls. Reject up front so the user
|
||||
// sees a clear error instead of a deceptive "Activated successfully".
|
||||
mockAxiosPost.mockResolvedValueOnce({
|
||||
data: {
|
||||
activated: true,
|
||||
license_key: { id: 1, status: 'active', key: 'k', activation_limit: 1, activation_usage: 1, created_at: '2026-01-01', expires_at: null },
|
||||
meta: {
|
||||
store_id: SENCHO_LS_STORE_ID,
|
||||
product_id: SENCHO_LS_PRODUCT_ID_SKIPPER,
|
||||
variant_id: VARIANT_SKIPPER_MONTHLY,
|
||||
variant_name: 'Skipper Monthly',
|
||||
product_name: 'Sencho Skipper',
|
||||
},
|
||||
// instance: omitted on purpose
|
||||
},
|
||||
});
|
||||
const result = await svc.activate('NO-INSTANCE-OBJECT-KEY');
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('License server returned an incomplete activation. Please try again.');
|
||||
expect(mockSetSystemState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects activation when LS returns instance with empty id', async () => {
|
||||
mockAxiosPost.mockResolvedValueOnce({
|
||||
data: {
|
||||
activated: true,
|
||||
license_key: { id: 1, status: 'active', key: 'k', activation_limit: 1, activation_usage: 1, created_at: '2026-01-01', expires_at: null },
|
||||
instance: { id: '', name: 'test', created_at: '2026-01-01' },
|
||||
meta: {
|
||||
store_id: SENCHO_LS_STORE_ID,
|
||||
product_id: SENCHO_LS_PRODUCT_ID_ADMIRAL,
|
||||
variant_id: VARIANT_ADMIRAL_LIFETIME,
|
||||
variant_name: 'Admiral Lifetime',
|
||||
product_name: 'Sencho Admiral',
|
||||
},
|
||||
},
|
||||
});
|
||||
const result = await svc.activate('EMPTY-INSTANCE-ID-KEY');
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('License server returned an incomplete activation. Please try again.');
|
||||
expect(mockSetSystemState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('writes nothing to system_state when the catalog guard rejects', async () => {
|
||||
// Stronger than checking individual keys: any future code that adds a
|
||||
// setSystemState() call above the guard would silently break the
|
||||
|
||||
Reference in New Issue
Block a user