feat(uptelligence): add AltumCode vendor update checks
Add automated version checking for AltumCode products and plugins: - isAltumPlatform() routes altum vendors before OSS checks - checkAltumProduct() fetches latest_release_version from product info.php - checkAltumPlugin() looks up plugin versions from dev.altumcode.com - In-memory cache avoids redundant HTTP calls for multiple plugins 14 Pest tests covering all paths (43 assertions). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
cc1918b511
commit
f0a2f3fd1a
2 changed files with 496 additions and 2 deletions
|
|
@ -6,6 +6,8 @@ namespace Core\Mod\Uptelligence\Services;
|
|||
|
||||
use Core\Mod\Uptelligence\Models\UpstreamTodo;
|
||||
use Core\Mod\Uptelligence\Models\Vendor;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
|
|
@ -17,6 +19,13 @@ use Illuminate\Support\Facades\RateLimiter;
|
|||
*/
|
||||
class VendorUpdateCheckerService
|
||||
{
|
||||
/**
|
||||
* In-memory cache for AltumCode plugin versions.
|
||||
*
|
||||
* Avoids redundant HTTP calls when checking multiple plugins in one run.
|
||||
*/
|
||||
protected ?array $altumPluginVersionsCache = null;
|
||||
|
||||
/**
|
||||
* Check all active vendors for updates.
|
||||
*
|
||||
|
|
@ -42,6 +51,8 @@ class VendorUpdateCheckerService
|
|||
{
|
||||
// Determine check method based on source type and git URL
|
||||
$result = match (true) {
|
||||
$this->isAltumPlatform($vendor) && $vendor->isLicensed() => $this->checkAltumProduct($vendor),
|
||||
$this->isAltumPlatform($vendor) && $vendor->isPlugin() => $this->checkAltumPlugin($vendor),
|
||||
$vendor->isOss() && $this->isGitHubUrl($vendor->git_repo_url) => $this->checkGitHub($vendor),
|
||||
$vendor->isOss() && $this->isGiteaUrl($vendor->git_repo_url) => $this->checkGitea($vendor),
|
||||
default => $this->skipCheck($vendor),
|
||||
|
|
@ -89,10 +100,10 @@ class VendorUpdateCheckerService
|
|||
->retry(3, function (int $attempt) {
|
||||
return (int) pow(2, $attempt - 1) * 1000;
|
||||
}, function (\Exception $exception) {
|
||||
if ($exception instanceof \Illuminate\Http\Client\ConnectionException) {
|
||||
if ($exception instanceof ConnectionException) {
|
||||
return true;
|
||||
}
|
||||
if ($exception instanceof \Illuminate\Http\Client\RequestException) {
|
||||
if ($exception instanceof RequestException) {
|
||||
$status = $exception->response?->status();
|
||||
|
||||
return $status >= 500 || $status === 429;
|
||||
|
|
@ -273,6 +284,125 @@ class VendorUpdateCheckerService
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if vendor belongs to the AltumCode platform.
|
||||
*/
|
||||
protected function isAltumPlatform(Vendor $vendor): bool
|
||||
{
|
||||
return $vendor->plugin_platform === Vendor::PLATFORM_ALTUM;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check an AltumCode licensed product for updates.
|
||||
*
|
||||
* Fetches the product's info.php endpoint (e.g. https://66analytics.com/info.php)
|
||||
* and reads the latest_release_version from the JSON response.
|
||||
*/
|
||||
protected function checkAltumProduct(Vendor $vendor): array
|
||||
{
|
||||
$url = "https://{$vendor->slug}.com/info.php";
|
||||
|
||||
try {
|
||||
$response = Http::timeout(5)->get($url);
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('Uptelligence: AltumCode product check failed', [
|
||||
'vendor' => $vendor->slug,
|
||||
'url' => $url,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return $this->errorResult("AltumCode product check failed: {$e->getMessage()}");
|
||||
}
|
||||
|
||||
if (! $response->successful()) {
|
||||
Log::warning('Uptelligence: AltumCode product info request failed', [
|
||||
'vendor' => $vendor->slug,
|
||||
'status' => $response->status(),
|
||||
]);
|
||||
|
||||
return $this->errorResult("AltumCode product info error: {$response->status()}");
|
||||
}
|
||||
|
||||
$data = $response->json();
|
||||
$latestVersion = $this->normaliseVersion($data['latest_release_version'] ?? '');
|
||||
|
||||
return $this->buildResult(
|
||||
vendor: $vendor,
|
||||
latestVersion: $latestVersion,
|
||||
releaseInfo: [
|
||||
'source' => 'altum_product',
|
||||
'url' => $url,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check an AltumCode plugin for updates.
|
||||
*
|
||||
* Fetches the plugin versions endpoint and looks up the plugin by ID
|
||||
* (stripping the 'altum-plugin-' prefix from the vendor slug).
|
||||
*/
|
||||
protected function checkAltumPlugin(Vendor $vendor): array
|
||||
{
|
||||
$versions = $this->getAltumPluginVersions();
|
||||
|
||||
if ($versions === null) {
|
||||
return $this->errorResult('Failed to fetch AltumCode plugin versions');
|
||||
}
|
||||
|
||||
// Strip 'altum-plugin-' prefix from slug to get the plugin ID
|
||||
$pluginId = str_replace('altum-plugin-', '', $vendor->slug);
|
||||
|
||||
if (! isset($versions[$pluginId])) {
|
||||
return $this->errorResult("Plugin '{$pluginId}' not found in AltumCode registry");
|
||||
}
|
||||
|
||||
$latestVersion = $this->normaliseVersion($versions[$pluginId]['version'] ?? '');
|
||||
|
||||
return $this->buildResult(
|
||||
vendor: $vendor,
|
||||
latestVersion: $latestVersion,
|
||||
releaseInfo: [
|
||||
'source' => 'altum_plugin',
|
||||
'plugin_id' => $pluginId,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch AltumCode plugin versions with in-memory caching.
|
||||
*
|
||||
* @return array<string, array{version: string}>|null
|
||||
*/
|
||||
protected function getAltumPluginVersions(): ?array
|
||||
{
|
||||
if ($this->altumPluginVersionsCache !== null) {
|
||||
return $this->altumPluginVersionsCache;
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::timeout(5)->get('https://dev.altumcode.com/plugins-versions');
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('Uptelligence: AltumCode plugin versions fetch failed', [
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (! $response->successful()) {
|
||||
Log::warning('Uptelligence: AltumCode plugin versions request failed', [
|
||||
'status' => $response->status(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->altumPluginVersionsCache = $response->json() ?? [];
|
||||
|
||||
return $this->altumPluginVersionsCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip check for vendors that don't support auto-checking.
|
||||
*/
|
||||
|
|
|
|||
364
tests/Unit/AltumCodeCheckerTest.php
Normal file
364
tests/Unit/AltumCodeCheckerTest.php
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Core\Mod\Uptelligence\Models\Vendor;
|
||||
use Core\Mod\Uptelligence\Services\VendorUpdateCheckerService;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
beforeEach(function () {
|
||||
$this->service = new VendorUpdateCheckerService;
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// isAltumPlatform
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('identifies altum platform vendors', function () {
|
||||
$vendor = new Vendor([
|
||||
'slug' => '66analytics',
|
||||
'plugin_platform' => Vendor::PLATFORM_ALTUM,
|
||||
]);
|
||||
|
||||
$method = new ReflectionMethod($this->service, 'isAltumPlatform');
|
||||
$method->setAccessible(true);
|
||||
|
||||
expect($method->invoke($this->service, $vendor))->toBeTrue();
|
||||
});
|
||||
|
||||
it('rejects non-altum platform vendors', function () {
|
||||
$vendor = new Vendor([
|
||||
'slug' => 'some-wp-plugin',
|
||||
'plugin_platform' => Vendor::PLATFORM_WORDPRESS,
|
||||
]);
|
||||
|
||||
$method = new ReflectionMethod($this->service, 'isAltumPlatform');
|
||||
$method->setAccessible(true);
|
||||
|
||||
expect($method->invoke($this->service, $vendor))->toBeFalse();
|
||||
});
|
||||
|
||||
it('rejects vendors with no platform set', function () {
|
||||
$vendor = new Vendor([
|
||||
'slug' => 'some-oss',
|
||||
'plugin_platform' => null,
|
||||
]);
|
||||
|
||||
$method = new ReflectionMethod($this->service, 'isAltumPlatform');
|
||||
$method->setAccessible(true);
|
||||
|
||||
expect($method->invoke($this->service, $vendor))->toBeFalse();
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// checkAltumProduct
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('fetches latest version from altum product info endpoint', function () {
|
||||
Http::fake([
|
||||
'https://66analytics.com/info.php' => Http::response([
|
||||
'latest_release_version' => '42.0.0',
|
||||
'product_name' => '66analytics',
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$vendor = new Vendor([
|
||||
'slug' => '66analytics',
|
||||
'name' => '66analytics',
|
||||
'plugin_platform' => Vendor::PLATFORM_ALTUM,
|
||||
'source_type' => Vendor::SOURCE_LICENSED,
|
||||
'current_version' => '41.0.0',
|
||||
]);
|
||||
|
||||
$method = new ReflectionMethod($this->service, 'checkAltumProduct');
|
||||
$method->setAccessible(true);
|
||||
|
||||
$result = $method->invoke($this->service, $vendor);
|
||||
|
||||
expect($result)
|
||||
->toBeArray()
|
||||
->and($result['status'])->toBe('success')
|
||||
->and($result['latest'])->toBe('42.0.0')
|
||||
->and($result['has_update'])->toBeTrue()
|
||||
->and($result['release_info']['source'])->toBe('altum_product');
|
||||
});
|
||||
|
||||
it('detects no update when product version is current', function () {
|
||||
Http::fake([
|
||||
'https://66biolinks.com/info.php' => Http::response([
|
||||
'latest_release_version' => '39.0.0',
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$vendor = new Vendor([
|
||||
'slug' => '66biolinks',
|
||||
'name' => '66biolinks',
|
||||
'plugin_platform' => Vendor::PLATFORM_ALTUM,
|
||||
'source_type' => Vendor::SOURCE_LICENSED,
|
||||
'current_version' => '39.0.0',
|
||||
]);
|
||||
|
||||
$method = new ReflectionMethod($this->service, 'checkAltumProduct');
|
||||
$method->setAccessible(true);
|
||||
|
||||
$result = $method->invoke($this->service, $vendor);
|
||||
|
||||
expect($result['status'])->toBe('success')
|
||||
->and($result['has_update'])->toBeFalse()
|
||||
->and($result['latest'])->toBe('39.0.0');
|
||||
});
|
||||
|
||||
it('returns error when altum product endpoint fails', function () {
|
||||
Http::fake([
|
||||
'https://66analytics.com/info.php' => Http::response('Server Error', 500),
|
||||
]);
|
||||
|
||||
$vendor = new Vendor([
|
||||
'slug' => '66analytics',
|
||||
'name' => '66analytics',
|
||||
'plugin_platform' => Vendor::PLATFORM_ALTUM,
|
||||
'source_type' => Vendor::SOURCE_LICENSED,
|
||||
'current_version' => '40.0.0',
|
||||
]);
|
||||
|
||||
$method = new ReflectionMethod($this->service, 'checkAltumProduct');
|
||||
$method->setAccessible(true);
|
||||
|
||||
$result = $method->invoke($this->service, $vendor);
|
||||
|
||||
expect($result['status'])->toBe('error')
|
||||
->and($result['message'])->toContain('500');
|
||||
});
|
||||
|
||||
it('returns error when altum product has no version in response', function () {
|
||||
Http::fake([
|
||||
'https://66pusher.com/info.php' => Http::response([
|
||||
'product_name' => '66pusher',
|
||||
// No latest_release_version key
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$vendor = new Vendor([
|
||||
'slug' => '66pusher',
|
||||
'name' => '66pusher',
|
||||
'plugin_platform' => Vendor::PLATFORM_ALTUM,
|
||||
'source_type' => Vendor::SOURCE_LICENSED,
|
||||
'current_version' => '5.0.0',
|
||||
]);
|
||||
|
||||
$method = new ReflectionMethod($this->service, 'checkAltumProduct');
|
||||
$method->setAccessible(true);
|
||||
|
||||
$result = $method->invoke($this->service, $vendor);
|
||||
|
||||
expect($result['status'])->toBe('error')
|
||||
->and($result['message'])->toContain('Could not determine latest version');
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// checkAltumPlugin
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('fetches latest version for an altum plugin', function () {
|
||||
Http::fake([
|
||||
'https://dev.altumcode.com/plugins-versions' => Http::response([
|
||||
'affiliate' => ['version' => '2.0.1'],
|
||||
'email-notifications' => ['version' => '3.1.0'],
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$vendor = new Vendor([
|
||||
'slug' => 'altum-plugin-affiliate',
|
||||
'name' => 'AltumCode Affiliate Plugin',
|
||||
'plugin_platform' => Vendor::PLATFORM_ALTUM,
|
||||
'source_type' => Vendor::SOURCE_PLUGIN,
|
||||
'current_version' => '1.5.0',
|
||||
]);
|
||||
|
||||
$method = new ReflectionMethod($this->service, 'checkAltumPlugin');
|
||||
$method->setAccessible(true);
|
||||
|
||||
$result = $method->invoke($this->service, $vendor);
|
||||
|
||||
expect($result['status'])->toBe('success')
|
||||
->and($result['latest'])->toBe('2.0.1')
|
||||
->and($result['has_update'])->toBeTrue()
|
||||
->and($result['release_info']['source'])->toBe('altum_plugin')
|
||||
->and($result['release_info']['plugin_id'])->toBe('affiliate');
|
||||
});
|
||||
|
||||
it('returns error when plugin is not found in altum registry', function () {
|
||||
Http::fake([
|
||||
'https://dev.altumcode.com/plugins-versions' => Http::response([
|
||||
'affiliate' => ['version' => '2.0.1'],
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$vendor = new Vendor([
|
||||
'slug' => 'altum-plugin-nonexistent',
|
||||
'name' => 'Nonexistent Plugin',
|
||||
'plugin_platform' => Vendor::PLATFORM_ALTUM,
|
||||
'source_type' => Vendor::SOURCE_PLUGIN,
|
||||
'current_version' => '1.0.0',
|
||||
]);
|
||||
|
||||
$method = new ReflectionMethod($this->service, 'checkAltumPlugin');
|
||||
$method->setAccessible(true);
|
||||
|
||||
$result = $method->invoke($this->service, $vendor);
|
||||
|
||||
expect($result['status'])->toBe('error')
|
||||
->and($result['message'])->toContain('nonexistent')
|
||||
->and($result['message'])->toContain('not found');
|
||||
});
|
||||
|
||||
it('returns error when plugin versions endpoint fails', function () {
|
||||
Http::fake([
|
||||
'https://dev.altumcode.com/plugins-versions' => Http::response('Bad Gateway', 502),
|
||||
]);
|
||||
|
||||
$vendor = new Vendor([
|
||||
'slug' => 'altum-plugin-affiliate',
|
||||
'name' => 'AltumCode Affiliate Plugin',
|
||||
'plugin_platform' => Vendor::PLATFORM_ALTUM,
|
||||
'source_type' => Vendor::SOURCE_PLUGIN,
|
||||
'current_version' => '1.0.0',
|
||||
]);
|
||||
|
||||
$method = new ReflectionMethod($this->service, 'checkAltumPlugin');
|
||||
$method->setAccessible(true);
|
||||
|
||||
$result = $method->invoke($this->service, $vendor);
|
||||
|
||||
expect($result['status'])->toBe('error')
|
||||
->and($result['message'])->toContain('Failed to fetch');
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// In-memory cache
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('caches plugin versions across multiple calls', function () {
|
||||
Http::fake([
|
||||
'https://dev.altumcode.com/plugins-versions' => Http::response([
|
||||
'affiliate' => ['version' => '2.0.1'],
|
||||
'email-notifications' => ['version' => '3.1.0'],
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$method = new ReflectionMethod($this->service, 'getAltumPluginVersions');
|
||||
$method->setAccessible(true);
|
||||
|
||||
// First call — should hit the HTTP endpoint
|
||||
$first = $method->invoke($this->service);
|
||||
expect($first)->toBeArray()->toHaveKey('affiliate');
|
||||
|
||||
// Second call — should use cache (no additional HTTP request)
|
||||
$second = $method->invoke($this->service);
|
||||
expect($second)->toBe($first);
|
||||
|
||||
// Verify HTTP was only called once
|
||||
Http::assertSentCount(1);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Match routing via checkVendor (integration-level)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('routes altum licensed vendor through checkAltumProduct', function () {
|
||||
Http::fake([
|
||||
'https://66analytics.com/info.php' => Http::response([
|
||||
'latest_release_version' => '42.0.0',
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$vendor = Mockery::mock(Vendor::class)->makePartial();
|
||||
$vendor->shouldReceive('getAttribute')->with('slug')->andReturn('66analytics');
|
||||
$vendor->shouldReceive('getAttribute')->with('name')->andReturn('66analytics');
|
||||
$vendor->shouldReceive('getAttribute')->with('plugin_platform')->andReturn(Vendor::PLATFORM_ALTUM);
|
||||
$vendor->shouldReceive('getAttribute')->with('source_type')->andReturn(Vendor::SOURCE_LICENSED);
|
||||
$vendor->shouldReceive('getAttribute')->with('current_version')->andReturn('41.0.0');
|
||||
$vendor->shouldReceive('getAttribute')->with('git_repo_url')->andReturn(null);
|
||||
$vendor->shouldReceive('update')->once()->with(Mockery::on(function ($data) {
|
||||
return array_key_exists('last_checked_at', $data);
|
||||
}));
|
||||
$vendor->shouldReceive('isLicensed')->andReturn(true);
|
||||
$vendor->shouldReceive('isPlugin')->andReturn(false);
|
||||
$vendor->shouldReceive('isOss')->andReturn(false);
|
||||
|
||||
// Use a partial mock of the service to prevent createUpdateTodo from hitting DB
|
||||
$service = Mockery::mock(VendorUpdateCheckerService::class)->makePartial();
|
||||
$service->shouldAllowMockingProtectedMethods();
|
||||
$service->shouldReceive('createUpdateTodo')->once();
|
||||
|
||||
$result = $service->checkVendor($vendor);
|
||||
|
||||
expect($result['status'])->toBe('success')
|
||||
->and($result['latest'])->toBe('42.0.0')
|
||||
->and($result['has_update'])->toBeTrue();
|
||||
|
||||
Http::assertSent(function ($request) {
|
||||
return $request->url() === 'https://66analytics.com/info.php';
|
||||
});
|
||||
});
|
||||
|
||||
it('routes altum plugin vendor through checkAltumPlugin', function () {
|
||||
Http::fake([
|
||||
'https://dev.altumcode.com/plugins-versions' => Http::response([
|
||||
'affiliate' => ['version' => '2.0.1'],
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$vendor = Mockery::mock(Vendor::class)->makePartial();
|
||||
$vendor->shouldReceive('getAttribute')->with('slug')->andReturn('altum-plugin-affiliate');
|
||||
$vendor->shouldReceive('getAttribute')->with('name')->andReturn('Affiliate Plugin');
|
||||
$vendor->shouldReceive('getAttribute')->with('plugin_platform')->andReturn(Vendor::PLATFORM_ALTUM);
|
||||
$vendor->shouldReceive('getAttribute')->with('source_type')->andReturn(Vendor::SOURCE_PLUGIN);
|
||||
$vendor->shouldReceive('getAttribute')->with('current_version')->andReturn('1.5.0');
|
||||
$vendor->shouldReceive('getAttribute')->with('git_repo_url')->andReturn(null);
|
||||
$vendor->shouldReceive('update')->once()->with(Mockery::on(function ($data) {
|
||||
return array_key_exists('last_checked_at', $data);
|
||||
}));
|
||||
$vendor->shouldReceive('isLicensed')->andReturn(false);
|
||||
$vendor->shouldReceive('isPlugin')->andReturn(true);
|
||||
$vendor->shouldReceive('isOss')->andReturn(false);
|
||||
|
||||
// Use a partial mock of the service to prevent createUpdateTodo from hitting DB
|
||||
$service = Mockery::mock(VendorUpdateCheckerService::class)->makePartial();
|
||||
$service->shouldAllowMockingProtectedMethods();
|
||||
$service->shouldReceive('createUpdateTodo')->once();
|
||||
|
||||
$result = $service->checkVendor($vendor);
|
||||
|
||||
expect($result['status'])->toBe('success')
|
||||
->and($result['latest'])->toBe('2.0.1')
|
||||
->and($result['has_update'])->toBeTrue();
|
||||
|
||||
Http::assertSent(function ($request) {
|
||||
return $request->url() === 'https://dev.altumcode.com/plugins-versions';
|
||||
});
|
||||
});
|
||||
|
||||
it('normalises version with v prefix from altum product', function () {
|
||||
Http::fake([
|
||||
'https://66socialproof.com/info.php' => Http::response([
|
||||
'latest_release_version' => 'v7.0.0',
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$vendor = new Vendor([
|
||||
'slug' => '66socialproof',
|
||||
'name' => '66socialproof',
|
||||
'plugin_platform' => Vendor::PLATFORM_ALTUM,
|
||||
'source_type' => Vendor::SOURCE_LICENSED,
|
||||
'current_version' => '6.0.0',
|
||||
]);
|
||||
|
||||
$method = new ReflectionMethod($this->service, 'checkAltumProduct');
|
||||
$method->setAccessible(true);
|
||||
|
||||
$result = $method->invoke($this->service, $vendor);
|
||||
|
||||
expect($result['latest'])->toBe('7.0.0')
|
||||
->and($result['has_update'])->toBeTrue();
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue