mirror of
https://github.com/sp-tarkov/forge.git
synced 2025-02-12 12:10:41 -05:00
Merge remote-tracking branch 'upstream/develop' into user-profile-info
This commit is contained in:
commit
6c86d6b370
@ -30,13 +30,13 @@ class ModController extends Controller
|
||||
$mod = Mod::withTotalDownloads()
|
||||
->with([
|
||||
'versions',
|
||||
'versions.sptVersion',
|
||||
'versions.dependencies',
|
||||
'versions.dependencies.resolvedVersion',
|
||||
'versions.dependencies.resolvedVersion.mod',
|
||||
'versions.latestSptVersion:id,version,color_class',
|
||||
'versions.latestResolvedDependencies',
|
||||
'versions.latestResolvedDependencies.mod:id,name,slug',
|
||||
'users:id,name',
|
||||
'license:id,name,link',
|
||||
])
|
||||
->whereHas('latestVersion')
|
||||
->findOrFail($modId);
|
||||
|
||||
if ($mod->slug !== $slug) {
|
||||
@ -45,9 +45,7 @@ class ModController extends Controller
|
||||
|
||||
$this->authorize('view', $mod);
|
||||
|
||||
$latestVersion = $mod->versions->first();
|
||||
|
||||
return view('mod.show', compact(['mod', 'latestVersion']));
|
||||
return view('mod.show', compact(['mod']));
|
||||
}
|
||||
|
||||
public function update(ModRequest $request, Mod $mod)
|
||||
|
@ -31,7 +31,12 @@ class ModFilter
|
||||
{
|
||||
return Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured', 'created_at'])
|
||||
->withTotalDownloads()
|
||||
->with(['latestVersion', 'latestVersion.sptVersion', 'users:id,name']);
|
||||
->with([
|
||||
'users:id,name',
|
||||
'latestVersion' => function ($query) {
|
||||
$query->with('latestSptVersion:id,version,color_class');
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -95,12 +100,10 @@ class ModFilter
|
||||
/**
|
||||
* Filter the results to a specific SPT version.
|
||||
*/
|
||||
private function sptVersion(array $versions): Builder
|
||||
private function sptVersions(array $versions): Builder
|
||||
{
|
||||
return $this->builder->whereHas('latestVersion', function ($query) use ($versions) {
|
||||
$query->whereHas('sptVersion', function ($query) use ($versions) {
|
||||
$query->whereIn('version', $versions);
|
||||
});
|
||||
return $this->builder->whereHas('latestVersion.sptVersions', function ($query) use ($versions) {
|
||||
$query->whereIn('spt_versions.version', $versions);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -608,8 +608,6 @@ class ImportHubDataJob implements ShouldBeUnique, ShouldQueue
|
||||
|
||||
/**
|
||||
* Import the SPT versions from the Hub database to the local database.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function importSptVersions(): void
|
||||
{
|
||||
@ -938,7 +936,6 @@ class ImportHubDataJob implements ShouldBeUnique, ShouldQueue
|
||||
'description' => $this->cleanHubContent($versionContent->description ?? ''),
|
||||
'link' => $version->downloadURL,
|
||||
'spt_version_constraint' => $sptVersionConstraint,
|
||||
'resolved_spt_version_id' => null,
|
||||
'virus_total_link' => $optionVirusTotal?->virus_total_link ?? '',
|
||||
'downloads' => max((int) $version->downloads, 0), // At least 0.
|
||||
'disabled' => (bool) $version->isDisabled,
|
||||
@ -955,9 +952,9 @@ class ImportHubDataJob implements ShouldBeUnique, ShouldQueue
|
||||
'description',
|
||||
'link',
|
||||
'spt_version_constraint',
|
||||
'resolved_spt_version_id',
|
||||
'virus_total_link',
|
||||
'downloads',
|
||||
'disabled',
|
||||
'published_at',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
|
@ -6,7 +6,9 @@ use App\Http\Filters\ModFilter;
|
||||
use App\Models\SptVersion;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Livewire\Attributes\Computed;
|
||||
use Livewire\Attributes\Session;
|
||||
use Livewire\Attributes\Url;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
@ -19,24 +21,28 @@ class Index extends Component
|
||||
* The search query value.
|
||||
*/
|
||||
#[Url]
|
||||
#[Session]
|
||||
public string $query = '';
|
||||
|
||||
/**
|
||||
* The sort order value.
|
||||
*/
|
||||
#[Url]
|
||||
#[Session]
|
||||
public string $order = 'created';
|
||||
|
||||
/**
|
||||
* The SPT version filter value.
|
||||
* The SPT versions filter value.
|
||||
*/
|
||||
#[Url]
|
||||
public array $sptVersion = [];
|
||||
#[Session]
|
||||
public array $sptVersions = [];
|
||||
|
||||
/**
|
||||
* The featured filter value.
|
||||
*/
|
||||
#[Url]
|
||||
#[Session]
|
||||
public string $featured = 'include';
|
||||
|
||||
/**
|
||||
@ -49,11 +55,13 @@ class Index extends Component
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
// TODO: This should be updated to only pull versions that have mods associated with them.
|
||||
// To do this, the ModVersion to SptVersion relationship needs to be converted to a many-to-many relationship. Ugh.
|
||||
$this->availableSptVersions = SptVersion::select(['id', 'version', 'color_class'])->orderByDesc('version')->get();
|
||||
// TODO: This should ideally be updated to only pull SPT versions that have mods associated with them so that no
|
||||
// empty options are shown in the listing filter.
|
||||
$this->availableSptVersions = Cache::remember('availableSptVersions', 60 * 60, function () {
|
||||
return SptVersion::select(['id', 'version', 'color_class'])->orderByDesc('version')->get();
|
||||
});
|
||||
|
||||
$this->sptVersion = $this->getLatestMinorVersions()->pluck('version')->toArray();
|
||||
$this->sptVersions = $this->sptVersions ?? $this->getLatestMinorVersions()->pluck('version')->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -76,7 +84,7 @@ class Index extends Component
|
||||
'query' => $this->query,
|
||||
'featured' => $this->featured,
|
||||
'order' => $this->order,
|
||||
'sptVersion' => $this->sptVersion,
|
||||
'sptVersions' => $this->sptVersions,
|
||||
];
|
||||
$mods = (new ModFilter($filters))->apply()->paginate(16);
|
||||
|
||||
@ -89,7 +97,7 @@ class Index extends Component
|
||||
public function resetFilters(): void
|
||||
{
|
||||
$this->query = '';
|
||||
$this->sptVersion = $this->getLatestMinorVersions()->pluck('version')->toArray();
|
||||
$this->sptVersions = $this->getLatestMinorVersions()->pluck('version')->toArray();
|
||||
$this->featured = 'include';
|
||||
|
||||
// Clear local storage
|
||||
@ -109,7 +117,7 @@ class Index extends Component
|
||||
if ($this->featured !== 'include') {
|
||||
$count++;
|
||||
}
|
||||
$count += count($this->sptVersion);
|
||||
$count += count($this->sptVersions);
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
@ -58,16 +58,11 @@ class Mod extends Model
|
||||
/**
|
||||
* The relationship between a mod and its versions.
|
||||
*/
|
||||
public function versions(bool $resolvedOnly = true): HasMany
|
||||
public function versions(): HasMany
|
||||
{
|
||||
$relation = $this->hasMany(ModVersion::class)
|
||||
return $this->hasMany(ModVersion::class)
|
||||
->whereHas('sptVersions')
|
||||
->orderByDesc('version');
|
||||
|
||||
if ($resolvedOnly) {
|
||||
$relation->whereNotNull('resolved_spt_version_id');
|
||||
}
|
||||
|
||||
return $relation;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -84,16 +79,11 @@ class Mod extends Model
|
||||
/**
|
||||
* The relationship between a mod and its last updated version.
|
||||
*/
|
||||
public function lastUpdatedVersion(bool $resolvedOnly = true): HasOne
|
||||
public function lastUpdatedVersion(): HasOne
|
||||
{
|
||||
$relation = $this->hasOne(ModVersion::class)
|
||||
return $this->hasOne(ModVersion::class)
|
||||
->whereHas('sptVersions')
|
||||
->orderByDesc('updated_at');
|
||||
|
||||
if ($resolvedOnly) {
|
||||
$relation->whereNotNull('resolved_spt_version_id');
|
||||
}
|
||||
|
||||
return $relation;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -101,10 +91,8 @@ class Mod extends Model
|
||||
*/
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
$latestVersion = $this->latestVersion()->with('sptVersion')->first();
|
||||
|
||||
return [
|
||||
'id' => (int) $this->id,
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'slug' => $this->slug,
|
||||
'description' => $this->description,
|
||||
@ -113,26 +101,21 @@ class Mod extends Model
|
||||
'created_at' => strtotime($this->created_at),
|
||||
'updated_at' => strtotime($this->updated_at),
|
||||
'published_at' => strtotime($this->published_at),
|
||||
'latestVersion' => $latestVersion?->sptVersion->version,
|
||||
'latestVersionColorClass' => $latestVersion?->sptVersion->color_class,
|
||||
'latestVersion' => $this->latestVersion()?->first()?->latestSptVersion()?->first()?->version_formatted,
|
||||
'latestVersionColorClass' => $this->latestVersion()?->first()?->latestSptVersion()?->first()?->color_class,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship to the latest mod version, dictated by the mod version number.
|
||||
*/
|
||||
public function latestVersion(bool $resolvedOnly = true): HasOne
|
||||
public function latestVersion(): HasOne
|
||||
{
|
||||
$relation = $this->hasOne(ModVersion::class)
|
||||
return $this->hasOne(ModVersion::class)
|
||||
->whereHas('sptVersions')
|
||||
->orderByDesc('version')
|
||||
->orderByDesc('updated_at')
|
||||
->take(1);
|
||||
|
||||
if ($resolvedOnly) {
|
||||
$relation->whereNotNull('resolved_spt_version_id');
|
||||
}
|
||||
|
||||
return $relation;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -140,7 +123,31 @@ class Mod extends Model
|
||||
*/
|
||||
public function shouldBeSearchable(): bool
|
||||
{
|
||||
return ! $this->disabled;
|
||||
// Ensure the mod is not disabled.
|
||||
if ($this->disabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure the mod has a publish date.
|
||||
if (is_null($this->published_at)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fetch the latest version instance.
|
||||
$latestVersion = $this->latestVersion()?->first();
|
||||
|
||||
// Ensure the mod has a latest version.
|
||||
if (is_null($latestVersion)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure the latest version has a latest SPT version.
|
||||
if ($latestVersion->latestSptVersion()->doesntExist()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// All conditions are met; the mod should be searchable.
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -5,12 +5,13 @@ namespace App\Models;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $mod_version_id
|
||||
* @property int $dependency_mod_id
|
||||
* @property string $version_constraint
|
||||
* @property string $constraint
|
||||
* @property int|null $resolved_version_id
|
||||
*/
|
||||
class ModDependency extends Model
|
||||
@ -18,7 +19,7 @@ class ModDependency extends Model
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* The relationship between a mod dependency and mod version.
|
||||
* The relationship between the mod dependency and the mod version.
|
||||
*/
|
||||
public function modVersion(): BelongsTo
|
||||
{
|
||||
@ -26,18 +27,18 @@ class ModDependency extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between the mod dependency and the mod that is depended on.
|
||||
* The relationship between the mod dependency and the resolved dependency.
|
||||
*/
|
||||
public function dependencyMod(): BelongsTo
|
||||
public function resolvedDependencies(): HasMany
|
||||
{
|
||||
return $this->belongsTo(Mod::class, 'dependency_mod_id');
|
||||
return $this->hasMany(ModResolvedDependency::class, 'dependency_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between a mod dependency and resolved mod version.
|
||||
* The relationship between the mod dependency and the dependent mod.
|
||||
*/
|
||||
public function resolvedVersion(): BelongsTo
|
||||
public function dependentMod(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ModVersion::class, 'resolved_version_id');
|
||||
return $this->belongsTo(Mod::class, 'dependent_mod_id');
|
||||
}
|
||||
}
|
||||
|
33
app/Models/ModResolvedDependency.php
Normal file
33
app/Models/ModResolvedDependency.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ModResolvedDependency extends Model
|
||||
{
|
||||
/**
|
||||
* The relationship between the resolved dependency and the mod version.
|
||||
*/
|
||||
public function modVersion(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ModVersion::class, 'mod_version_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between the resolved dependency and the dependency.
|
||||
*/
|
||||
public function dependency(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ModDependency::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between the resolved dependency and the resolved mod version.
|
||||
*/
|
||||
public function resolvedModVersion(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ModVersion::class, 'resolved_mod_version_id');
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ use App\Models\Scopes\PublishedScope;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
@ -39,22 +40,53 @@ class ModVersion extends Model
|
||||
/**
|
||||
* The relationship between a mod version and its dependencies.
|
||||
*/
|
||||
public function dependencies(bool $resolvedOnly = true): HasMany
|
||||
public function dependencies(): HasMany
|
||||
{
|
||||
$relation = $this->hasMany(ModDependency::class);
|
||||
|
||||
if ($resolvedOnly) {
|
||||
$relation->whereNotNull('resolved_version_id');
|
||||
}
|
||||
|
||||
return $relation;
|
||||
return $this->hasMany(ModDependency::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between a mod version and SPT version.
|
||||
* The relationship between a mod version and its resolved dependencies.
|
||||
*/
|
||||
public function sptVersion(): BelongsTo
|
||||
public function resolvedDependencies(): BelongsToMany
|
||||
{
|
||||
return $this->belongsTo(SptVersion::class, 'resolved_spt_version_id');
|
||||
return $this->belongsToMany(ModVersion::class, 'mod_resolved_dependencies', 'mod_version_id', 'resolved_mod_version_id')
|
||||
->withPivot('dependency_id')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between a mod version and its each of it's resolved dependencies' latest versions.
|
||||
*/
|
||||
public function latestResolvedDependencies(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(ModVersion::class, 'mod_resolved_dependencies', 'mod_version_id', 'resolved_mod_version_id')
|
||||
->withPivot('dependency_id')
|
||||
->join('mod_versions as latest_versions', function ($join) {
|
||||
$join->on('latest_versions.id', '=', 'mod_versions.id')
|
||||
->whereRaw('latest_versions.version = (SELECT MAX(mv.version) FROM mod_versions mv WHERE mv.mod_id = mod_versions.mod_id)');
|
||||
})
|
||||
->with('mod:id,name,slug')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between a mod version and each of its SPT versions' latest version.
|
||||
* Hint: Be sure to call `->first()` on this to get the actual instance.
|
||||
*/
|
||||
public function latestSptVersion(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(SptVersion::class, 'mod_version_spt_version')
|
||||
->orderBy('version', 'desc')
|
||||
->limit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between a mod version and its SPT versions.
|
||||
*/
|
||||
public function sptVersions(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(SptVersion::class, 'mod_version_spt_version')
|
||||
->orderByDesc('version');
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ use App\Exceptions\InvalidVersionNumberException;
|
||||
use App\Services\LatestSptVersionService;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Facades\App;
|
||||
|
||||
@ -14,12 +14,20 @@ class SptVersion extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
/**
|
||||
* Get the version with "SPT " prepended.
|
||||
*/
|
||||
public function getVersionFormattedAttribute(): string
|
||||
{
|
||||
return __('SPT ').$this->version;
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between an SPT version and mod version.
|
||||
*/
|
||||
public function modVersions(): HasMany
|
||||
public function modVersions(): BelongsToMany
|
||||
{
|
||||
return $this->hasMany(ModVersion::class);
|
||||
return $this->belongsToMany(ModVersion::class, 'mod_version_spt_version');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2,9 +2,7 @@
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Exceptions\CircularDependencyException;
|
||||
use App\Models\ModDependency;
|
||||
use App\Models\ModVersion;
|
||||
use App\Services\DependencyVersionService;
|
||||
|
||||
class ModDependencyObserver
|
||||
@ -18,34 +16,17 @@ class ModDependencyObserver
|
||||
|
||||
/**
|
||||
* Handle the ModDependency "saved" event.
|
||||
*
|
||||
* @throws CircularDependencyException
|
||||
*/
|
||||
public function saved(ModDependency $modDependency): void
|
||||
{
|
||||
$this->resolveDependencyVersion($modDependency);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the ModDependency's dependencies.
|
||||
*
|
||||
* @throws CircularDependencyException
|
||||
*/
|
||||
public function resolveDependencyVersion(ModDependency $modDependency): void
|
||||
{
|
||||
$modVersion = ModVersion::find($modDependency->mod_version_id);
|
||||
if ($modVersion) {
|
||||
$this->dependencyVersionService->resolve($modVersion);
|
||||
}
|
||||
$this->dependencyVersionService->resolve($modDependency->modVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the ModDependency "deleted" event.
|
||||
*
|
||||
* @throws CircularDependencyException
|
||||
*/
|
||||
public function deleted(ModDependency $modDependency): void
|
||||
{
|
||||
$this->resolveDependencyVersion($modDependency);
|
||||
$this->dependencyVersionService->resolve($modDependency->modVersion);
|
||||
}
|
||||
}
|
||||
|
27
app/Observers/ModObserver.php
Normal file
27
app/Observers/ModObserver.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Models\Mod;
|
||||
use App\Services\DependencyVersionService;
|
||||
|
||||
class ModObserver
|
||||
{
|
||||
protected DependencyVersionService $dependencyVersionService;
|
||||
|
||||
public function __construct(
|
||||
DependencyVersionService $dependencyVersionService,
|
||||
) {
|
||||
$this->dependencyVersionService = $dependencyVersionService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the Mod "saved" event.
|
||||
*/
|
||||
public function saved(Mod $mod): void
|
||||
{
|
||||
foreach ($mod->versions as $modVersion) {
|
||||
$this->dependencyVersionService->resolve($modVersion);
|
||||
}
|
||||
}
|
||||
}
|
@ -2,8 +2,6 @@
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Exceptions\CircularDependencyException;
|
||||
use App\Models\ModDependency;
|
||||
use App\Models\ModVersion;
|
||||
use App\Services\DependencyVersionService;
|
||||
use App\Services\SptVersionService;
|
||||
@ -24,35 +22,18 @@ class ModVersionObserver
|
||||
|
||||
/**
|
||||
* Handle the ModVersion "saved" event.
|
||||
*
|
||||
* @throws CircularDependencyException
|
||||
*/
|
||||
public function saved(ModVersion $modVersion): void
|
||||
{
|
||||
$this->resolveDependencyVersion($modVersion);
|
||||
$this->dependencyVersionService->resolve($modVersion);
|
||||
$this->sptVersionService->resolve($modVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the ModVersion's dependencies.
|
||||
*
|
||||
* @throws CircularDependencyException
|
||||
*/
|
||||
private function resolveDependencyVersion(ModVersion $modVersion): void
|
||||
{
|
||||
$dependencies = ModDependency::where('resolved_version_id', $modVersion->id)->get();
|
||||
foreach ($dependencies as $dependency) {
|
||||
$this->dependencyVersionService->resolve($dependency->modVersion);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the ModVersion "deleted" event.
|
||||
*
|
||||
* @throws CircularDependencyException
|
||||
*/
|
||||
public function deleted(ModVersion $modVersion): void
|
||||
{
|
||||
$this->resolveDependencyVersion($modVersion);
|
||||
$this->dependencyVersionService->resolve($modVersion);
|
||||
}
|
||||
}
|
||||
|
@ -2,11 +2,13 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Models\Mod;
|
||||
use App\Models\ModDependency;
|
||||
use App\Models\ModVersion;
|
||||
use App\Models\SptVersion;
|
||||
use App\Models\User;
|
||||
use App\Observers\ModDependencyObserver;
|
||||
use App\Observers\ModObserver;
|
||||
use App\Observers\ModVersionObserver;
|
||||
use App\Observers\SptVersionObserver;
|
||||
use App\Services\LatestSptVersionService;
|
||||
@ -36,6 +38,7 @@ class AppServiceProvider extends ServiceProvider
|
||||
Model::unguard();
|
||||
|
||||
// Register observers.
|
||||
Mod::observe(ModObserver::class);
|
||||
ModVersion::observe(ModVersionObserver::class);
|
||||
ModDependency::observe(ModDependencyObserver::class);
|
||||
SptVersion::observe(SptVersionObserver::class);
|
||||
|
@ -2,118 +2,46 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Exceptions\CircularDependencyException;
|
||||
use App\Models\ModDependency;
|
||||
use App\Models\ModVersion;
|
||||
use Composer\Semver\Semver;
|
||||
|
||||
class DependencyVersionService
|
||||
{
|
||||
/**
|
||||
* Keep track of visited versions to avoid resolving them again.
|
||||
* Resolve the dependencies for a mod version.
|
||||
*/
|
||||
protected array $visited = [];
|
||||
|
||||
/**
|
||||
* Keep track of the current path in the depth-first search.
|
||||
*/
|
||||
protected array $stack = [];
|
||||
|
||||
/**
|
||||
* Resolve dependencies for the given mod version.
|
||||
*
|
||||
* @throws CircularDependencyException
|
||||
*/
|
||||
public function resolve(ModVersion $modVersion): array
|
||||
public function resolve(ModVersion $modVersion): void
|
||||
{
|
||||
$this->visited = [];
|
||||
$this->stack = [];
|
||||
|
||||
// Store the resolved versions for each dependency.
|
||||
$resolvedVersions = [];
|
||||
|
||||
// Start the recursive depth-first search to resolve dependencies.
|
||||
$this->processDependencies($modVersion, $resolvedVersions);
|
||||
|
||||
return $resolvedVersions;
|
||||
$dependencies = $this->satisfyConstraint($modVersion);
|
||||
$modVersion->resolvedDependencies()->sync($dependencies);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a depth-first search to resolve dependencies for the given mod version.
|
||||
*
|
||||
* @throws CircularDependencyException
|
||||
* Satisfies all dependency constraints of a ModVersion.
|
||||
*/
|
||||
protected function processDependencies(ModVersion $modVersion, array &$resolvedVersions): void
|
||||
private function satisfyConstraint(ModVersion $modVersion): array
|
||||
{
|
||||
// Detect circular dependencies
|
||||
if (in_array($modVersion->id, $this->stack)) {
|
||||
throw new CircularDependencyException("Circular dependency detected in ModVersion ID: {$modVersion->id}");
|
||||
}
|
||||
// Eager load the dependencies and their mod versions.
|
||||
$modVersion->load('dependencies.dependentMod.versions');
|
||||
|
||||
// Skip already processed versions
|
||||
if (in_array($modVersion->id, $this->visited)) {
|
||||
return;
|
||||
}
|
||||
// Iterate over each ModVersion dependency.
|
||||
$dependencies = [];
|
||||
foreach ($modVersion->dependencies as $dependency) {
|
||||
|
||||
// Mark the current version
|
||||
$this->visited[] = $modVersion->id;
|
||||
$this->stack[] = $modVersion->id;
|
||||
// Get all dependent mod versions.
|
||||
$dependentModVersions = $dependency->dependentMod->versions()->get();
|
||||
|
||||
// Get the dependencies for the current mod version.
|
||||
$dependencies = $modVersion->dependencies(resolvedOnly: false)->get();
|
||||
// Filter the dependent mod versions to find the ones that satisfy the dependency constraint.
|
||||
$matchedVersions = $dependentModVersions->filter(function ($version) use ($dependency) {
|
||||
return Semver::satisfies($version->version, $dependency->constraint);
|
||||
});
|
||||
|
||||
foreach ($dependencies as $dependency) {
|
||||
// Resolve the latest mod version ID that satisfies the version constraint on the mod version dependency.
|
||||
$resolvedId = $this->resolveDependency($dependency);
|
||||
|
||||
// Update the resolved version ID for the dependency if it has changed.
|
||||
// Do it "quietly" to avoid triggering the observer again.
|
||||
if ($dependency->resolved_version_id !== $resolvedId) {
|
||||
$dependency->updateQuietly(['resolved_version_id' => $resolvedId]);
|
||||
}
|
||||
|
||||
// At this point, the dependency has been resolved (or not) and we can add it to the resolved versions to
|
||||
// avoid resolving it again in the future and to help with circular dependency detection.
|
||||
$resolvedVersions[$dependency->id] = $resolvedId ? ModVersion::find($resolvedId) : null;
|
||||
|
||||
// Recursively process the resolved dependency.
|
||||
if ($resolvedId) {
|
||||
$nextModVersion = ModVersion::find($resolvedId);
|
||||
if ($nextModVersion) {
|
||||
$this->processDependencies($nextModVersion, $resolvedVersions);
|
||||
}
|
||||
// Map the matched versions to the sync data.
|
||||
foreach ($matchedVersions as $matchedVersion) {
|
||||
$dependencies[$matchedVersion->id] = ['dependency_id' => $dependency->id];
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the current version from the stack now that we have processed all its dependencies.
|
||||
array_pop($this->stack);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the latest mod version ID that satisfies the version constraint on the mod version dependency.
|
||||
*/
|
||||
protected function resolveDependency(ModDependency $dependency): ?int
|
||||
{
|
||||
$dependencyModVersions = $dependency->dependencyMod->versions(resolvedOnly: false);
|
||||
|
||||
// There are no mod versions for the dependency mod.
|
||||
if ($dependencyModVersions->doesntExist()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$availableVersions = $dependencyModVersions->pluck('id', 'version')->toArray();
|
||||
$satisfyingVersions = Semver::satisfiedBy(array_keys($availableVersions), $dependency->version_constraint);
|
||||
|
||||
// There are no mod versions that satisfy the version constraint.
|
||||
if (empty($satisfyingVersions)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Sort the satisfying versions in descending order using the version_compare function.
|
||||
usort($satisfyingVersions, 'version_compare');
|
||||
$satisfyingVersions = array_reverse($satisfyingVersions);
|
||||
|
||||
// Return the latest (highest version number) satisfying version.
|
||||
return $availableVersions[$satisfyingVersions[0]];
|
||||
return $dependencies;
|
||||
}
|
||||
}
|
||||
|
@ -13,14 +13,14 @@ class SptVersionService
|
||||
*/
|
||||
public function resolve(ModVersion $modVersion): void
|
||||
{
|
||||
$modVersion->resolved_spt_version_id = $this->satisfyconstraint($modVersion);
|
||||
$modVersion->saveQuietly();
|
||||
$satisfyingVersionIds = $this->satisfyConstraint($modVersion);
|
||||
$modVersion->sptVersions()->sync($satisfyingVersionIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Satisfies the version constraint of a given ModVersion. Returns the ID of the satisfying SptVersion.
|
||||
*/
|
||||
private function satisfyConstraint(ModVersion $modVersion): ?int
|
||||
private function satisfyConstraint(ModVersion $modVersion): array
|
||||
{
|
||||
$availableVersions = SptVersion::query()
|
||||
->orderBy('version', 'desc')
|
||||
@ -29,14 +29,10 @@ class SptVersionService
|
||||
|
||||
$satisfyingVersions = Semver::satisfiedBy(array_keys($availableVersions), $modVersion->spt_version_constraint);
|
||||
if (empty($satisfyingVersions)) {
|
||||
return null;
|
||||
return [];
|
||||
}
|
||||
|
||||
// Ensure the satisfying versions are sorted in descending order to get the latest version
|
||||
usort($satisfyingVersions, 'version_compare');
|
||||
$satisfyingVersions = array_reverse($satisfyingVersions);
|
||||
|
||||
// Return the ID of the latest satisfying version
|
||||
return $availableVersions[$satisfyingVersions[0]];
|
||||
// Return the IDs of all satisfying versions
|
||||
return array_map(fn ($version) => $availableVersions[$version], $satisfyingVersions);
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ use App\Models\Mod;
|
||||
use App\Models\ModVersion;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class ModListSection extends Component
|
||||
@ -26,44 +25,53 @@ class ModListSection extends Component
|
||||
|
||||
private function fetchFeaturedMods(): Collection
|
||||
{
|
||||
return Cache::remember('homepage-featured-mods', now()->addMinutes(5), function () {
|
||||
return Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured'])
|
||||
->withTotalDownloads()
|
||||
->with(['latestVersion', 'latestVersion.sptVersion', 'users:id,name'])
|
||||
->where('featured', true)
|
||||
->latest()
|
||||
->limit(6)
|
||||
->get();
|
||||
});
|
||||
return Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured'])
|
||||
->withTotalDownloads()
|
||||
->with([
|
||||
'latestVersion',
|
||||
'latestVersion.latestSptVersion:id,version,color_class',
|
||||
'users:id,name',
|
||||
'license:id,name,link',
|
||||
])
|
||||
->where('featured', true)
|
||||
->latest()
|
||||
->limit(6)
|
||||
->get();
|
||||
}
|
||||
|
||||
private function fetchLatestMods(): Collection
|
||||
{
|
||||
return Cache::remember('homepage-latest-mods', now()->addMinutes(5), function () {
|
||||
return Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured', 'created_at'])
|
||||
->withTotalDownloads()
|
||||
->with(['latestVersion', 'latestVersion.sptVersion', 'users:id,name'])
|
||||
->latest()
|
||||
->limit(6)
|
||||
->get();
|
||||
});
|
||||
return Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured', 'created_at'])
|
||||
->withTotalDownloads()
|
||||
->with([
|
||||
'latestVersion',
|
||||
'latestVersion.latestSptVersion:id,version,color_class',
|
||||
'users:id,name',
|
||||
'license:id,name,link',
|
||||
])
|
||||
->latest()
|
||||
->limit(6)
|
||||
->get();
|
||||
}
|
||||
|
||||
private function fetchUpdatedMods(): Collection
|
||||
{
|
||||
return Cache::remember('homepage-updated-mods', now()->addMinutes(5), function () {
|
||||
return Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured'])
|
||||
->withTotalDownloads()
|
||||
->with(['lastUpdatedVersion', 'lastUpdatedVersion.sptVersion', 'users:id,name'])
|
||||
->orderByDesc(
|
||||
ModVersion::select('updated_at')
|
||||
->whereColumn('mod_id', 'mods.id')
|
||||
->orderByDesc('updated_at')
|
||||
->take(1)
|
||||
)
|
||||
->limit(6)
|
||||
->get();
|
||||
});
|
||||
return Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured'])
|
||||
->withTotalDownloads()
|
||||
->with([
|
||||
'latestVersion',
|
||||
'latestVersion.latestSptVersion:id,version,color_class',
|
||||
'users:id,name',
|
||||
'license:id,name,link',
|
||||
])
|
||||
->orderByDesc(
|
||||
ModVersion::select('updated_at')
|
||||
->whereColumn('mod_id', 'mods.id')
|
||||
->orderByDesc('updated_at')
|
||||
->take(1)
|
||||
)
|
||||
->limit(6)
|
||||
->get();
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
|
@ -16,20 +16,10 @@ class ModDependencyFactory extends Factory
|
||||
{
|
||||
return [
|
||||
'mod_version_id' => ModVersion::factory(),
|
||||
'dependency_mod_id' => Mod::factory(),
|
||||
'version_constraint' => fake()->numerify($this->generateVersionConstraint()),
|
||||
'dependent_mod_id' => Mod::factory(),
|
||||
'constraint' => '*',
|
||||
'created_at' => Carbon::now()->subDays(rand(0, 365))->subHours(rand(0, 23)),
|
||||
'updated_at' => Carbon::now()->subDays(rand(0, 365))->subHours(rand(0, 23)),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* This method generates a random version constraint from a predefined set of options.
|
||||
*/
|
||||
private function generateVersionConstraint(): string
|
||||
{
|
||||
$versionConstraints = ['*', '^1.#.#', '>=2.#.#', '~1.#.#', '>=1.2.# <2.#.#'];
|
||||
|
||||
return $versionConstraints[array_rand($versionConstraints)];
|
||||
}
|
||||
}
|
||||
|
@ -14,15 +14,15 @@ class ModVersionFactory extends Factory
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
$constraint = fake()->numerify($this->generateVersionConstraint());
|
||||
|
||||
return [
|
||||
'mod_id' => Mod::factory(),
|
||||
'version' => fake()->numerify('#.#.#'),
|
||||
'description' => fake()->text(),
|
||||
'link' => fake()->url(),
|
||||
'spt_version_constraint' => $constraint,
|
||||
'resolved_spt_version_id' => null,
|
||||
|
||||
// Unless a custom constraint is provided, this will also generate the required SPT versions.
|
||||
'spt_version_constraint' => $this->faker->randomElement(['^1.0', '^2.0', '>=3.0', '<4.0']),
|
||||
|
||||
'virus_total_link' => fake()->url(),
|
||||
'downloads' => fake()->randomNumber(),
|
||||
'published_at' => Carbon::now()->subDays(rand(0, 365))->subHours(rand(0, 23)),
|
||||
@ -32,28 +32,43 @@ class ModVersionFactory extends Factory
|
||||
}
|
||||
|
||||
/**
|
||||
* This method generates a random version constraint from a predefined set of options.
|
||||
* Configure the model factory.
|
||||
*/
|
||||
private function generateVersionConstraint(): string
|
||||
public function configure(): ModVersionFactory
|
||||
{
|
||||
$versionConstraints = ['*', '^1.#.#', '>=2.#.#', '~1.#.#'];
|
||||
|
||||
return $versionConstraints[array_rand($versionConstraints)];
|
||||
return $this->afterCreating(function (ModVersion $modVersion) {
|
||||
$this->ensureSptVersionsExist($modVersion); // Create SPT Versions
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the mod version should have a resolved SPT version.
|
||||
* Ensure that the required SPT versions exist and are associated with the mod version.
|
||||
*/
|
||||
public function sptVersionResolved(): static
|
||||
protected function ensureSptVersionsExist(ModVersion $modVersion): void
|
||||
{
|
||||
$constraint = fake()->numerify('#.#.#');
|
||||
$constraint = $modVersion->spt_version_constraint;
|
||||
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'spt_version_constraint' => $constraint,
|
||||
'resolved_spt_version_id' => SptVersion::factory()->create([
|
||||
'version' => $constraint,
|
||||
]),
|
||||
]);
|
||||
$requiredVersions = match ($constraint) {
|
||||
'^1.0' => ['1.0.0', '1.1.0', '1.2.0'],
|
||||
'^2.0' => ['2.0.0', '2.1.0'],
|
||||
'>=3.0' => ['3.0.0', '3.1.0', '3.2.0', '4.0.0'],
|
||||
'<4.0' => ['1.0.0', '2.0.0', '3.0.0'],
|
||||
default => [],
|
||||
};
|
||||
|
||||
// If the version is anything but the default, no SPT versions are created.
|
||||
if (! $requiredVersions) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($requiredVersions as $version) {
|
||||
SptVersion::firstOrCreate(['version' => $version], [
|
||||
'color_class' => $this->faker->randomElement(['red', 'green', 'emerald', 'lime', 'yellow', 'grey']),
|
||||
'link' => $this->faker->url,
|
||||
]);
|
||||
}
|
||||
|
||||
$modVersion->sptVersions()->sync(SptVersion::whereIn('version', $requiredVersions)->pluck('id')->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -35,7 +35,9 @@ return new class extends Migration
|
||||
$table->timestamp('published_at')->nullable()->default(null);
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['deleted_at', 'disabled'], 'mods_show_index');
|
||||
$table->index(['slug']);
|
||||
$table->index(['featured']);
|
||||
$table->index(['deleted_at', 'disabled', 'published_at'], 'mods_filtering_index');
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -23,11 +23,6 @@ return new class extends Migration
|
||||
$table->longText('description');
|
||||
$table->string('link');
|
||||
$table->string('spt_version_constraint');
|
||||
$table->foreignId('resolved_spt_version_id')
|
||||
->nullable()
|
||||
->constrained('spt_versions')
|
||||
->nullOnDelete()
|
||||
->cascadeOnUpdate();
|
||||
$table->string('virus_total_link');
|
||||
$table->unsignedBigInteger('downloads');
|
||||
$table->boolean('disabled')->default(false);
|
||||
@ -35,7 +30,9 @@ return new class extends Migration
|
||||
$table->timestamp('published_at')->nullable()->default(null);
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['mod_id', 'deleted_at', 'disabled', 'version'], 'mod_versions_filtering_index');
|
||||
$table->index(['version']);
|
||||
$table->index(['mod_id', 'deleted_at', 'disabled', 'published_at'], 'mod_versions_filtering_index');
|
||||
$table->index(['id', 'deleted_at'], 'mod_versions_id_deleted_at_index');
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('mod_version_spt_version', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('mod_version_id')->constrained('mod_versions')->cascadeOnDelete()->cascadeOnUpdate();
|
||||
$table->foreignId('spt_version_id')->constrained('spt_versions')->cascadeOnDelete()->cascadeOnUpdate();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['mod_version_id', 'spt_version_id'], 'mod_version_spt_version_index');
|
||||
$table->index(['spt_version_id', 'mod_version_id'], 'spt_version_mod_version_index');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('mod_version_spt_version');
|
||||
}
|
||||
};
|
@ -10,23 +10,12 @@ return new class extends Migration
|
||||
{
|
||||
Schema::create('mod_dependencies', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('mod_version_id')
|
||||
->constrained('mod_versions')
|
||||
->cascadeOnDelete()
|
||||
->cascadeOnUpdate();
|
||||
$table->foreignId('dependency_mod_id')
|
||||
->constrained('mods')
|
||||
->cascadeOnDelete()
|
||||
->cascadeOnUpdate();
|
||||
$table->string('version_constraint');
|
||||
$table->foreignId('resolved_version_id')
|
||||
->nullable()
|
||||
->constrained('mod_versions')
|
||||
->nullOnDelete()
|
||||
->cascadeOnUpdate();
|
||||
$table->foreignId('mod_version_id')->constrained('mod_versions')->cascadeOnDelete()->cascadeOnUpdate();
|
||||
$table->foreignId('dependent_mod_id')->constrained('mods')->cascadeOnDelete()->cascadeOnUpdate();
|
||||
$table->string('constraint');
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['mod_version_id', 'dependency_mod_id', 'version_constraint'], 'mod_dependencies_unique');
|
||||
$table->index(['mod_version_id', 'dependent_mod_id']);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('mod_resolved_dependencies', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('mod_version_id')->constrained('mod_versions')->cascadeOnDelete()->cascadeOnUpdate();
|
||||
$table->foreignId('dependency_id')->constrained('mod_dependencies')->cascadeOnDelete()->cascadeOnUpdate();
|
||||
$table->foreignId('resolved_mod_version_id')->constrained('mod_versions')->cascadeOnDelete()->cascadeOnUpdate();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['mod_version_id', 'dependency_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('mod_resolved_dependencies');
|
||||
}
|
||||
};
|
@ -1,12 +1,3 @@
|
||||
import "./registerViteAssets";
|
||||
import "./registerAlpineLivewire";
|
||||
import "./themeToggle";
|
||||
|
||||
document.addEventListener("livewire:init", () => {
|
||||
Livewire.on("clear-filters", (event) => {
|
||||
localStorage.removeItem("filter-query");
|
||||
localStorage.removeItem("filter-order");
|
||||
localStorage.removeItem("filter-sptVersion");
|
||||
localStorage.removeItem("filter-featured");
|
||||
});
|
||||
});
|
||||
|
@ -18,8 +18,8 @@
|
||||
<div class="pb-3">
|
||||
<div class="flex justify-between items-center space-x-3">
|
||||
<h3 class="block mt-1 text-lg leading-tight font-medium text-black dark:text-white group-hover:underline">{{ $mod->name }}</h3>
|
||||
<span class="badge-version {{ $mod->{$versionScope}->sptVersion->color_class }} inline-flex items-center rounded-md px-2 py-1 text-xs font-medium text-nowrap">
|
||||
{{ $mod->{$versionScope}->sptVersion->version }}
|
||||
<span class="badge-version {{ $mod->{$versionScope}->latestSptVersion->first()->color_class }} inline-flex items-center rounded-md px-2 py-1 text-xs font-medium text-nowrap">
|
||||
{{ $mod->{$versionScope}->latestSptVersion->first()->version_formatted }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-sm italic text-slate-600 dark:text-gray-200">
|
||||
|
@ -1,35 +1,4 @@
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8"
|
||||
x-data="{
|
||||
query: @entangle('query'),
|
||||
order: @entangle('order'),
|
||||
sptVersion: @entangle('sptVersion'),
|
||||
featured: @entangle('featured'),
|
||||
init() {
|
||||
this.loadFiltersFromLocalStorage();
|
||||
$wire.$refresh();
|
||||
|
||||
$watch('query', value => this.saveFilterToLocalStorage('query', value));
|
||||
$watch('order', value => this.saveFilterToLocalStorage('order', value));
|
||||
$watch('sptVersion', value => this.saveFilterToLocalStorage('sptVersion', value));
|
||||
$watch('featured', value => this.saveFilterToLocalStorage('featured', value));
|
||||
},
|
||||
saveFilterToLocalStorage(key, value) {
|
||||
localStorage.setItem(`filter-${key}`, JSON.stringify(value));
|
||||
},
|
||||
loadFiltersFromLocalStorage() {
|
||||
const query = localStorage.getItem('filter-query');
|
||||
if (query) this.query = JSON.parse(query);
|
||||
|
||||
const order = localStorage.getItem('filter-order');
|
||||
if (order) this.order = JSON.parse(order);
|
||||
|
||||
const sptVersion = localStorage.getItem('filter-sptVersion');
|
||||
if (sptVersion) this.sptVersion = JSON.parse(sptVersion);
|
||||
|
||||
const featured = localStorage.getItem('filter-featured');
|
||||
if (featured) this.featured = JSON.parse(featured);
|
||||
},
|
||||
}">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="px-4 py-8 sm:px-6 lg:px-8 bg-white dark:bg-gray-900 overflow-hidden shadow-xl dark:shadow-gray-900 rounded-none sm:rounded-lg">
|
||||
<h1 class="text-4xl font-bold tracking-tight text-gray-900 dark:text-gray-200">{{ __('Mods') }}</h1>
|
||||
<p class="mt-4 text-base text-slate-500 dark:text-gray-300">{!! __('Explore an enhanced <abbr title="Single Player Tarkov">SPT</abbr> experience with the mods available below. Check out the featured mods for a tailored solo-survival game with maximum immersion.') !!}</p>
|
||||
@ -61,7 +30,13 @@
|
||||
<button @click="$wire.call('resetFilters')" type="button" class="pl-6 text-gray-500 dark:text-gray-300">{{ __('Reset Filters') }}</button>
|
||||
|
||||
<div wire:loading.flex>
|
||||
<p class="pl-6 flex items-center font-medium text-gray-700 dark:text-gray-300">{{ __('Loading...') }}</p>
|
||||
<p class="pl-6 flex items-center font-medium text-gray-700 dark:text-gray-300">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" class="w-4 h-4 fill-cyan-600 dark:fill-cyan-600 motion-safe:animate-spin">
|
||||
<path d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" opacity=".25" />
|
||||
<path d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" />
|
||||
</svg>
|
||||
<span class="pl-1.5">{{ __('Loading...') }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -86,7 +61,7 @@
|
||||
<div class="space-y-6 pt-6 sm:space-y-4 sm:pt-4">
|
||||
@foreach ($availableSptVersions as $index => $version)
|
||||
@if ($index < $half)
|
||||
<x-filter-checkbox id="sptVersion-{{ $index }}" name="sptVersion" value="{{ $version->version }}">{{ $version->version }}</x-filter-checkbox>
|
||||
<x-filter-checkbox id="sptVersions-{{ $index }}" name="sptVersions" value="{{ $version->version }}">{{ $version->version }}</x-filter-checkbox>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
@ -96,7 +71,7 @@
|
||||
<div class="space-y-6 pt-6 sm:space-y-4 sm:pt-4">
|
||||
@foreach ($availableSptVersions as $index => $version)
|
||||
@if ($index >= $half)
|
||||
<x-filter-checkbox id="sptVersion-{{ $index }}" name="sptVersion" value="{{ $version->version }}">{{ $version->version }}</x-filter-checkbox>
|
||||
<x-filter-checkbox id="sptVersions-{{ $index }}" name="sptVersions" value="{{ $version->version }}">{{ $version->version }}</x-filter-checkbox>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
|
@ -28,7 +28,7 @@
|
||||
<h2 class="pb-1 sm:pb-2 text-3xl font-bold text-gray-900 dark:text-white">
|
||||
{{ $mod->name }}
|
||||
<span class="font-light text-nowrap text-gray-700 dark:text-gray-400">
|
||||
{{ $latestVersion->version }}
|
||||
{{ $mod->latestVersion->version }}
|
||||
</span>
|
||||
</h2>
|
||||
</div>
|
||||
@ -40,8 +40,8 @@
|
||||
</p>
|
||||
<p title="{{ __('Exactly') }} {{ $mod->total_downloads }}">{{ Number::downloads($mod->total_downloads) }} {{ __('Downloads') }}</p>
|
||||
<p class="mt-2">
|
||||
<span class="badge-version {{ $latestVersion->sptVersion->color_class }} inline-flex items-center rounded-md px-2 py-1 text-xs font-medium text-nowrap">
|
||||
{{ $latestVersion->sptVersion->version }} {{ __('Compatible') }}
|
||||
<span class="badge-version {{ $mod->latestVersion->latestSptVersion->first()->color_class }} inline-flex items-center rounded-md px-2 py-1 text-xs font-medium text-nowrap">
|
||||
{{ $mod->latestVersion->latestSptVersion->first()->version_formatted }} {{ __('Compatible') }}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
@ -54,8 +54,8 @@
|
||||
</div>
|
||||
|
||||
{{-- Mobile Download Button --}}
|
||||
<a href="{{ $latestVersion->link }}" class="block lg:hidden">
|
||||
<button class="text-lg font-extrabold hover:bg-cyan-400 dark:hover:bg-cyan-600 shadow-md dark:shadow-gray-950 drop-shadow-2xl bg-cyan-500 dark:bg-cyan-700 rounded-xl w-full h-20">{{ __('Download Latest Version') }} ({{ $latestVersion->version }})</button>
|
||||
<a href="{{ $mod->latestVersion->link }}" class="block lg:hidden">
|
||||
<button class="text-lg font-extrabold hover:bg-cyan-400 dark:hover:bg-cyan-600 shadow-md dark:shadow-gray-950 drop-shadow-2xl bg-cyan-500 dark:bg-cyan-700 rounded-xl w-full h-20">{{ __('Download Latest Version') }} ({{ $mod->latestVersion->version }})</button>
|
||||
</a>
|
||||
|
||||
{{-- Tabs --}}
|
||||
@ -108,8 +108,8 @@
|
||||
<p class="text-gray-700 dark:text-gray-300" title="{{ __('Exactly') }} {{ $version->downloads }}">{{ Number::downloads($version->downloads) }} {{ __('Downloads') }}</p>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="badge-version {{ $version->sptVersion->color_class }} inline-flex items-center rounded-md px-2 py-1 text-xs font-medium text-nowrap">
|
||||
{{ $version->sptVersion->version }}
|
||||
<span class="badge-version {{ $version->latestSptVersion->first()->color_class }} inline-flex items-center rounded-md px-2 py-1 text-xs font-medium text-nowrap">
|
||||
{{ $version->latestSptVersion->first()->version_formatted }}
|
||||
</span>
|
||||
<a href="{{ $version->virus_total_link }}">{{__('Virus Total Results')}}</a>
|
||||
</div>
|
||||
@ -117,20 +117,18 @@
|
||||
<span>{{ __('Created') }} {{ $version->created_at->format("M d, h:m a") }}</span>
|
||||
<span>{{ __('Updated') }} {{ $version->updated_at->format("M d, h:m a") }}</span>
|
||||
</div>
|
||||
@if ($version->dependencies->isNotEmpty() && $version->dependencies->contains(fn($dependency) => $dependency->resolvedVersion?->mod))
|
||||
|
||||
{{-- Display latest resolved dependencies --}}
|
||||
@if ($version->latestResolvedDependencies->isNotEmpty())
|
||||
<div class="text-gray-600 dark:text-gray-400">
|
||||
{{ __('Dependencies:') }}
|
||||
@foreach ($version->dependencies as $dependency)
|
||||
@if ($dependency->resolvedVersion?->mod)
|
||||
<a href="{{ $dependency->resolvedVersion->mod->detailUrl() }}">
|
||||
{{ $dependency->resolvedVersion->mod->name }} ({{ $dependency->resolvedVersion->version }})
|
||||
</a>@if (!$loop->last), @endif
|
||||
@endif
|
||||
@foreach ($version->latestResolvedDependencies as $resolvedDependency)
|
||||
<a href="{{ $resolvedDependency->mod->detailUrl() }}">{{ $resolvedDependency->mod->name }} ({{ $resolvedDependency->version }})</a>@if (!$loop->last), @endif
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="p-3 user-markdown">
|
||||
<div class="py-3 user-markdown">
|
||||
{{-- The description below is safe to write directly because it has been run though HTMLPurifier during the import process. --}}
|
||||
{!! Str::markdown($version->description) !!}
|
||||
</div>
|
||||
@ -149,8 +147,8 @@
|
||||
<div class="col-span-1 flex flex-col gap-6">
|
||||
|
||||
{{-- Desktop Download Button --}}
|
||||
<a href="{{ $latestVersion->link }}" class="hidden lg:block">
|
||||
<button class="text-lg font-extrabold hover:bg-cyan-400 dark:hover:bg-cyan-600 shadow-md dark:shadow-gray-950 drop-shadow-2xl bg-cyan-500 dark:bg-cyan-700 rounded-xl w-full h-20">{{ __('Download Latest Version') }} ({{ $latestVersion->version }})</button>
|
||||
<a href="{{ $mod->latestVersion->link }}" class="hidden lg:block">
|
||||
<button class="text-lg font-extrabold hover:bg-cyan-400 dark:hover:bg-cyan-600 shadow-md dark:shadow-gray-950 drop-shadow-2xl bg-cyan-500 dark:bg-cyan-700 rounded-xl w-full h-20">{{ __('Download Latest Version') }} ({{ $mod->latestVersion->version }})</button>
|
||||
</a>
|
||||
|
||||
{{-- Additional Mod Details --}}
|
||||
@ -177,21 +175,21 @@
|
||||
</p>
|
||||
</li>
|
||||
@endif
|
||||
@if ($latestVersion->virus_total_link)
|
||||
@if ($mod->latestVersion->virus_total_link)
|
||||
<li class="px-4 py-4 sm:px-0">
|
||||
<h3>{{ __('Latest Version VirusTotal Result') }}</h3>
|
||||
<p class="truncate">
|
||||
<a href="{{ $latestVersion->virus_total_link }}" title="{{ $latestVersion->virus_total_link }}" target="_blank">
|
||||
{{ $latestVersion->virus_total_link }}
|
||||
<a href="{{ $mod->latestVersion->virus_total_link }}" title="{{ $mod->latestVersion->virus_total_link }}" target="_blank">
|
||||
{{ $mod->latestVersion->virus_total_link }}
|
||||
</a>
|
||||
</p>
|
||||
</li>
|
||||
@endif
|
||||
@if ($latestVersion->dependencies->isNotEmpty() && $latestVersion->dependencies->contains(fn($dependency) => $dependency->resolvedVersion?->mod))
|
||||
@if ($mod->latestVersion->dependencies->isNotEmpty() && $mod->latestVersion->dependencies->contains(fn($dependency) => $dependency->resolvedVersion?->mod))
|
||||
<li class="px-4 py-4 sm:px-0">
|
||||
<h3>{{ __('Latest Version Dependencies') }}</h3>
|
||||
<p class="truncate">
|
||||
@foreach ($latestVersion->dependencies as $dependency)
|
||||
@foreach ($mod->latestVersion->dependencies as $dependency)
|
||||
<a href="{{ $dependency->resolvedVersion->mod->detailUrl() }}">
|
||||
{{ $dependency->resolvedVersion->mod->name }} ({{ $dependency->resolvedVersion->version }})
|
||||
</a><br />
|
||||
|
312
tests/Feature/Mod/ModDependencyTest.php
Normal file
312
tests/Feature/Mod/ModDependencyTest.php
Normal file
@ -0,0 +1,312 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Mod;
|
||||
use App\Models\ModDependency;
|
||||
use App\Models\ModResolvedDependency;
|
||||
use App\Models\ModVersion;
|
||||
use App\Services\DependencyVersionService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('resolves mod version dependencies on create', function () {
|
||||
$modVersion = ModVersion::factory()->create();
|
||||
|
||||
$dependentMod = Mod::factory()->create();
|
||||
$dependentVersion1 = ModVersion::factory()->recycle($dependentMod)->create(['version' => '1.0.0']);
|
||||
$dependentVersion2 = ModVersion::factory()->recycle($dependentMod)->create(['version' => '2.0.0']);
|
||||
|
||||
// Create a dependency
|
||||
ModDependency::factory()->recycle([$modVersion, $dependentMod])->create([
|
||||
'constraint' => '^1.0', // Should resolve to dependentVersion1
|
||||
]);
|
||||
|
||||
// Check that the resolved dependency has been created
|
||||
expect(ModResolvedDependency::where('mod_version_id', $modVersion->id)->first())
|
||||
->not()->toBeNull()
|
||||
->resolved_mod_version_id->toBe($dependentVersion1->id);
|
||||
});
|
||||
|
||||
it('resolves multiple matching versions', function () {
|
||||
$modVersion = ModVersion::factory()->create();
|
||||
|
||||
$dependentMod = Mod::factory()->create();
|
||||
$dependentVersion1 = ModVersion::factory()->recycle($dependentMod)->create(['version' => '1.0.0']);
|
||||
$dependentVersion2 = ModVersion::factory()->recycle($dependentMod)->create(['version' => '1.1.0']);
|
||||
$dependentVersion3 = ModVersion::factory()->recycle($dependentMod)->create(['version' => '2.0.0']);
|
||||
|
||||
// Create a dependency
|
||||
ModDependency::factory()->recycle([$modVersion, $dependentMod])->create([
|
||||
'constraint' => '^1.0', // Should resolve to dependentVersion1 and dependentVersion2
|
||||
]);
|
||||
|
||||
$resolvedDependencies = ModResolvedDependency::where('mod_version_id', $modVersion->id)->get();
|
||||
|
||||
expect($resolvedDependencies->count())->toBe(2)
|
||||
->and($resolvedDependencies->pluck('resolved_mod_version_id'))
|
||||
->toContain($dependentVersion1->id)
|
||||
->toContain($dependentVersion2->id);
|
||||
});
|
||||
|
||||
it('does not resolve dependencies when no versions match', function () {
|
||||
$modVersion = ModVersion::factory()->create();
|
||||
|
||||
$dependentMod = Mod::factory()->create();
|
||||
ModVersion::factory()->recycle($dependentMod)->create(['version' => '2.0.0']);
|
||||
ModVersion::factory()->recycle($dependentMod)->create(['version' => '3.0.0']);
|
||||
|
||||
// Create a dependency
|
||||
ModDependency::factory()->recycle([$modVersion, $dependentMod])->create([
|
||||
'constraint' => '^1.0', // No versions match
|
||||
]);
|
||||
|
||||
// Check that no resolved dependencies were created
|
||||
expect(ModResolvedDependency::where('mod_version_id', $modVersion->id)->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
it('updates resolved dependencies when constraint changes', function () {
|
||||
$modVersion = ModVersion::factory()->create();
|
||||
|
||||
$dependentMod = Mod::factory()->create();
|
||||
$dependentVersion1 = ModVersion::factory()->recycle($dependentMod)->create(['version' => '1.0.0']);
|
||||
$dependentVersion2 = ModVersion::factory()->recycle($dependentMod)->create(['version' => '2.0.0']);
|
||||
|
||||
// Create a dependency with an initial constraint
|
||||
$dependency = ModDependency::factory()->recycle([$modVersion, $dependentMod])->create([
|
||||
'constraint' => '^1.0', // Should resolve to dependentVersion1
|
||||
]);
|
||||
|
||||
$resolvedDependency = ModResolvedDependency::where('mod_version_id', $modVersion->id)->first();
|
||||
expect($resolvedDependency->resolved_mod_version_id)->toBe($dependentVersion1->id);
|
||||
|
||||
// Update the constraint
|
||||
$dependency->update(['constraint' => '^2.0']); // Should now resolve to dependentVersion2
|
||||
|
||||
$resolvedDependency = ModResolvedDependency::where('mod_version_id', $modVersion->id)->first();
|
||||
expect($resolvedDependency->resolved_mod_version_id)->toBe($dependentVersion2->id);
|
||||
});
|
||||
|
||||
it('removes resolved dependencies when dependency is removed', function () {
|
||||
$modVersion = ModVersion::factory()->create();
|
||||
|
||||
$dependentMod = Mod::factory()->create();
|
||||
$dependentVersion1 = ModVersion::factory()->recycle($dependentMod)->create(['version' => '1.0.0']);
|
||||
|
||||
// Create a dependency
|
||||
$dependency = ModDependency::factory()->recycle([$modVersion, $dependentMod])->create([
|
||||
'constraint' => '^1.0',
|
||||
]);
|
||||
|
||||
$resolvedDependency = ModResolvedDependency::where('mod_version_id', $modVersion->id)->first();
|
||||
expect($resolvedDependency)->not()->toBeNull();
|
||||
|
||||
// Delete the dependency
|
||||
$dependency->delete();
|
||||
|
||||
// Check that the resolved dependency is removed
|
||||
expect(ModResolvedDependency::where('mod_version_id', $modVersion->id)->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
it('handles mod versions with no dependencies gracefully', function () {
|
||||
$serviceSpy = $this->spy(DependencyVersionService::class);
|
||||
|
||||
$modVersion = ModVersion::factory()->create(['version' => '1.0.0']);
|
||||
|
||||
// Check that the service was called and that no resolved dependencies were created.
|
||||
$serviceSpy->shouldHaveReceived('resolve');
|
||||
expect(ModResolvedDependency::where('mod_version_id', $modVersion->id)->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
it('resolves the correct versions with a complex semver constraint', function () {
|
||||
$modVersion = ModVersion::factory()->create(['version' => '1.0.0']);
|
||||
|
||||
$dependentMod = Mod::factory()->create();
|
||||
$dependentVersion1 = ModVersion::factory()->recycle($dependentMod)->create(['version' => '1.0.0']);
|
||||
$dependentVersion2 = ModVersion::factory()->recycle($dependentMod)->create(['version' => '1.2.0']);
|
||||
$dependentVersion3 = ModVersion::factory()->recycle($dependentMod)->create(['version' => '1.5.0']);
|
||||
$dependentVersion4 = ModVersion::factory()->recycle($dependentMod)->create(['version' => '2.0.0']);
|
||||
$dependentVersion5 = ModVersion::factory()->recycle($dependentMod)->create(['version' => '2.5.0']);
|
||||
|
||||
// Create a complex SemVer constraint
|
||||
ModDependency::factory()->recycle([$modVersion, $dependentMod])->create([
|
||||
'constraint' => '>1.0 <2.0 || >=2.5.0 <3.0', // Should resolve to dependentVersion2, dependentVersion3, and dependentVersion5
|
||||
]);
|
||||
|
||||
$resolvedDependencies = ModResolvedDependency::where('mod_version_id', $modVersion->id)->pluck('resolved_mod_version_id');
|
||||
|
||||
expect($resolvedDependencies)->toContain($dependentVersion2->id)
|
||||
->toContain($dependentVersion3->id)
|
||||
->toContain($dependentVersion5->id)
|
||||
->not->toContain($dependentVersion1->id)
|
||||
->not->toContain($dependentVersion4->id);
|
||||
});
|
||||
|
||||
it('resolves overlapping version constraints from multiple dependencies correctly', function () {
|
||||
$modVersion = ModVersion::factory()->create(['version' => '1.0.0']);
|
||||
|
||||
$dependentMod1 = Mod::factory()->create();
|
||||
$dependentVersion1_1 = ModVersion::factory()->recycle($dependentMod1)->create(['version' => '1.0.0']);
|
||||
$dependentVersion1_2 = ModVersion::factory()->recycle($dependentMod1)->create(['version' => '1.5.0']);
|
||||
|
||||
$dependentMod2 = Mod::factory()->create();
|
||||
$dependentVersion2_1 = ModVersion::factory()->recycle($dependentMod2)->create(['version' => '1.0.0']);
|
||||
$dependentVersion2_2 = ModVersion::factory()->recycle($dependentMod2)->create(['version' => '1.5.0']);
|
||||
|
||||
// Create two dependencies with overlapping constraints
|
||||
ModDependency::factory()->recycle([$modVersion, $dependentMod1])->create([
|
||||
'constraint' => '>=1.0 <2.0', // Matches both versions of dependentMod1
|
||||
]);
|
||||
|
||||
ModDependency::factory()->recycle([$modVersion, $dependentMod2])->create([
|
||||
'constraint' => '>=1.5.0 <2.0.0', // Matches only the second version of dependentMod2
|
||||
]);
|
||||
|
||||
$resolvedDependencies = ModResolvedDependency::where('mod_version_id', $modVersion->id)->get();
|
||||
|
||||
expect($resolvedDependencies->pluck('resolved_mod_version_id'))
|
||||
->toContain($dependentVersion1_1->id)
|
||||
->toContain($dependentVersion1_2->id)
|
||||
->toContain($dependentVersion2_2->id)
|
||||
->not->toContain($dependentVersion2_1->id);
|
||||
});
|
||||
|
||||
it('handles the case where a dependent mod has no versions available', function () {
|
||||
$modVersion = ModVersion::factory()->create(['version' => '1.0.0']);
|
||||
$dependentMod = Mod::factory()->create();
|
||||
|
||||
// Create a dependency where the dependent mod has no versions.
|
||||
ModDependency::factory()->recycle([$modVersion, $dependentMod])->create([
|
||||
'constraint' => '>=1.0.0',
|
||||
]);
|
||||
|
||||
// Verify that no versions were resolved.
|
||||
expect(ModResolvedDependency::where('mod_version_id', $modVersion->id)->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
it('handles a large number of versions efficiently', function () {
|
||||
$versionCount = 100;
|
||||
$modVersion = ModVersion::factory()->create();
|
||||
|
||||
$dependentMod = Mod::factory()->create();
|
||||
for ($i = 0; $i < $versionCount; $i++) {
|
||||
ModVersion::factory()->recycle($dependentMod)->create(['version' => "1.0.$i"]);
|
||||
}
|
||||
|
||||
// Create a dependency with a broad constraint
|
||||
ModDependency::factory()->recycle([$modVersion, $dependentMod])->create([
|
||||
'constraint' => '>=1.0.0',
|
||||
]);
|
||||
|
||||
// Verify that all versions were resolved
|
||||
expect(ModResolvedDependency::where('mod_version_id', $modVersion->id)->count())->toBe($versionCount);
|
||||
});
|
||||
|
||||
it('calls DependencyVersionService when a Mod is updated', function () {
|
||||
$mod = Mod::factory()->create();
|
||||
ModVersion::factory(2)->recycle($mod)->create();
|
||||
|
||||
$serviceSpy = $this->spy(DependencyVersionService::class);
|
||||
|
||||
$mod->update(['name' => 'New Mod Name']);
|
||||
|
||||
$mod->refresh();
|
||||
|
||||
expect($mod->versions)->toHaveCount(2);
|
||||
foreach ($mod->versions as $modVersion) {
|
||||
$serviceSpy->shouldReceive('resolve')->with($modVersion);
|
||||
}
|
||||
});
|
||||
|
||||
it('calls DependencyVersionService when a Mod is deleted', function () {
|
||||
$mod = Mod::factory()->create();
|
||||
ModVersion::factory(2)->recycle($mod)->create();
|
||||
|
||||
$serviceSpy = $this->spy(DependencyVersionService::class);
|
||||
|
||||
$mod->delete();
|
||||
|
||||
$mod->refresh();
|
||||
|
||||
expect($mod->versions)->toHaveCount(2);
|
||||
foreach ($mod->versions as $modVersion) {
|
||||
$serviceSpy->shouldReceive('resolve')->with($modVersion);
|
||||
}
|
||||
});
|
||||
|
||||
it('calls DependencyVersionService when a ModVersion is updated', function () {
|
||||
$modVersion = ModVersion::factory()->create();
|
||||
|
||||
$serviceSpy = $this->spy(DependencyVersionService::class);
|
||||
|
||||
$modVersion->update(['version' => '1.1.0']);
|
||||
|
||||
$serviceSpy->shouldHaveReceived('resolve');
|
||||
});
|
||||
|
||||
it('calls DependencyVersionService when a ModVersion is deleted', function () {
|
||||
$modVersion = ModVersion::factory()->create();
|
||||
|
||||
$serviceSpy = $this->spy(DependencyVersionService::class);
|
||||
|
||||
$modVersion->delete();
|
||||
|
||||
$serviceSpy->shouldHaveReceived('resolve');
|
||||
});
|
||||
|
||||
it('calls DependencyVersionService when a ModDependency is updated', function () {
|
||||
$modVersion = ModVersion::factory()->create(['version' => '1.0.0']);
|
||||
$dependentMod = Mod::factory()->create();
|
||||
$modDependency = ModDependency::factory()->recycle([$modVersion, $dependentMod])->create([
|
||||
'constraint' => '^1.0',
|
||||
]);
|
||||
|
||||
$serviceSpy = $this->spy(DependencyVersionService::class);
|
||||
|
||||
$modDependency->update(['constraint' => '^2.0']);
|
||||
|
||||
$serviceSpy->shouldHaveReceived('resolve');
|
||||
});
|
||||
|
||||
it('calls DependencyVersionService when a ModDependency is deleted', function () {
|
||||
$modVersion = ModVersion::factory()->create(['version' => '1.0.0']);
|
||||
$dependentMod = Mod::factory()->create();
|
||||
$modDependency = ModDependency::factory()->recycle([$modVersion, $dependentMod])->create([
|
||||
'constraint' => '^1.0',
|
||||
]);
|
||||
|
||||
$serviceSpy = $this->spy(DependencyVersionService::class);
|
||||
|
||||
$modDependency->delete();
|
||||
|
||||
$serviceSpy->shouldHaveReceived('resolve');
|
||||
});
|
||||
|
||||
it('displays the latest resolved dependencies on the mod detail page', function () {
|
||||
$dependentMod1 = Mod::factory()->create();
|
||||
$dependentMod1Version1 = ModVersion::factory()->recycle($dependentMod1)->create(['version' => '1.0.0']);
|
||||
$dependentMod1Version2 = ModVersion::factory()->recycle($dependentMod1)->create(['version' => '2.0.0']);
|
||||
|
||||
$dependentMod2 = Mod::factory()->create();
|
||||
$dependentMod2Version1 = ModVersion::factory()->recycle($dependentMod2)->create(['version' => '1.0.0']);
|
||||
$dependentMod2Version2 = ModVersion::factory()->recycle($dependentMod2)->create(['version' => '1.1.0']);
|
||||
$dependentMod2Version3 = ModVersion::factory()->recycle($dependentMod2)->create(['version' => '1.2.0']);
|
||||
$dependentMod2Version4 = ModVersion::factory()->recycle($dependentMod2)->create(['version' => '1.2.1']);
|
||||
|
||||
$mod = Mod::factory()->create();
|
||||
$mainModVersion = ModVersion::factory()->recycle($mod)->create();
|
||||
|
||||
ModDependency::factory()->recycle([$mainModVersion, $dependentMod1])->create(['constraint' => '>=1.0.0']);
|
||||
ModDependency::factory()->recycle([$mainModVersion, $dependentMod2])->create(['constraint' => '>=1.0.0']);
|
||||
|
||||
$mainModVersion->load('latestResolvedDependencies');
|
||||
|
||||
expect($mainModVersion->latestResolvedDependencies)->toHaveCount(2)
|
||||
->and($mainModVersion->latestResolvedDependencies->pluck('version'))
|
||||
->toContain($dependentMod1Version2->version) // Latest version of dependentMod1
|
||||
->toContain($dependentMod2Version4->version); // Latest version of dependentMod2
|
||||
|
||||
$response = $this->get(route('mod.show', ['mod' => $mod->id, 'slug' => $mod->slug]));
|
||||
|
||||
$response->assertSeeInOrder(explode(' ', __('Dependencies: ')."$dependentMod1->name ($dependentMod1Version2->version)"));
|
||||
$response->assertSeeInOrder(explode(' ', __('Dependencies: ')."$dependentMod2->name ($dependentMod2Version4->version)"));
|
||||
});
|
161
tests/Feature/Mod/ModFilterTest.php
Normal file
161
tests/Feature/Mod/ModFilterTest.php
Normal file
@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Filters\ModFilter;
|
||||
use App\Models\Mod;
|
||||
use App\Models\ModVersion;
|
||||
use App\Models\SptVersion;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('filters mods by a single SPT version', function () {
|
||||
$sptVersion1 = SptVersion::factory()->create(['version' => '1.0.0']);
|
||||
$sptVersion2 = SptVersion::factory()->create(['version' => '2.0.0']);
|
||||
|
||||
$mod1 = Mod::factory()->create();
|
||||
$modVersion1 = ModVersion::factory()->recycle($mod1)->create([
|
||||
'spt_version_constraint' => '1.0.0', // Constraint matching sptVersion1
|
||||
]);
|
||||
|
||||
$mod2 = Mod::factory()->create();
|
||||
$modVersion2 = ModVersion::factory()->recycle($mod2)->create([
|
||||
'spt_version_constraint' => '2.0.0', // Constraint matching sptVersion2
|
||||
]);
|
||||
|
||||
// Confirm associations created by observers and services
|
||||
expect($modVersion1->sptVersions->pluck('version')->toArray())->toContain($sptVersion1->version)
|
||||
->and($modVersion2->sptVersions->pluck('version')->toArray())->toContain($sptVersion2->version);
|
||||
|
||||
// Apply the filter
|
||||
$filters = ['sptVersions' => [$sptVersion1->version]];
|
||||
$filteredMods = (new ModFilter($filters))->apply()->get();
|
||||
|
||||
// Assert that only the correct mod is returned
|
||||
expect($filteredMods)->toHaveCount(1)
|
||||
->and($filteredMods->first()->id)->toBe($mod1->id);
|
||||
});
|
||||
|
||||
it('filters mods by multiple SPT versions', function () {
|
||||
// Create the SPT versions
|
||||
$sptVersion1 = SptVersion::factory()->create(['version' => '1.0.0']);
|
||||
$sptVersion2 = SptVersion::factory()->create(['version' => '2.0.0']);
|
||||
$sptVersion3 = SptVersion::factory()->create(['version' => '3.0.0']);
|
||||
|
||||
// Create the mods and their versions with appropriate constraints
|
||||
$mod1 = Mod::factory()->create();
|
||||
$modVersion1 = ModVersion::factory()->recycle($mod1)->create([
|
||||
'spt_version_constraint' => '1.0.0', // Constraint matching sptVersion1
|
||||
]);
|
||||
|
||||
$mod2 = Mod::factory()->create();
|
||||
$modVersion2 = ModVersion::factory()->recycle($mod2)->create([
|
||||
'spt_version_constraint' => '2.0.0', // Constraint matching sptVersion2
|
||||
]);
|
||||
|
||||
$mod3 = Mod::factory()->create();
|
||||
$modVersion3 = ModVersion::factory()->recycle($mod3)->create([
|
||||
'spt_version_constraint' => '3.0.0', // Constraint matching sptVersion3
|
||||
]);
|
||||
|
||||
// Confirm associations created by observers and services
|
||||
expect($modVersion1->sptVersions->pluck('version')->toArray())->toContain($sptVersion1->version)
|
||||
->and($modVersion2->sptVersions->pluck('version')->toArray())->toContain($sptVersion2->version)
|
||||
->and($modVersion3->sptVersions->pluck('version')->toArray())->toContain($sptVersion3->version);
|
||||
|
||||
// Apply the filter with multiple SPT versions
|
||||
$filters = ['sptVersions' => [$sptVersion1->version, $sptVersion3->version]];
|
||||
$filteredMods = (new ModFilter($filters))->apply()->get();
|
||||
|
||||
// Assert that the correct mods are returned
|
||||
expect($filteredMods)->toHaveCount(2)
|
||||
->and($filteredMods->pluck('id')->toArray())->toContain($mod1->id, $mod3->id);
|
||||
});
|
||||
|
||||
it('returns no mods when no SPT versions match', function () {
|
||||
// Create the SPT versions
|
||||
$sptVersion1 = SptVersion::factory()->create(['version' => '1.0.0']);
|
||||
$sptVersion2 = SptVersion::factory()->create(['version' => '2.0.0']);
|
||||
|
||||
// Create the mods and their versions with appropriate constraints
|
||||
$mod1 = Mod::factory()->create();
|
||||
$modVersion1 = ModVersion::factory()->recycle($mod1)->create([
|
||||
'spt_version_constraint' => '1.0.0', // Constraint matching sptVersion1
|
||||
]);
|
||||
|
||||
$mod2 = Mod::factory()->create();
|
||||
$modVersion2 = ModVersion::factory()->recycle($mod2)->create([
|
||||
'spt_version_constraint' => '2.0.0', // Constraint matching sptVersion2
|
||||
]);
|
||||
|
||||
// Confirm associations created by observers and services
|
||||
expect($modVersion1->sptVersions->pluck('version')->toArray())->toContain($sptVersion1->version)
|
||||
->and($modVersion2->sptVersions->pluck('version')->toArray())->toContain($sptVersion2->version);
|
||||
|
||||
// Apply the filter with a non-matching SPT version
|
||||
$filters = ['sptVersions' => ['3.0.0']]; // Version '3.0.0' does not exist in associations
|
||||
$filteredMods = (new ModFilter($filters))->apply()->get();
|
||||
|
||||
// Assert that no mods are returned
|
||||
expect($filteredMods)->toBeEmpty();
|
||||
});
|
||||
|
||||
it('filters mods correctly with combined filters', function () {
|
||||
// Create the SPT versions
|
||||
$sptVersion1 = SptVersion::factory()->create(['version' => '1.0.0']);
|
||||
$sptVersion2 = SptVersion::factory()->create(['version' => '2.0.0']);
|
||||
|
||||
// Create the mods and their versions with appropriate names and featured status
|
||||
$mod1 = Mod::factory()->create(['name' => 'Awesome Mod', 'featured' => true]);
|
||||
$modVersion1 = ModVersion::factory()->recycle($mod1)->create([
|
||||
'spt_version_constraint' => '1.0.0', // Constraint matching sptVersion1
|
||||
]);
|
||||
|
||||
$mod2 = Mod::factory()->create(['name' => 'Cool Mod', 'featured' => false]);
|
||||
$modVersion2 = ModVersion::factory()->recycle($mod2)->create([
|
||||
'spt_version_constraint' => '2.0.0', // Constraint matching sptVersion2
|
||||
]);
|
||||
|
||||
// Confirm associations created by observers and services
|
||||
expect($modVersion1->sptVersions->pluck('version')->toArray())->toContain($sptVersion1->version)
|
||||
->and($modVersion2->sptVersions->pluck('version')->toArray())->toContain($sptVersion2->version);
|
||||
|
||||
// Apply combined filters
|
||||
$filters = [
|
||||
'query' => 'Awesome',
|
||||
'featured' => 'only',
|
||||
'sptVersions' => [$sptVersion1->version],
|
||||
];
|
||||
$filteredMods = (new ModFilter($filters))->apply()->get();
|
||||
|
||||
// Assert that only the correct mod is returned
|
||||
expect($filteredMods)->toHaveCount(1)
|
||||
->and($filteredMods->first()->id)->toBe($mod1->id);
|
||||
});
|
||||
|
||||
it('handles an empty SPT versions array correctly', function () {
|
||||
// Create the SPT versions
|
||||
$sptVersion1 = SptVersion::factory()->create(['version' => '1.0.0']);
|
||||
$sptVersion2 = SptVersion::factory()->create(['version' => '2.0.0']);
|
||||
|
||||
// Create the mods and their versions with appropriate constraints
|
||||
$mod1 = Mod::factory()->create();
|
||||
$modVersion1 = ModVersion::factory()->recycle($mod1)->create([
|
||||
'spt_version_constraint' => '1.0.0', // Constraint matching sptVersion1
|
||||
]);
|
||||
|
||||
$mod2 = Mod::factory()->create();
|
||||
$modVersion2 = ModVersion::factory()->recycle($mod2)->create([
|
||||
'spt_version_constraint' => '2.0.0', // Constraint matching sptVersion2
|
||||
]);
|
||||
|
||||
// Confirm associations created by observers and services
|
||||
expect($modVersion1->sptVersions->pluck('version')->toArray())->toContain($sptVersion1->version)
|
||||
->and($modVersion2->sptVersions->pluck('version')->toArray())->toContain($sptVersion2->version);
|
||||
|
||||
// Apply the filter with an empty SPT versions array
|
||||
$filters = ['sptVersions' => []];
|
||||
$filteredMods = (new ModFilter($filters))->apply()->get();
|
||||
|
||||
// Assert that the behavior is as expected (return all mods, or none, depending on intended behavior)
|
||||
expect($filteredMods)->toHaveCount(2); // Modify this assertion to reflect your desired behavior
|
||||
});
|
52
tests/Feature/Mod/ModTest.php
Normal file
52
tests/Feature/Mod/ModTest.php
Normal file
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Mod;
|
||||
use App\Models\ModVersion;
|
||||
use App\Models\SptVersion;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('displays homepage mod cards with the latest supported spt version number', function () {
|
||||
$sptVersion1 = SptVersion::factory()->create(['version' => '1.0.0']);
|
||||
$sptVersion2 = SptVersion::factory()->create(['version' => '2.0.0']);
|
||||
$sptVersion3 = SptVersion::factory()->create(['version' => '3.0.0']);
|
||||
|
||||
$mod1 = Mod::factory()->create();
|
||||
ModVersion::factory()->recycle($mod1)->create(['spt_version_constraint' => $sptVersion1->version]);
|
||||
ModVersion::factory()->recycle($mod1)->create(['spt_version_constraint' => $sptVersion1->version]);
|
||||
ModVersion::factory()->recycle($mod1)->create(['spt_version_constraint' => $sptVersion2->version]);
|
||||
ModVersion::factory()->recycle($mod1)->create(['spt_version_constraint' => $sptVersion2->version]);
|
||||
ModVersion::factory()->recycle($mod1)->create(['spt_version_constraint' => $sptVersion3->version]);
|
||||
ModVersion::factory()->recycle($mod1)->create(['spt_version_constraint' => $sptVersion3->version]);
|
||||
|
||||
$response = $this->get(route('home'));
|
||||
|
||||
$response->assertSeeInOrder(explode(' ', "$mod1->name $sptVersion3->version_formatted"));
|
||||
});
|
||||
|
||||
it('displays the latest version on the mod detail page', function () {
|
||||
$versions = [
|
||||
'1.0.0',
|
||||
'1.1.0',
|
||||
'1.2.0',
|
||||
'2.0.0',
|
||||
'2.1.0',
|
||||
];
|
||||
$latestVersion = max($versions);
|
||||
|
||||
$mod = Mod::factory()->create();
|
||||
foreach ($versions as $version) {
|
||||
ModVersion::factory()->recycle($mod)->create(['version' => $version]);
|
||||
}
|
||||
|
||||
$response = $this->get($mod->detailUrl());
|
||||
|
||||
expect($latestVersion)->toBe('2.1.0');
|
||||
|
||||
// Assert the latest version is next to the mod's name
|
||||
$response->assertSeeInOrder(explode(' ', "$mod->name $latestVersion"));
|
||||
|
||||
// Assert the latest version is in the latest download button
|
||||
$response->assertSeeText(__('Download Latest Version')." ($latestVersion)");
|
||||
});
|
@ -7,7 +7,7 @@ use Illuminate\Support\Carbon;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('resolves spt version when mod version is created', function () {
|
||||
it('resolves spt versions when mod version is created', function () {
|
||||
SptVersion::factory()->create(['version' => '1.0.0']);
|
||||
SptVersion::factory()->create(['version' => '1.1.0']);
|
||||
SptVersion::factory()->create(['version' => '1.1.1']);
|
||||
@ -17,11 +17,13 @@ it('resolves spt version when mod version is created', function () {
|
||||
|
||||
$modVersion->refresh();
|
||||
|
||||
expect($modVersion->resolved_spt_version_id)->not->toBeNull();
|
||||
expect($modVersion->sptVersion->version)->toBe('1.1.1');
|
||||
$sptVersions = $modVersion->sptVersions;
|
||||
|
||||
expect($sptVersions)->toHaveCount(2)
|
||||
->and($sptVersions->pluck('version'))->toContain('1.1.0', '1.1.1');
|
||||
});
|
||||
|
||||
it('resolves spt version when constraint is updated', function () {
|
||||
it('resolves spt versions when constraint is updated', function () {
|
||||
SptVersion::factory()->create(['version' => '1.0.0']);
|
||||
SptVersion::factory()->create(['version' => '1.1.0']);
|
||||
SptVersion::factory()->create(['version' => '1.1.1']);
|
||||
@ -29,19 +31,25 @@ it('resolves spt version when constraint is updated', function () {
|
||||
|
||||
$modVersion = ModVersion::factory()->create(['spt_version_constraint' => '~1.1.0']);
|
||||
|
||||
expect($modVersion->resolved_spt_version_id)->not->toBeNull();
|
||||
expect($modVersion->sptVersion->version)->toBe('1.1.1');
|
||||
$modVersion->refresh();
|
||||
|
||||
$sptVersions = $modVersion->sptVersions;
|
||||
|
||||
expect($sptVersions)->toHaveCount(2)
|
||||
->and($sptVersions->pluck('version'))->toContain('1.1.0', '1.1.1');
|
||||
|
||||
$modVersion->spt_version_constraint = '~1.2.0';
|
||||
$modVersion->save();
|
||||
|
||||
$modVersion->refresh();
|
||||
|
||||
expect($modVersion->resolved_spt_version_id)->not->toBeNull();
|
||||
expect($modVersion->sptVersion->version)->toBe('1.2.0');
|
||||
$sptVersions = $modVersion->sptVersions;
|
||||
|
||||
expect($sptVersions)->toHaveCount(1)
|
||||
->and($sptVersions->pluck('version'))->toContain('1.2.0');
|
||||
});
|
||||
|
||||
it('resolves spt version when spt version is created', function () {
|
||||
it('resolves spt versions when spt version is created', function () {
|
||||
SptVersion::factory()->create(['version' => '1.0.0']);
|
||||
SptVersion::factory()->create(['version' => '1.1.0']);
|
||||
SptVersion::factory()->create(['version' => '1.1.1']);
|
||||
@ -49,17 +57,24 @@ it('resolves spt version when spt version is created', function () {
|
||||
|
||||
$modVersion = ModVersion::factory()->create(['spt_version_constraint' => '~1.1.0']);
|
||||
|
||||
expect($modVersion->resolved_spt_version_id)->not->toBeNull();
|
||||
expect($modVersion->sptVersion->version)->toBe('1.1.1');
|
||||
$modVersion->refresh();
|
||||
|
||||
$sptVersions = $modVersion->sptVersions;
|
||||
|
||||
expect($sptVersions)->toHaveCount(2)
|
||||
->and($sptVersions->pluck('version'))->toContain('1.1.0', '1.1.1');
|
||||
|
||||
SptVersion::factory()->create(['version' => '1.1.2']);
|
||||
|
||||
$modVersion->refresh();
|
||||
|
||||
expect($modVersion->sptVersion->version)->toBe('1.1.2');
|
||||
$sptVersions = $modVersion->sptVersions;
|
||||
|
||||
expect($sptVersions)->toHaveCount(3)
|
||||
->and($sptVersions->pluck('version'))->toContain('1.1.0', '1.1.1', '1.1.2');
|
||||
});
|
||||
|
||||
it('resolves spt version when spt version is deleted', function () {
|
||||
it('resolves spt versions when spt version is deleted', function () {
|
||||
SptVersion::factory()->create(['version' => '1.0.0']);
|
||||
SptVersion::factory()->create(['version' => '1.1.0']);
|
||||
SptVersion::factory()->create(['version' => '1.1.1']);
|
||||
@ -67,13 +82,19 @@ it('resolves spt version when spt version is deleted', function () {
|
||||
|
||||
$modVersion = ModVersion::factory()->create(['spt_version_constraint' => '~1.1.0']);
|
||||
|
||||
expect($modVersion->resolved_spt_version_id)->not->toBeNull();
|
||||
expect($modVersion->sptVersion->version)->toBe('1.1.2');
|
||||
$modVersion->refresh();
|
||||
$sptVersions = $modVersion->sptVersions;
|
||||
|
||||
expect($sptVersions)->toHaveCount(3)
|
||||
->and($sptVersions->pluck('version'))->toContain('1.1.0', '1.1.1', '1.1.2');
|
||||
|
||||
$sptVersion->delete();
|
||||
$modVersion->refresh();
|
||||
|
||||
expect($modVersion->sptVersion->version)->toBe('1.1.1');
|
||||
$modVersion->refresh();
|
||||
$sptVersions = $modVersion->sptVersions;
|
||||
|
||||
expect($sptVersions)->toHaveCount(2)
|
||||
->and($sptVersions->pluck('version'))->toContain('1.1.0', '1.1.1');
|
||||
});
|
||||
|
||||
it('includes only published mod versions', function () {
|
@ -1,266 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Exceptions\CircularDependencyException;
|
||||
use App\Models\Mod;
|
||||
use App\Models\ModDependency;
|
||||
use App\Models\ModVersion;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('resolves mod version dependency when mod version is created', function () {
|
||||
$modA = Mod::factory()->create(['name' => 'Mod A']);
|
||||
$modB = Mod::factory()->create(['name' => 'Mod B']);
|
||||
|
||||
// Create versions for Mod B
|
||||
ModVersion::factory()->recycle($modB)->create(['version' => '1.0.0']);
|
||||
ModVersion::factory()->recycle($modB)->create(['version' => '1.1.0']);
|
||||
ModVersion::factory()->recycle($modB)->create(['version' => '1.1.1']);
|
||||
ModVersion::factory()->recycle($modB)->create(['version' => '2.0.0']);
|
||||
|
||||
// Create versions for Mod A that depends on Mod B
|
||||
$modAv1 = ModVersion::factory()->recycle($modA)->create(['version' => '1.0.0']);
|
||||
$modDependency = ModDependency::factory()->recycle([$modAv1, $modB])->create([
|
||||
'version_constraint' => '^1.0.0',
|
||||
]);
|
||||
|
||||
$modDependency->refresh();
|
||||
|
||||
expect($modDependency->resolvedVersion->version)->toBe('1.1.1');
|
||||
});
|
||||
|
||||
it('resolves mod version dependency when mod version is updated', function () {
|
||||
$modA = Mod::factory()->create(['name' => 'Mod A']);
|
||||
$modB = Mod::factory()->create(['name' => 'Mod B']);
|
||||
|
||||
// Create versions for Mod B
|
||||
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.0.0']);
|
||||
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.1.0']);
|
||||
$modBv3 = ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.1.1']);
|
||||
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '2.0.0']);
|
||||
|
||||
// Create versions for Mod A that depends on Mod B
|
||||
$modAv1 = ModVersion::factory()->create(['mod_id' => $modA->id, 'version' => '1.0.0']);
|
||||
$modDependency = ModDependency::factory()->recycle([$modAv1, $modB])->create([
|
||||
'version_constraint' => '^1.0.0',
|
||||
]);
|
||||
|
||||
$modDependency->refresh();
|
||||
expect($modDependency->resolvedVersion->version)->toBe('1.1.1');
|
||||
|
||||
// Update the mod B version
|
||||
$modBv3->update(['version' => '1.1.2']);
|
||||
|
||||
$modDependency->refresh();
|
||||
expect($modDependency->resolvedVersion->version)->toBe('1.1.2');
|
||||
});
|
||||
|
||||
it('resolves mod version dependency when mod version is deleted', function () {
|
||||
$modA = Mod::factory()->create(['name' => 'Mod A']);
|
||||
$modB = Mod::factory()->create(['name' => 'Mod B']);
|
||||
|
||||
// Create versions for Mod B
|
||||
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.0.0']);
|
||||
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.1.0']);
|
||||
$modBv3 = ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.1.1']);
|
||||
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '2.0.0']);
|
||||
|
||||
// Create versions for Mod A that depends on Mod B
|
||||
$modAv1 = ModVersion::factory()->create(['mod_id' => $modA->id, 'version' => '1.0.0']);
|
||||
$modDependency = ModDependency::factory()->recycle([$modAv1, $modB])->create([
|
||||
'version_constraint' => '^1.0.0',
|
||||
]);
|
||||
|
||||
$modDependency->refresh();
|
||||
expect($modDependency->resolvedVersion->version)->toBe('1.1.1');
|
||||
|
||||
// Delete the mod B version
|
||||
$modBv3->delete();
|
||||
|
||||
$modDependency->refresh();
|
||||
expect($modDependency->resolvedVersion->version)->toBe('1.1.0');
|
||||
});
|
||||
|
||||
it('resolves mod version dependency after semantic version constraint is updated', function () {
|
||||
$modA = Mod::factory()->create(['name' => 'Mod A']);
|
||||
$modB = Mod::factory()->create(['name' => 'Mod B']);
|
||||
|
||||
// Create versions for Mod B
|
||||
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.0.0']);
|
||||
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.1.0']);
|
||||
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.1.1']);
|
||||
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '2.0.0']);
|
||||
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '2.0.1']);
|
||||
|
||||
// Create versions for Mod A that depends on Mod B
|
||||
$modAv1 = ModVersion::factory()->create(['mod_id' => $modA->id, 'version' => '1.0.0']);
|
||||
$modDependency = ModDependency::factory()->recycle([$modAv1, $modB])->create([
|
||||
'version_constraint' => '^1.0.0',
|
||||
]);
|
||||
|
||||
$modDependency->refresh();
|
||||
expect($modDependency->resolvedVersion->version)->toBe('1.1.1');
|
||||
|
||||
// Update the dependency version constraint
|
||||
$modDependency->update(['version_constraint' => '^2.0.0']);
|
||||
|
||||
$modDependency->refresh();
|
||||
expect($modDependency->resolvedVersion->version)->toBe('2.0.1');
|
||||
});
|
||||
|
||||
it('resolves mod version dependency with exact semantic version constraint', function () {
|
||||
$modA = Mod::factory()->create(['name' => 'Mod A']);
|
||||
$modB = Mod::factory()->create(['name' => 'Mod B']);
|
||||
|
||||
// Create versions for Mod B
|
||||
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.0.0']);
|
||||
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.1.0']);
|
||||
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.1.1']);
|
||||
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '2.0.0']);
|
||||
|
||||
// Create versions for Mod A that depends on Mod B
|
||||
$modAv1 = ModVersion::factory()->create(['mod_id' => $modA->id, 'version' => '1.0.0']);
|
||||
$modDependency = ModDependency::factory()->recycle([$modAv1, $modB])->create([
|
||||
'version_constraint' => '1.1.0',
|
||||
]);
|
||||
|
||||
$modDependency->refresh();
|
||||
expect($modDependency->resolvedVersion->version)->toBe('1.1.0');
|
||||
});
|
||||
|
||||
it('resolves mod version dependency with complex semantic version constraint', function () {
|
||||
$modA = Mod::factory()->create(['name' => 'Mod A']);
|
||||
$modB = Mod::factory()->create(['name' => 'Mod B']);
|
||||
|
||||
// Create versions for Mod B
|
||||
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.0.0']);
|
||||
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.1.0']);
|
||||
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.1.1']);
|
||||
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.2.0']);
|
||||
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.2.1']);
|
||||
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '2.0.0']);
|
||||
|
||||
// Create versions for Mod A that depends on Mod B
|
||||
$modAv1 = ModVersion::factory()->create(['mod_id' => $modA->id, 'version' => '1.0.0']);
|
||||
$modDependency = ModDependency::factory()->recycle([$modAv1, $modB])->create([
|
||||
'version_constraint' => '>=1.0.0 <2.0.0',
|
||||
]);
|
||||
|
||||
$modDependency->refresh();
|
||||
expect($modDependency->resolvedVersion->version)->toBe('1.2.1');
|
||||
|
||||
$modDependency->update(['version_constraint' => '1.0.0 || >=1.1.0 <1.2.0']);
|
||||
|
||||
$modDependency->refresh();
|
||||
expect($modDependency->resolvedVersion->version)->toBe('1.1.1');
|
||||
});
|
||||
|
||||
it('resolves previously unresolved mod version dependency after semantic version constraint is updated', function () {
|
||||
$modA = Mod::factory()->create(['name' => 'Mod A']);
|
||||
$modB = Mod::factory()->create(['name' => 'Mod B']);
|
||||
|
||||
// Create versions for Mod B
|
||||
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.0.0']);
|
||||
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.1.0']);
|
||||
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.1.1']);
|
||||
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '2.0.0']);
|
||||
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '2.0.1']);
|
||||
|
||||
// Create version for Mod A that depends on Mod B, but no version satisfies the constraint.
|
||||
$modAv1 = ModVersion::factory()->create(['mod_id' => $modA->id, 'version' => '1.0.0']);
|
||||
$modDependency = ModDependency::factory()->recycle([$modAv1, $modB])->create([
|
||||
'version_constraint' => '^3.0.0',
|
||||
]);
|
||||
|
||||
$modDependency->refresh();
|
||||
expect($modDependency->resolved_version_id)->toBeNull();
|
||||
|
||||
// Update the dependency version constraint
|
||||
$modDependency->update(['version_constraint' => '^2.0.0']);
|
||||
|
||||
$modDependency->refresh();
|
||||
expect($modDependency->resolved_version_id)->not->toBeNull();
|
||||
expect($modDependency->resolvedVersion->version)->toBe('2.0.1');
|
||||
});
|
||||
|
||||
it('resolves null when no mod versions are available', function () {
|
||||
$modA = Mod::factory()->create(['name' => 'Mod A']);
|
||||
$modB = Mod::factory()->create(['name' => 'Mod B']);
|
||||
|
||||
// Create version for Mod A that has no resolvable dependency
|
||||
$modAv1 = ModVersion::factory()->create(['mod_id' => $modA->id, 'version' => '1.0.0']);
|
||||
$modDependency = ModDependency::factory()->recycle([$modAv1, $modB])->create([
|
||||
'version_constraint' => '^1.0.0',
|
||||
]);
|
||||
|
||||
$modDependency->refresh();
|
||||
expect($modDependency->resolved_version_id)->toBeNull();
|
||||
});
|
||||
|
||||
it('resolves null when no mod versions match against semantic version constraint', function () {
|
||||
$modA = Mod::factory()->create(['name' => 'Mod A']);
|
||||
$modB = Mod::factory()->create(['name' => 'Mod B']);
|
||||
|
||||
// Create versions for Mod B
|
||||
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.0.0']);
|
||||
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.1.0']);
|
||||
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '2.0.0']);
|
||||
|
||||
// Create version for Mod A that has no resolvable dependency
|
||||
$modAv1 = ModVersion::factory()->create(['mod_id' => $modA->id, 'version' => '1.0.0']);
|
||||
$modDependency = ModDependency::factory()->recycle([$modAv1, $modB])->create([
|
||||
'version_constraint' => '~1.2.0',
|
||||
]);
|
||||
|
||||
$modDependency->refresh();
|
||||
expect($modDependency->resolved_version_id)->toBeNull();
|
||||
});
|
||||
|
||||
it('resolves multiple dependencies', function () {
|
||||
$modA = Mod::factory()->create(['name' => 'Mod A']);
|
||||
$modB = Mod::factory()->create(['name' => 'Mod B']);
|
||||
$modC = Mod::factory()->create(['name' => 'Mod C']);
|
||||
|
||||
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.0.0']);
|
||||
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.1.0']);
|
||||
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.1.1']);
|
||||
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '2.0.0']);
|
||||
|
||||
ModVersion::factory()->create(['mod_id' => $modC->id, 'version' => '1.0.0']);
|
||||
ModVersion::factory()->create(['mod_id' => $modC->id, 'version' => '1.1.0']);
|
||||
ModVersion::factory()->create(['mod_id' => $modC->id, 'version' => '1.1.1']);
|
||||
ModVersion::factory()->create(['mod_id' => $modC->id, 'version' => '2.0.0']);
|
||||
|
||||
// Creating a version for Mod A that depends on Mod B and Mod C
|
||||
$modAv1 = ModVersion::factory()->create(['mod_id' => $modA->id, 'version' => '1.0.0']);
|
||||
|
||||
$modDependencyB = ModDependency::factory()->recycle([$modAv1, $modB])->create([
|
||||
'version_constraint' => '^1.0.0',
|
||||
]);
|
||||
$modDependencyC = ModDependency::factory()->recycle([$modAv1, $modC])->create([
|
||||
'version_constraint' => '^1.0.0',
|
||||
]);
|
||||
|
||||
$modDependencyB->refresh();
|
||||
expect($modDependencyB->resolvedVersion->version)->toBe('1.1.1');
|
||||
|
||||
$modDependencyC->refresh();
|
||||
expect($modDependencyC->resolvedVersion->version)->toBe('1.1.1');
|
||||
});
|
||||
|
||||
it('throws exception when there is a circular version dependency', function () {
|
||||
$modA = Mod::factory()->create(['name' => 'Mod A']);
|
||||
$modAv1 = ModVersion::factory()->recycle($modA)->create(['version' => '1.0.0']);
|
||||
|
||||
$modB = Mod::factory()->create(['name' => 'Mod B']);
|
||||
$modBv1 = ModVersion::factory()->recycle($modB)->create(['version' => '1.0.0']);
|
||||
|
||||
$modDependencyAtoB = ModDependency::factory()->recycle([$modAv1, $modB])->create([
|
||||
'version_constraint' => '1.0.0',
|
||||
]);
|
||||
|
||||
// Create circular dependencies
|
||||
$modDependencyBtoA = ModDependency::factory()->recycle([$modBv1, $modA])->create([
|
||||
'version_constraint' => '1.0.0',
|
||||
]);
|
||||
})->throws(CircularDependencyException::class);
|
@ -1,54 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Mod;
|
||||
use App\Models\ModVersion;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('can retrieve all unresolved versions', function () {
|
||||
// Create a mod instance
|
||||
$mod = Mod::factory()->create();
|
||||
ModVersion::factory(5)->recycle($mod)->create();
|
||||
|
||||
ModVersion::all()->each(function (ModVersion $modVersion) {
|
||||
$modVersion->resolved_spt_version_id = null;
|
||||
$modVersion->saveQuietly();
|
||||
});
|
||||
|
||||
$unresolvedMix = $mod->versions(resolvedOnly: false);
|
||||
|
||||
$unresolvedMix->each(function (ModVersion $modVersion) {
|
||||
expect($modVersion)->toBeInstanceOf(ModVersion::class)
|
||||
->and($modVersion->resolved_spt_version_id)->toBeNull();
|
||||
});
|
||||
|
||||
expect($unresolvedMix->count())->toBe(5)
|
||||
->and($mod->versions->count())->toBe(0);
|
||||
});
|
||||
|
||||
it('shows the latest version on the mod detail page', function () {
|
||||
$versions = [
|
||||
'1.0.0',
|
||||
'1.1.0',
|
||||
'1.2.0',
|
||||
'2.0.0',
|
||||
'2.1.0',
|
||||
];
|
||||
$latestVersion = max($versions);
|
||||
|
||||
$mod = Mod::factory()->create();
|
||||
foreach ($versions as $version) {
|
||||
ModVersion::factory()->sptVersionResolved()->recycle($mod)->create(['version' => $version]);
|
||||
}
|
||||
|
||||
$response = $this->get($mod->detailUrl());
|
||||
|
||||
expect($latestVersion)->toBe('2.1.0');
|
||||
|
||||
// Assert the latest version is next to the mod's name
|
||||
$response->assertSeeInOrder(explode(' ', "$mod->name $latestVersion"));
|
||||
|
||||
// Assert the latest version is in the latest download button
|
||||
$response->assertSeeText(__('Download Latest Version')." ($latestVersion)");
|
||||
});
|
@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
use function Pest\Stressless\stress;
|
||||
|
||||
it('homepage has a fast response time', function () {
|
||||
$result = stress('/');
|
||||
|
||||
expect($result->requests()->duration()->med())->toBeLessThan(100);
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user