diff --git a/app/Http/Controllers/ModController.php b/app/Http/Controllers/ModController.php index 777f095..f019ad4 100644 --- a/app/Http/Controllers/ModController.php +++ b/app/Http/Controllers/ModController.php @@ -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) diff --git a/app/Http/Filters/ModFilter.php b/app/Http/Filters/ModFilter.php index a162a94..561ef24 100644 --- a/app/Http/Filters/ModFilter.php +++ b/app/Http/Filters/ModFilter.php @@ -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); }); } } diff --git a/app/Jobs/ImportHubDataJob.php b/app/Jobs/ImportHubDataJob.php index f6b9dc6..2dff469 100644 --- a/app/Jobs/ImportHubDataJob.php +++ b/app/Jobs/ImportHubDataJob.php @@ -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', diff --git a/app/Livewire/Mod/Index.php b/app/Livewire/Mod/Index.php index 92d1fbb..3cc2c34 100644 --- a/app/Livewire/Mod/Index.php +++ b/app/Livewire/Mod/Index.php @@ -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; } diff --git a/app/Models/Mod.php b/app/Models/Mod.php index 57596b0..5261c14 100644 --- a/app/Models/Mod.php +++ b/app/Models/Mod.php @@ -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; } /** diff --git a/app/Models/ModDependency.php b/app/Models/ModDependency.php index 47430da..e147196 100644 --- a/app/Models/ModDependency.php +++ b/app/Models/ModDependency.php @@ -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'); } } diff --git a/app/Models/ModResolvedDependency.php b/app/Models/ModResolvedDependency.php new file mode 100644 index 0000000..ee6e747 --- /dev/null +++ b/app/Models/ModResolvedDependency.php @@ -0,0 +1,33 @@ +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'); + } +} diff --git a/app/Models/ModVersion.php b/app/Models/ModVersion.php index 149d9d9..c834a41 100644 --- a/app/Models/ModVersion.php +++ b/app/Models/ModVersion.php @@ -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'); } } diff --git a/app/Models/SptVersion.php b/app/Models/SptVersion.php index b1a27f7..42bd18b 100644 --- a/app/Models/SptVersion.php +++ b/app/Models/SptVersion.php @@ -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'); } /** diff --git a/app/Observers/ModDependencyObserver.php b/app/Observers/ModDependencyObserver.php index 353e873..49c3425 100644 --- a/app/Observers/ModDependencyObserver.php +++ b/app/Observers/ModDependencyObserver.php @@ -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); } } diff --git a/app/Observers/ModObserver.php b/app/Observers/ModObserver.php new file mode 100644 index 0000000..77e3947 --- /dev/null +++ b/app/Observers/ModObserver.php @@ -0,0 +1,27 @@ +dependencyVersionService = $dependencyVersionService; + } + + /** + * Handle the Mod "saved" event. + */ + public function saved(Mod $mod): void + { + foreach ($mod->versions as $modVersion) { + $this->dependencyVersionService->resolve($modVersion); + } + } +} diff --git a/app/Observers/ModVersionObserver.php b/app/Observers/ModVersionObserver.php index abd2ecd..2182f67 100644 --- a/app/Observers/ModVersionObserver.php +++ b/app/Observers/ModVersionObserver.php @@ -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); } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index d52f31b..5bac452 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -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); diff --git a/app/Services/DependencyVersionService.php b/app/Services/DependencyVersionService.php index d9f7b0c..556314e 100644 --- a/app/Services/DependencyVersionService.php +++ b/app/Services/DependencyVersionService.php @@ -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; } } diff --git a/app/Services/SptVersionService.php b/app/Services/SptVersionService.php index c480d50..8eba213 100644 --- a/app/Services/SptVersionService.php +++ b/app/Services/SptVersionService.php @@ -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); } } diff --git a/app/View/Components/ModListSection.php b/app/View/Components/ModListSection.php index 23950d5..38890fb 100644 --- a/app/View/Components/ModListSection.php +++ b/app/View/Components/ModListSection.php @@ -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 diff --git a/database/factories/ModDependencyFactory.php b/database/factories/ModDependencyFactory.php index 012f5f8..b3318ac 100644 --- a/database/factories/ModDependencyFactory.php +++ b/database/factories/ModDependencyFactory.php @@ -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)]; - } } diff --git a/database/factories/ModVersionFactory.php b/database/factories/ModVersionFactory.php index e9a78ab..716fd20 100644 --- a/database/factories/ModVersionFactory.php +++ b/database/factories/ModVersionFactory.php @@ -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()); } /** diff --git a/database/migrations/2024_05_15_022710_create_mods_table.php b/database/migrations/2024_05_15_022710_create_mods_table.php index 9846664..9e5c9d8 100644 --- a/database/migrations/2024_05_15_022710_create_mods_table.php +++ b/database/migrations/2024_05_15_022710_create_mods_table.php @@ -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'); }); } diff --git a/database/migrations/2024_05_15_023705_create_mod_versions_table.php b/database/migrations/2024_05_15_023705_create_mod_versions_table.php index dd92675..0301ead 100644 --- a/database/migrations/2024_05_15_023705_create_mod_versions_table.php +++ b/database/migrations/2024_05_15_023705_create_mod_versions_table.php @@ -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'); }); } diff --git a/database/migrations/2024_05_15_023710_create_mod_version_spt_version_table.php b/database/migrations/2024_05_15_023710_create_mod_version_spt_version_table.php new file mode 100644 index 0000000..2848d91 --- /dev/null +++ b/database/migrations/2024_05_15_023710_create_mod_version_spt_version_table.php @@ -0,0 +1,26 @@ +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'); + } +}; diff --git a/database/migrations/2024_07_25_161219_create_mod_dependencies.php b/database/migrations/2024_07_25_161219_create_mod_dependencies.php index 482e867..0e07be2 100644 --- a/database/migrations/2024_07_25_161219_create_mod_dependencies.php +++ b/database/migrations/2024_07_25_161219_create_mod_dependencies.php @@ -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']); }); } diff --git a/database/migrations/2024_07_25_161219_create_mod_resolved_dependencies.php b/database/migrations/2024_07_25_161219_create_mod_resolved_dependencies.php new file mode 100644 index 0000000..7df0d45 --- /dev/null +++ b/database/migrations/2024_07_25_161219_create_mod_resolved_dependencies.php @@ -0,0 +1,26 @@ +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'); + } +}; diff --git a/resources/js/app.js b/resources/js/app.js index 43479a9..efade33 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -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"); - }); -}); diff --git a/resources/views/components/mod-card.blade.php b/resources/views/components/mod-card.blade.php index 0f1f22e..1a2dbc7 100644 --- a/resources/views/components/mod-card.blade.php +++ b/resources/views/components/mod-card.blade.php @@ -18,8 +18,8 @@
diff --git a/resources/views/livewire/mod/index.blade.php b/resources/views/livewire/mod/index.blade.php index 1cede53..1761406 100644 --- a/resources/views/livewire/mod/index.blade.php +++ b/resources/views/livewire/mod/index.blade.php @@ -1,35 +1,4 @@ -
{!! __('Explore an enhanced SPT experience with the mods available below. Check out the featured mods for a tailored solo-survival game with maximum immersion.') !!}
@@ -61,7 +30,13 @@{{ __('Loading...') }}
++ + {{ __('Loading...') }} +
{{ Number::downloads($mod->total_downloads) }} {{ __('Downloads') }}
- - {{ $latestVersion->sptVersion->version }} {{ __('Compatible') }} + + {{ $mod->latestVersion->latestSptVersion->first()->version_formatted }} {{ __('Compatible') }}
{{ Number::downloads($version->downloads) }} {{ __('Downloads') }}
- - {{ $latestVersion->virus_total_link }} + + {{ $mod->latestVersion->virus_total_link }}
- @foreach ($latestVersion->dependencies as $dependency)
+ @foreach ($mod->latestVersion->dependencies as $dependency)
{{ $dependency->resolvedVersion->mod->name }} ({{ $dependency->resolvedVersion->version }})
diff --git a/tests/Feature/Mod/ModDependencyTest.php b/tests/Feature/Mod/ModDependencyTest.php
new file mode 100644
index 0000000..b5db542
--- /dev/null
+++ b/tests/Feature/Mod/ModDependencyTest.php
@@ -0,0 +1,312 @@
+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)"));
+});
diff --git a/tests/Feature/Mod/ModFilterTest.php b/tests/Feature/Mod/ModFilterTest.php
new file mode 100644
index 0000000..6e4d998
--- /dev/null
+++ b/tests/Feature/Mod/ModFilterTest.php
@@ -0,0 +1,161 @@
+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
+});
diff --git a/tests/Feature/Mod/ModTest.php b/tests/Feature/Mod/ModTest.php
new file mode 100644
index 0000000..f853a64
--- /dev/null
+++ b/tests/Feature/Mod/ModTest.php
@@ -0,0 +1,52 @@
+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)");
+});
diff --git a/tests/Feature/ModVersionTest.php b/tests/Feature/Mod/ModVersionTest.php
similarity index 64%
rename from tests/Feature/ModVersionTest.php
rename to tests/Feature/Mod/ModVersionTest.php
index dd4e91e..c706f0a 100644
--- a/tests/Feature/ModVersionTest.php
+++ b/tests/Feature/Mod/ModVersionTest.php
@@ -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 () {
diff --git a/tests/Feature/ModDependencyTest.php b/tests/Feature/ModDependencyTest.php
deleted file mode 100644
index a6f1643..0000000
--- a/tests/Feature/ModDependencyTest.php
+++ /dev/null
@@ -1,266 +0,0 @@
-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);
diff --git a/tests/Feature/ModTest.php b/tests/Feature/ModTest.php
deleted file mode 100644
index 3365414..0000000
--- a/tests/Feature/ModTest.php
+++ /dev/null
@@ -1,54 +0,0 @@
-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)");
-});
diff --git a/tests/Feature/StressTest.php b/tests/Feature/StressTest.php
deleted file mode 100644
index 275df32..0000000
--- a/tests/Feature/StressTest.php
+++ /dev/null
@@ -1,9 +0,0 @@
-requests()->duration()->med())->toBeLessThan(100);
-});