Route languages

This commit is contained in:
myrmidex 2025-08-16 10:09:38 +02:00
parent f765d04d06
commit 2087ca389e
15 changed files with 1279 additions and 322 deletions

View file

@ -0,0 +1,62 @@
<?php
namespace App\Http\Controllers\Api\V1;
use Domains\Settings\Models\Language;
use Illuminate\Http\JsonResponse;
class LanguagesController extends BaseController
{
/**
* Get languages available for route creation
* Returns languages that have both active feeds and active channels
*/
public function availableForRoutes(): JsonResponse
{
$languages = Language::availableForRoutes()
->orderBy('name')
->get(['id', 'short_code', 'name', 'native_name']);
return $this->sendResponse(
$languages,
'Available languages for routes retrieved successfully.'
);
}
/**
* Get feeds filtered by language
*/
public function feedsByLanguage(int $languageId): JsonResponse
{
$language = Language::findOrFail($languageId);
$feeds = $language->feeds()
->where('feeds.is_active', true)
->where('feed_languages.is_active', true)
->with('languages')
->get(['feeds.id', 'feeds.name', 'feeds.url', 'feeds.type', 'feeds.provider']);
return $this->sendResponse(
$feeds,
"Feeds for language '{$language->name}' retrieved successfully."
);
}
/**
* Get channels filtered by language
*/
public function channelsByLanguage(int $languageId): JsonResponse
{
$language = Language::findOrFail($languageId);
$channels = $language->platformChannels()
->where('platform_channels.is_active', true)
->with(['platformInstance:id,name,url', 'language:id,name,short_code'])
->get(['id', 'platform_instance_id', 'name', 'display_name', 'description', 'language_id']);
return $this->sendResponse(
$channels,
"Channels for language '{$language->name}' retrieved successfully."
);
}
}

View file

@ -91,13 +91,19 @@ public function options(): JsonResponse
// Get existing feeds and channels for route creation
$feeds = Feed::where('is_active', true)
->with('languages')
->orderBy('name')
->get(['id', 'name', 'url', 'type']);
$platformChannels = PlatformChannel::where('is_active', true)
->with(['platformInstance:id,name,url'])
->with(['platformInstance:id,name,url', 'language:id,name,short_code'])
->orderBy('name')
->get(['id', 'platform_instance_id', 'name', 'display_name', 'description']);
->get(['id', 'platform_instance_id', 'name', 'display_name', 'description', 'language_id']);
// Get languages available for routes (have both feeds and channels)
$availableLanguages = Language::availableForRoutes()
->orderBy('name')
->get(['id', 'short_code', 'name', 'native_name']);
// Get feed providers from config
$feedProviders = collect(config('feed.providers', []))
@ -109,6 +115,7 @@ public function options(): JsonResponse
'platform_instances' => $platformInstances,
'feeds' => $feeds,
'platform_channels' => $platformChannels,
'available_languages' => $availableLanguages,
'feed_providers' => $feedProviders,
], 'Onboarding options retrieved successfully.');
}
@ -352,6 +359,7 @@ public function createRoute(Request $request): JsonResponse
$validator = Validator::make($request->all(), [
'feed_id' => 'required|exists:feeds,id',
'platform_channel_id' => 'required|exists:platform_channels,id',
'language_id' => 'required|exists:languages,id',
'priority' => 'nullable|integer|min:1|max:100',
]);
@ -361,9 +369,22 @@ public function createRoute(Request $request): JsonResponse
$validated = $validator->validated();
// Validate language consistency
$feed = Feed::find($validated['feed_id']);
$channel = PlatformChannel::find($validated['platform_channel_id']);
if (!$feed || !$feed->languages()->where('languages.id', $validated['language_id'])->exists()) {
return $this->sendError('The selected feed does not support the chosen language.', [], 422);
}
if (!$channel || $channel->language_id !== (int)$validated['language_id']) {
return $this->sendError('The selected channel does not support the chosen language.', [], 422);
}
$route = Route::create([
'feed_id' => $validated['feed_id'],
'platform_channel_id' => $validated['platform_channel_id'],
'language_id' => $validated['language_id'],
'priority' => $validated['priority'] ?? 50,
'is_active' => true,
]);
@ -373,7 +394,7 @@ public function createRoute(Request $request): JsonResponse
ArticleDiscoveryJob::dispatch();
return $this->sendResponse(
new RouteResource($route->load(['feed', 'platformChannel'])),
new RouteResource($route->load(['feed', 'platformChannel', 'language'])),
'Route created successfully.'
);
}

View file

@ -6,6 +6,8 @@
use Domains\Feed\Models\Feed;
use Domains\Platform\Models\PlatformChannel;
use Domains\Feed\Models\Route;
use Domains\Feed\Requests\StoreRouteRequest;
use Domains\Feed\Requests\UpdateRouteRequest;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
@ -17,7 +19,7 @@ class RoutingController extends BaseController
*/
public function index(): JsonResponse
{
$routes = Route::with(['feed', 'platformChannel', 'keywords'])
$routes = Route::with(['feed', 'platformChannel', 'language', 'keywords'])
->orderBy('is_active', 'desc')
->orderBy('priority', 'asc')
->get();
@ -31,15 +33,10 @@ public function index(): JsonResponse
/**
* Store a newly created routing configuration
*/
public function store(Request $request): JsonResponse
public function store(StoreRouteRequest $request): JsonResponse
{
try {
$validated = $request->validate([
'feed_id' => 'required|exists:feeds,id',
'platform_channel_id' => 'required|exists:platform_channels,id',
'is_active' => 'boolean',
'priority' => 'nullable|integer|min:0',
]);
$validated = $request->validated();
$validated['is_active'] = $validated['is_active'] ?? true;
$validated['priority'] = $validated['priority'] ?? 0;
@ -47,7 +44,7 @@ public function store(Request $request): JsonResponse
$route = Route::create($validated);
return $this->sendResponse(
new RouteResource($route->load(['feed', 'platformChannel', 'keywords'])),
new RouteResource($route->load(['feed', 'platformChannel', 'language', 'keywords'])),
'Routing configuration created successfully!',
201
);
@ -69,7 +66,7 @@ public function show(Feed $feed, PlatformChannel $channel): JsonResponse
return $this->sendNotFound('Routing configuration not found.');
}
$route->load(['feed', 'platformChannel', 'keywords']);
$route->load(['feed', 'platformChannel', 'language', 'keywords']);
return $this->sendResponse(
new RouteResource($route),
@ -80,7 +77,7 @@ public function show(Feed $feed, PlatformChannel $channel): JsonResponse
/**
* Update the specified routing configuration
*/
public function update(Request $request, Feed $feed, PlatformChannel $channel): JsonResponse
public function update(UpdateRouteRequest $request, Feed $feed, PlatformChannel $channel): JsonResponse
{
try {
$route = $this->findRoute($feed, $channel);
@ -89,17 +86,14 @@ public function update(Request $request, Feed $feed, PlatformChannel $channel):
return $this->sendNotFound('Routing configuration not found.');
}
$validated = $request->validate([
'is_active' => 'boolean',
'priority' => 'nullable|integer|min:0',
]);
$validated = $request->validated();
Route::where('feed_id', $feed->id)
->where('platform_channel_id', $channel->id)
->update($validated);
return $this->sendResponse(
new RouteResource($route->fresh(['feed', 'platformChannel', 'keywords'])),
new RouteResource($route->fresh(['feed', 'platformChannel', 'language', 'keywords'])),
'Routing configuration updated successfully!'
);
} catch (ValidationException $e) {
@ -154,7 +148,7 @@ public function toggle(Feed $feed, PlatformChannel $channel): JsonResponse
$status = $newStatus ? 'activated' : 'deactivated';
return $this->sendResponse(
new RouteResource($route->fresh(['feed', 'platformChannel', 'keywords'])),
new RouteResource($route->fresh(['feed', 'platformChannel', 'language', 'keywords'])),
"Routing configuration {$status} successfully!"
);
} catch (\Exception $e) {

View file

@ -0,0 +1,41 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('routes', function (Blueprint $table) {
$table->foreignId('language_id')->nullable()->after('platform_channel_id')->constrained()->onDelete('cascade');
$table->index(['language_id', 'is_active']);
});
// Migrate existing routes to set language based on feed's primary language and channel language (where they match)
DB::statement("
UPDATE routes r
INNER JOIN platform_channels pc ON r.platform_channel_id = pc.id
INNER JOIN feed_languages fl ON r.feed_id = fl.feed_id AND fl.is_primary = 1 AND fl.is_active = 1
SET r.language_id = pc.language_id
WHERE pc.language_id = fl.language_id
");
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('routes', function (Blueprint $table) {
$table->dropForeign(['language_id']);
$table->dropIndex(['language_id', 'is_active']);
$table->dropColumn('language_id');
});
}
};

View file

@ -3,6 +3,7 @@
use App\Http\Controllers\Api\V1\ArticlesController;
use App\Http\Controllers\Api\V1\DashboardController;
use App\Http\Controllers\Api\V1\FeedsController;
use App\Http\Controllers\Api\V1\LanguagesController;
use App\Http\Controllers\Api\V1\LogsController;
use App\Http\Controllers\Api\V1\OnboardingController;
use App\Http\Controllers\Api\V1\PlatformAccountsController;
@ -101,6 +102,11 @@
Route::get('/settings', [SettingsController::class, 'index'])->name('api.settings.index');
Route::put('/settings', [SettingsController::class, 'update'])->name('api.settings.update');
// Languages
Route::get('/languages/available-for-routes', [LanguagesController::class, 'availableForRoutes'])->name('api.languages.available-for-routes');
Route::get('/languages/{language}/feeds', [LanguagesController::class, 'feedsByLanguage'])->name('api.languages.feeds');
Route::get('/languages/{language}/channels', [LanguagesController::class, 'channelsByLanguage'])->name('api.languages.channels');
// Logs
Route::get('/logs', [LogsController::class, 'index'])->name('api.logs.index');
});

View file

@ -5,6 +5,7 @@
use Domains\Article\Models\Keyword;
use Domains\Feed\Factories\RouteFactory;
use Domains\Platform\Models\PlatformChannel;
use Domains\Settings\Models\Language;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
@ -14,10 +15,12 @@
/**
* @property int $feed_id
* @property int $platform_channel_id
* @property int|null $language_id
* @property bool $is_active
* @property int $priority
* @property Carbon $created_at
* @property Carbon $updated_at
* @property Language|null $language
*/
class Route extends Model
{
@ -38,6 +41,7 @@ protected static function newFactory()
protected $fillable = [
'feed_id',
'platform_channel_id',
'language_id',
'is_active',
'priority'
];
@ -62,6 +66,14 @@ public function platformChannel(): BelongsTo
return $this->belongsTo(PlatformChannel::class);
}
/**
* @return BelongsTo<Language, $this>
*/
public function language(): BelongsTo
{
return $this->belongsTo(Language::class);
}
/**
* @return HasMany<Keyword, $this>
*/
@ -70,4 +82,51 @@ public function keywords(): HasMany
return $this->hasMany(Keyword::class, 'feed_id', 'feed_id')
->where('platform_channel_id', $this->platform_channel_id);
}
/**
* Check if the feed has the given language
*/
public function feedHasLanguage(int $languageId): bool
{
if (!$this->feed) {
return false;
}
return $this->feed->languages()->where('languages.id', $languageId)->exists();
}
/**
* Check if the platform channel has the given language
*/
public function channelHasLanguage(int $languageId): bool
{
return $this->platformChannel && $this->platformChannel->language_id === $languageId;
}
/**
* Check if the route's language is consistent with feed and channel
*/
public function hasConsistentLanguage(): bool
{
if (!$this->language_id) {
return false;
}
return $this->feedHasLanguage($this->language_id) && $this->channelHasLanguage($this->language_id);
}
/**
* Get common languages between feed and channel
*/
public function getCommonLanguages(): array
{
if (!$this->feed || !$this->platformChannel) {
return [];
}
$feedLanguageIds = $this->feed->languages()->pluck('languages.id')->toArray();
$channelLanguageId = $this->platformChannel->language_id;
return in_array($channelLanguageId, $feedLanguageIds) ? [$channelLanguageId] : [];
}
}

View file

@ -0,0 +1,91 @@
<?php
namespace Domains\Feed\Requests;
use Domains\Feed\Models\Feed;
use Domains\Platform\Models\PlatformChannel;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Validator;
class StoreRouteRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'feed_id' => 'required|exists:feeds,id',
'platform_channel_id' => 'required|exists:platform_channels,id',
'language_id' => 'required|exists:languages,id',
'is_active' => 'boolean',
'priority' => 'nullable|integer|min:0',
];
}
/**
* Configure the validator instance.
*/
public function withValidator(Validator $validator): void
{
$validator->after(function (Validator $validator) {
$this->validateLanguageConsistency($validator);
});
}
/**
* Validate that the language is consistent between feed and channel
*/
protected function validateLanguageConsistency(Validator $validator): void
{
$feedId = $this->input('feed_id');
$channelId = $this->input('platform_channel_id');
$languageId = $this->input('language_id');
if (!$feedId || !$channelId || !$languageId) {
return;
}
// Check if feed has the selected language
$feed = Feed::find($feedId);
if (!$feed || !$feed->languages()->where('languages.id', $languageId)->exists()) {
$validator->errors()->add('language_id', 'The selected feed does not support this language.');
return;
}
// Check if channel has the selected language
$channel = PlatformChannel::find($channelId);
if (!$channel || $channel->language_id !== (int)$languageId) {
$validator->errors()->add('language_id', 'The selected channel does not support this language.');
return;
}
}
/**
* Get custom messages for validator errors.
*
* @return array<string, string>
*/
public function messages(): array
{
return [
'feed_id.required' => 'A feed must be selected.',
'feed_id.exists' => 'The selected feed is invalid.',
'platform_channel_id.required' => 'A platform channel must be selected.',
'platform_channel_id.exists' => 'The selected platform channel is invalid.',
'language_id.required' => 'A language must be selected.',
'language_id.exists' => 'The selected language is invalid.',
'priority.min' => 'Priority must be at least 0.',
];
}
}

View file

@ -0,0 +1,89 @@
<?php
namespace Domains\Feed\Requests;
use Domains\Feed\Models\Feed;
use Domains\Platform\Models\PlatformChannel;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Validator;
class UpdateRouteRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'language_id' => 'nullable|exists:languages,id',
'is_active' => 'boolean',
'priority' => 'nullable|integer|min:0',
];
}
/**
* Configure the validator instance.
*/
public function withValidator(Validator $validator): void
{
$validator->after(function (Validator $validator) {
$this->validateLanguageConsistency($validator);
});
}
/**
* Validate that the language is consistent between feed and channel
*/
protected function validateLanguageConsistency(Validator $validator): void
{
$languageId = $this->input('language_id');
// If no language_id provided, skip validation (allowing null updates)
if (!$languageId) {
return;
}
// Get feed and channel from route parameters
$feed = $this->route('feed');
$channel = $this->route('channel');
if (!$feed || !$channel) {
return;
}
// Check if feed has the selected language
if (!$feed->languages()->where('languages.id', $languageId)->exists()) {
$validator->errors()->add('language_id', 'The selected feed does not support this language.');
return;
}
// Check if channel has the selected language
if ($channel->language_id !== (int)$languageId) {
$validator->errors()->add('language_id', 'The selected channel does not support this language.');
return;
}
}
/**
* Get custom messages for validator errors.
*
* @return array<string, string>
*/
public function messages(): array
{
return [
'language_id.exists' => 'The selected language is invalid.',
'priority.min' => 'Priority must be at least 0.',
];
}
}

View file

@ -4,6 +4,7 @@
use Domains\Feed\Resources\FeedResource;
use Domains\Platform\Resources\PlatformChannelResource;
use Domains\Settings\Resources\LanguageResource;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
@ -20,12 +21,14 @@ public function toArray(Request $request): array
'id' => $this->id,
'feed_id' => $this->feed_id,
'platform_channel_id' => $this->platform_channel_id,
'language_id' => $this->language_id,
'is_active' => $this->is_active,
'priority' => $this->priority,
'created_at' => $this->created_at->toISOString(),
'updated_at' => $this->updated_at->toISOString(),
'feed' => new FeedResource($this->whenLoaded('feed')),
'platform_channel' => new PlatformChannelResource($this->whenLoaded('platformChannel')),
'language' => new LanguageResource($this->whenLoaded('language')),
'keywords' => $this->whenLoaded('keywords', function () {
return $this->keywords->map(function ($keyword) {
return [

View file

@ -3,6 +3,7 @@
namespace Domains\Settings\Models;
use Domains\Feed\Models\Feed;
use Domains\Feed\Models\Route;
use Domains\Platform\Models\PlatformChannel;
use Domains\Platform\Models\PlatformInstance;
use Domains\Settings\Factories\LanguageFactory;
@ -10,6 +11,7 @@
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Builder;
class Language extends Model
{
@ -59,4 +61,67 @@ public function feeds(): BelongsToMany
->withPivot(['url', 'settings', 'is_active', 'is_primary'])
->withTimestamps();
}
/**
* @return HasMany<Route, $this>
*/
public function routes(): HasMany
{
return $this->hasMany(Route::class);
}
/**
* Scope to get languages that have both active feeds and active channels
*
* @param Builder<Language> $query
* @return Builder<Language>
*/
public function scopeAvailableForRoutes(Builder $query): Builder
{
return $query->where('is_active', true)
->whereHas('feeds', function (Builder $feedQuery) {
$feedQuery->where('feeds.is_active', true)
->where('feed_languages.is_active', true);
})
->whereHas('platformChannels', function (Builder $channelQuery) {
$channelQuery->where('platform_channels.is_active', true);
});
}
/**
* Scope to get languages with active feeds
*
* @param Builder<Language> $query
* @return Builder<Language>
*/
public function scopeWithActiveFeeds(Builder $query): Builder
{
return $query->whereHas('feeds', function (Builder $feedQuery) {
$feedQuery->where('feeds.is_active', true)
->where('feed_languages.is_active', true);
});
}
/**
* Scope to get languages with active channels
*
* @param Builder<Language> $query
* @return Builder<Language>
*/
public function scopeWithActiveChannels(Builder $query): Builder
{
return $query->whereHas('platformChannels', function (Builder $channelQuery) {
$channelQuery->where('platform_channels.is_active', true);
});
}
/**
* Check if this language can be used for routes (has both feeds and channels)
*/
public function canBeUsedForRoutes(): bool
{
return $this->is_active &&
$this->feeds()->where('feeds.is_active', true)->where('feed_languages.is_active', true)->exists() &&
$this->platformChannels()->where('platform_channels.is_active', true)->exists();
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace Domains\Settings\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class LanguageResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'short_code' => $this->short_code,
'name' => $this->name,
'native_name' => $this->native_name,
'is_active' => $this->is_active,
];
}
}

View file

@ -0,0 +1,293 @@
<?php
namespace Tests\Feature\Http\Controllers\Api\V1;
use Domains\Feed\Models\Feed;
use Domains\Feed\Models\Route;
use Domains\Platform\Models\PlatformChannel;
use Domains\Settings\Models\Language;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class RoutingControllerLanguageTest extends TestCase
{
use RefreshDatabase;
public function test_store_route_requires_language_id(): void
{
$language = Language::factory()->create();
$feed = Feed::factory()->create();
$channel = PlatformChannel::factory()->create(['language_id' => $language->id]);
$response = $this->postJson('/api/v1/routing', [
'feed_id' => $feed->id,
'platform_channel_id' => $channel->id,
// 'language_id' is missing
'is_active' => true,
'priority' => 50
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors(['language_id']);
}
public function test_store_route_validates_language_consistency_with_feed(): void
{
$language1 = Language::factory()->create();
$language2 = Language::factory()->create();
$feed = Feed::factory()->create();
$channel = PlatformChannel::factory()->create(['language_id' => $language1->id]);
// Attach only language2 to feed, but request language1
$feed->languages()->attach($language2->id, [
'url' => $feed->url,
'is_active' => true,
'is_primary' => true
]);
$response = $this->postJson('/api/v1/routing', [
'feed_id' => $feed->id,
'platform_channel_id' => $channel->id,
'language_id' => $language1->id,
'is_active' => true,
'priority' => 50
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors(['language_id']);
$this->assertStringContainsString('The selected feed does not support this language',
$response->json('errors.language_id.0'));
}
public function test_store_route_validates_language_consistency_with_channel(): void
{
$language1 = Language::factory()->create();
$language2 = Language::factory()->create();
$feed = Feed::factory()->create();
$channel = PlatformChannel::factory()->create(['language_id' => $language2->id]);
// Attach language1 to feed, but channel has language2
$feed->languages()->attach($language1->id, [
'url' => $feed->url,
'is_active' => true,
'is_primary' => true
]);
$response = $this->postJson('/api/v1/routing', [
'feed_id' => $feed->id,
'platform_channel_id' => $channel->id,
'language_id' => $language1->id,
'is_active' => true,
'priority' => 50
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors(['language_id']);
$this->assertStringContainsString('The selected channel does not support this language',
$response->json('errors.language_id.0'));
}
public function test_store_route_succeeds_with_valid_language_consistency(): void
{
$language = Language::factory()->create();
$feed = Feed::factory()->create();
$channel = PlatformChannel::factory()->create(['language_id' => $language->id]);
// Attach language to feed
$feed->languages()->attach($language->id, [
'url' => $feed->url,
'is_active' => true,
'is_primary' => true
]);
$response = $this->postJson('/api/v1/routing', [
'feed_id' => $feed->id,
'platform_channel_id' => $channel->id,
'language_id' => $language->id,
'is_active' => true,
'priority' => 50
]);
$response->assertStatus(201);
$response->assertJson([
'success' => true,
'message' => 'Routing configuration created successfully!',
'data' => [
'feed_id' => $feed->id,
'platform_channel_id' => $channel->id,
'language_id' => $language->id,
'is_active' => true,
'priority' => 50
]
]);
$this->assertDatabaseHas('routes', [
'feed_id' => $feed->id,
'platform_channel_id' => $channel->id,
'language_id' => $language->id,
'is_active' => true,
'priority' => 50
]);
}
public function test_update_route_validates_language_consistency(): void
{
$language1 = Language::factory()->create();
$language2 = Language::factory()->create();
$feed = Feed::factory()->create();
$channel = PlatformChannel::factory()->create(['language_id' => $language1->id]);
// Attach both languages to feed
$feed->languages()->attach([
$language1->id => [
'url' => $feed->url,
'is_active' => true,
'is_primary' => true
],
$language2->id => [
'url' => $feed->url,
'is_active' => true,
'is_primary' => false
]
]);
// Create route with language1
$route = Route::create([
'feed_id' => $feed->id,
'platform_channel_id' => $channel->id,
'language_id' => $language1->id,
'is_active' => true,
'priority' => 50
]);
// Try to update to language2, which feed supports but channel doesn't
$response = $this->putJson("/api/v1/routing/{$feed->id}/{$channel->id}", [
'language_id' => $language2->id,
'is_active' => false,
'priority' => 75
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors(['language_id']);
$this->assertStringContainsString('The selected channel does not support this language',
$response->json('errors.language_id.0'));
}
public function test_update_route_succeeds_with_valid_language(): void
{
$language = Language::factory()->create();
$feed = Feed::factory()->create();
$channel = PlatformChannel::factory()->create(['language_id' => $language->id]);
// Attach language to feed
$feed->languages()->attach($language->id, [
'url' => $feed->url,
'is_active' => true,
'is_primary' => true
]);
// Create route
$route = Route::create([
'feed_id' => $feed->id,
'platform_channel_id' => $channel->id,
'language_id' => $language->id,
'is_active' => true,
'priority' => 50
]);
$response = $this->putJson("/api/v1/routing/{$feed->id}/{$channel->id}", [
'language_id' => $language->id,
'is_active' => false,
'priority' => 75
]);
$response->assertStatus(200);
$response->assertJson([
'success' => true,
'message' => 'Routing configuration updated successfully!',
'data' => [
'feed_id' => $feed->id,
'platform_channel_id' => $channel->id,
'language_id' => $language->id,
'is_active' => false,
'priority' => 75
]
]);
$this->assertDatabaseHas('routes', [
'feed_id' => $feed->id,
'platform_channel_id' => $channel->id,
'language_id' => $language->id,
'is_active' => false,
'priority' => 75
]);
}
public function test_index_includes_language_relationship(): void
{
$language = Language::factory()->create();
$feed = Feed::factory()->create();
$channel = PlatformChannel::factory()->create(['language_id' => $language->id]);
// Create route
Route::create([
'feed_id' => $feed->id,
'platform_channel_id' => $channel->id,
'language_id' => $language->id,
'is_active' => true,
'priority' => 50
]);
$response = $this->getJson('/api/v1/routing');
$response->assertStatus(200);
$response->assertJson([
'success' => true,
'data' => [
[
'feed_id' => $feed->id,
'platform_channel_id' => $channel->id,
'language_id' => $language->id,
'language' => [
'id' => $language->id,
'name' => $language->name,
'short_code' => $language->short_code
]
]
]
]);
}
public function test_show_includes_language_relationship(): void
{
$language = Language::factory()->create();
$feed = Feed::factory()->create();
$channel = PlatformChannel::factory()->create(['language_id' => $language->id]);
// Create route
Route::create([
'feed_id' => $feed->id,
'platform_channel_id' => $channel->id,
'language_id' => $language->id,
'is_active' => true,
'priority' => 50
]);
$response = $this->getJson("/api/v1/routing/{$feed->id}/{$channel->id}");
$response->assertStatus(200);
$response->assertJson([
'success' => true,
'data' => [
'feed_id' => $feed->id,
'platform_channel_id' => $channel->id,
'language_id' => $language->id,
'language' => [
'id' => $language->id,
'name' => $language->name,
'short_code' => $language->short_code
]
]
]);
}
}

View file

@ -3,9 +3,9 @@
namespace Tests\Unit\Models;
use Domains\Feed\Models\Feed;
use Domains\Settings\Models\Language;
use Domains\Feed\Models\Route;
use Domains\Platform\Models\PlatformChannel;
use Domains\Platform\Models\PlatformInstance;
use Domains\Settings\Models\Language;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
@ -13,335 +13,139 @@ class LanguageTest extends TestCase
{
use RefreshDatabase;
public function test_fillable_fields(): void
{
$fillableFields = ['short_code', 'name', 'native_name', 'is_active'];
$language = new Language();
$this->assertEquals($fillableFields, $language->getFillable());
}
public function test_table_name(): void
{
$language = new Language();
$this->assertEquals('languages', $language->getTable());
}
public function test_casts_is_active_to_boolean(): void
{
$language = Language::factory()->create(['is_active' => '1']);
$this->assertIsBool($language->is_active);
$this->assertTrue($language->is_active);
$language->update(['is_active' => '0']);
$language->refresh();
$this->assertIsBool($language->is_active);
$this->assertFalse($language->is_active);
}
public function test_belongs_to_many_platform_instances_relationship(): void
public function test_routes_relationship(): void
{
$language = Language::factory()->create();
$instance1 = PlatformInstance::factory()->create();
$instance2 = PlatformInstance::factory()->create();
// Attach with required platform_language_id
$language->platformInstances()->attach([
$instance1->id => ['platform_language_id' => 1],
$instance2->id => ['platform_language_id' => 2]
]);
$instances = $language->platformInstances;
$this->assertCount(2, $instances);
$this->assertTrue($instances->contains('id', $instance1->id));
$this->assertTrue($instances->contains('id', $instance2->id));
}
public function test_has_many_platform_channels_relationship(): void
{
$language = Language::factory()->create();
$feed1 = Feed::factory()->create();
$feed2 = Feed::factory()->create();
$channel1 = PlatformChannel::factory()->create(['language_id' => $language->id]);
$channel2 = PlatformChannel::factory()->create(['language_id' => $language->id]);
// Create channel for different language
$otherLanguage = Language::factory()->create();
PlatformChannel::factory()->create(['language_id' => $otherLanguage->id]);
$channels = $language->platformChannels;
$this->assertCount(2, $channels);
$this->assertTrue($channels->contains('id', $channel1->id));
$this->assertTrue($channels->contains('id', $channel2->id));
$this->assertInstanceOf(PlatformChannel::class, $channels->first());
}
public function test_belongs_to_many_feeds_relationship(): void
{
$language = Language::factory()->create();
$feed1 = Feed::factory()->create();
$feed2 = Feed::factory()->create();
// Attach feeds to this language
$language->feeds()->attach($feed1->id, [
'url' => $feed1->url,
$route1 = Route::create([
'feed_id' => $feed1->id,
'platform_channel_id' => $channel1->id,
'language_id' => $language->id,
'is_active' => true,
'is_primary' => true
'priority' => 50
]);
$language->feeds()->attach($feed2->id, [
'url' => $feed2->url,
'is_active' => true,
'is_primary' => false
$route2 = Route::create([
'feed_id' => $feed2->id,
'platform_channel_id' => $channel2->id,
'language_id' => $language->id,
'is_active' => false,
'priority' => 60
]);
$routes = $language->routes;
$this->assertCount(2, $routes);
$this->assertTrue($routes->contains('id', $route1->id));
$this->assertTrue($routes->contains('id', $route2->id));
}
public function test_available_for_routes_scope(): void
{
// Create languages
$language1 = Language::factory()->create(['is_active' => true]);
$language2 = Language::factory()->create(['is_active' => true]);
$language3 = Language::factory()->create(['is_active' => false]); // inactive language
// Create feeds and channels
$feed1 = Feed::factory()->create(['is_active' => true]);
$feed2 = Feed::factory()->create(['is_active' => false]); // inactive feed
$channel1 = PlatformChannel::factory()->create(['language_id' => $language1->id, 'is_active' => true]);
$channel2 = PlatformChannel::factory()->create(['language_id' => $language2->id, 'is_active' => false]); // inactive channel
// Attach languages to feeds
$feed1->languages()->attach($language1->id, ['url' => $feed1->url, 'is_active' => true, 'is_primary' => true]);
$feed2->languages()->attach($language2->id, ['url' => $feed2->url, 'is_active' => true, 'is_primary' => true]);
$feed1->languages()->attach($language3->id, ['url' => $feed1->url, 'is_active' => true, 'is_primary' => false]);
$availableLanguages = Language::availableForRoutes()->get();
// Only language1 should be available (has active feed and active channel)
$this->assertCount(1, $availableLanguages);
$this->assertEquals($language1->id, $availableLanguages->first()->id);
}
public function test_with_active_feeds_scope(): void
{
$language1 = Language::factory()->create(['is_active' => true]);
$language2 = Language::factory()->create(['is_active' => true]);
// Create feed for different language
$otherLanguage = Language::factory()->create();
$feed3 = Feed::factory()->create();
$otherLanguage->feeds()->attach($feed3->id, [
'url' => $feed3->url,
'is_active' => true,
'is_primary' => true
]);
$activeFeed = Feed::factory()->create(['is_active' => true]);
$inactiveFeed = Feed::factory()->create(['is_active' => false]);
$feeds = $language->feeds;
// Attach languages to feeds
$activeFeed->languages()->attach($language1->id, ['url' => $activeFeed->url, 'is_active' => true, 'is_primary' => true]);
$inactiveFeed->languages()->attach($language2->id, ['url' => $inactiveFeed->url, 'is_active' => true, 'is_primary' => true]);
$this->assertCount(2, $feeds);
$this->assertTrue($feeds->contains('id', $feed1->id));
$this->assertTrue($feeds->contains('id', $feed2->id));
$this->assertFalse($feeds->contains('id', $feed3->id));
$this->assertInstanceOf(Feed::class, $feeds->first());
$languagesWithActiveFeeds = Language::withActiveFeeds()->get();
$this->assertCount(1, $languagesWithActiveFeeds);
$this->assertEquals($language1->id, $languagesWithActiveFeeds->first()->id);
}
public function test_language_creation_with_factory(): void
public function test_with_active_channels_scope(): void
{
$language = Language::factory()->create();
$this->assertInstanceOf(Language::class, $language);
$this->assertIsString($language->short_code);
$this->assertIsString($language->name);
$this->assertTrue($language->is_active);
}
public function test_language_creation_with_explicit_values(): void
{
$language = Language::create([
'short_code' => 'fr',
'name' => 'French',
'native_name' => 'Français',
'is_active' => false
]);
$this->assertEquals('fr', $language->short_code);
$this->assertEquals('French', $language->name);
$this->assertEquals('Français', $language->native_name);
$this->assertFalse($language->is_active);
}
public function test_language_factory_states(): void
{
$inactiveLanguage = Language::factory()->inactive()->create();
$this->assertFalse($inactiveLanguage->is_active);
$englishLanguage = Language::factory()->english()->create();
$this->assertEquals('en', $englishLanguage->short_code);
$this->assertEquals('English', $englishLanguage->name);
$this->assertEquals('English', $englishLanguage->native_name);
}
public function test_language_update(): void
{
$language = Language::factory()->create([
'name' => 'Original Name',
'is_active' => true
]);
$language->update([
'name' => 'Updated Name',
'is_active' => false
]);
$language->refresh();
$this->assertEquals('Updated Name', $language->name);
$this->assertFalse($language->is_active);
}
public function test_language_deletion(): void
{
$language = Language::factory()->create();
$languageId = $language->id;
$language->delete();
$this->assertDatabaseMissing('languages', ['id' => $languageId]);
}
public function test_language_can_have_null_native_name(): void
{
$language = Language::factory()->create(['native_name' => null]);
$this->assertNull($language->native_name);
}
public function test_language_can_have_empty_native_name(): void
{
$language = Language::factory()->create(['native_name' => '']);
$this->assertEquals('', $language->native_name);
}
public function test_language_short_code_variations(): void
{
$shortCodes = ['en', 'fr', 'es', 'de', 'zh', 'pt', 'nl', 'it'];
$language1 = Language::factory()->create(['is_active' => true]);
$language2 = Language::factory()->create(['is_active' => true]);
foreach ($shortCodes as $code) {
$language = Language::factory()->create(['short_code' => $code]);
$this->assertEquals($code, $language->short_code);
}
PlatformChannel::factory()->create(['language_id' => $language1->id, 'is_active' => true]);
PlatformChannel::factory()->create(['language_id' => $language2->id, 'is_active' => false]);
$languagesWithActiveChannels = Language::withActiveChannels()->get();
$this->assertCount(1, $languagesWithActiveChannels);
$this->assertEquals($language1->id, $languagesWithActiveChannels->first()->id);
}
public function test_language_timestamps(): void
public function test_can_be_used_for_routes_method(): void
{
$language = Language::factory()->create();
$language = Language::factory()->create(['is_active' => true]);
$feed = Feed::factory()->create(['is_active' => true]);
$channel = PlatformChannel::factory()->create(['language_id' => $language->id, 'is_active' => true]);
$this->assertNotNull($language->created_at);
$this->assertNotNull($language->updated_at);
$this->assertInstanceOf(\Carbon\Carbon::class, $language->created_at);
$this->assertInstanceOf(\Carbon\Carbon::class, $language->updated_at);
// Attach language to feed
$feed->languages()->attach($language->id, ['url' => $feed->url, 'is_active' => true, 'is_primary' => true]);
$this->assertTrue($language->canBeUsedForRoutes());
}
public function test_language_can_have_multiple_platform_instances(): void
public function test_can_be_used_for_routes_method_returns_false_when_language_inactive(): void
{
$language = Language::factory()->create();
$instance1 = PlatformInstance::factory()->create();
$instance2 = PlatformInstance::factory()->create();
$instance3 = PlatformInstance::factory()->create();
$language = Language::factory()->create(['is_active' => false]);
$feed = Feed::factory()->create(['is_active' => true]);
$channel = PlatformChannel::factory()->create(['language_id' => $language->id, 'is_active' => true]);
// Attach with required platform_language_id values
$language->platformInstances()->attach([
$instance1->id => ['platform_language_id' => 1],
$instance2->id => ['platform_language_id' => 2],
$instance3->id => ['platform_language_id' => 3]
]);
// Attach language to feed
$feed->languages()->attach($language->id, ['url' => $feed->url, 'is_active' => true, 'is_primary' => true]);
$instances = $language->platformInstances;
$this->assertCount(3, $instances);
$this->assertTrue($instances->contains('id', $instance1->id));
$this->assertTrue($instances->contains('id', $instance2->id));
$this->assertTrue($instances->contains('id', $instance3->id));
$this->assertFalse($language->canBeUsedForRoutes());
}
public function test_language_platform_instances_relationship_is_empty_by_default(): void
public function test_can_be_used_for_routes_method_returns_false_when_no_active_feeds(): void
{
$language = Language::factory()->create();
$language = Language::factory()->create(['is_active' => true]);
$feed = Feed::factory()->create(['is_active' => false]); // inactive feed
$channel = PlatformChannel::factory()->create(['language_id' => $language->id, 'is_active' => true]);
$this->assertCount(0, $language->platformInstances);
// Attach language to inactive feed
$feed->languages()->attach($language->id, ['url' => $feed->url, 'is_active' => true, 'is_primary' => true]);
$this->assertFalse($language->canBeUsedForRoutes());
}
public function test_language_platform_channels_relationship_is_empty_by_default(): void
public function test_can_be_used_for_routes_method_returns_false_when_no_active_channels(): void
{
$language = Language::factory()->create();
$language = Language::factory()->create(['is_active' => true]);
$feed = Feed::factory()->create(['is_active' => true]);
$channel = PlatformChannel::factory()->create(['language_id' => $language->id, 'is_active' => false]); // inactive channel
$this->assertCount(0, $language->platformChannels);
// Attach language to feed
$feed->languages()->attach($language->id, ['url' => $feed->url, 'is_active' => true, 'is_primary' => true]);
$this->assertFalse($language->canBeUsedForRoutes());
}
public function test_language_feeds_relationship_is_empty_by_default(): void
{
$language = Language::factory()->create();
$this->assertCount(0, $language->feeds);
}
public function test_multiple_languages_with_same_name_different_regions(): void
{
$englishUS = Language::factory()->create([
'short_code' => 'en-US',
'name' => 'English (United States)',
'native_name' => 'English'
]);
$englishGB = Language::factory()->create([
'short_code' => 'en-GB',
'name' => 'English (United Kingdom)',
'native_name' => 'English'
]);
$this->assertEquals('English', $englishUS->native_name);
$this->assertEquals('English', $englishGB->native_name);
$this->assertNotEquals($englishUS->short_code, $englishGB->short_code);
$this->assertNotEquals($englishUS->name, $englishGB->name);
}
public function test_language_with_complex_native_name(): void
{
$complexLanguages = [
['short_code' => 'zh-CN', 'name' => 'Chinese (Simplified)', 'native_name' => '简体中文'],
['short_code' => 'zh-TW', 'name' => 'Chinese (Traditional)', 'native_name' => '繁體中文'],
['short_code' => 'ar', 'name' => 'Arabic', 'native_name' => 'العربية'],
['short_code' => 'ru', 'name' => 'Russian', 'native_name' => 'Русский'],
['short_code' => 'ja', 'name' => 'Japanese', 'native_name' => '日本語'],
];
foreach ($complexLanguages as $langData) {
$language = Language::factory()->create($langData);
$this->assertEquals($langData['short_code'], $language->short_code);
$this->assertEquals($langData['name'], $language->name);
$this->assertEquals($langData['native_name'], $language->native_name);
}
}
public function test_language_active_and_inactive_states(): void
{
$activeLanguage = Language::factory()->create(['is_active' => true]);
$inactiveLanguage = Language::factory()->create(['is_active' => false]);
$this->assertTrue($activeLanguage->is_active);
$this->assertFalse($inactiveLanguage->is_active);
}
public function test_language_relationships_maintain_referential_integrity(): void
{
$language = Language::factory()->create();
// Create related models
$instance = PlatformInstance::factory()->create();
$channel = PlatformChannel::factory()->create(['language_id' => $language->id]);
$feed = Feed::factory()->create();
$language->feeds()->attach($feed->id, [
'url' => $feed->url,
'is_active' => true,
'is_primary' => true
]);
// Attach instance
$language->platformInstances()->attach($instance->id, [
'platform_language_id' => 1,
'is_default' => true
]);
// Verify all relationships work
$this->assertCount(1, $language->platformInstances);
$this->assertCount(1, $language->platformChannels);
$this->assertCount(1, $language->feeds);
$this->assertEquals($language->id, $channel->language_id);
$this->assertTrue($language->feeds->contains('id', $feed->id));
}
public function test_language_factory_unique_constraints(): void
{
// The factory should generate unique short codes
$language1 = Language::factory()->create();
$language2 = Language::factory()->create();
$this->assertNotEquals($language1->short_code, $language2->short_code);
$this->assertNotEquals($language1->name, $language2->name);
}
}
}

View file

@ -6,6 +6,7 @@
use Domains\Article\Models\Keyword;
use Domains\Platform\Models\PlatformChannel;
use Domains\Feed\Models\Route;
use Domains\Settings\Models\Language;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
@ -15,7 +16,7 @@ class RouteTest extends TestCase
public function test_fillable_fields(): void
{
$fillableFields = ['feed_id', 'platform_channel_id', 'is_active', 'priority'];
$fillableFields = ['feed_id', 'platform_channel_id', 'language_id', 'is_active', 'priority'];
$route = new Route();
$this->assertEquals($fillableFields, $route->getFillable());
@ -258,4 +259,218 @@ public function test_route_with_multiple_keywords_active_and_inactive(): void
$this->assertEquals('active_keyword', $activeKeywords->first()->keyword);
$this->assertEquals('inactive_keyword', $inactiveKeywords->first()->keyword);
}
public function test_belongs_to_language_relationship(): void
{
$language = Language::factory()->create();
$feed = Feed::factory()->create();
$channel = PlatformChannel::factory()->create(['language_id' => $language->id]);
$route = Route::create([
'feed_id' => $feed->id,
'platform_channel_id' => $channel->id,
'language_id' => $language->id,
'is_active' => true,
'priority' => 50
]);
$this->assertInstanceOf(Language::class, $route->language);
$this->assertEquals($language->id, $route->language->id);
$this->assertEquals($language->name, $route->language->name);
}
public function test_route_creation_with_language(): void
{
$language = Language::factory()->create();
$feed = Feed::factory()->create();
$channel = PlatformChannel::factory()->create(['language_id' => $language->id]);
// Attach language to feed
$feed->languages()->attach($language->id, [
'url' => $feed->url,
'is_active' => true,
'is_primary' => true
]);
$route = Route::create([
'feed_id' => $feed->id,
'platform_channel_id' => $channel->id,
'language_id' => $language->id,
'is_active' => true,
'priority' => 75
]);
$this->assertEquals($language->id, $route->language_id);
$this->assertEquals($feed->id, $route->feed_id);
$this->assertEquals($channel->id, $route->platform_channel_id);
$this->assertTrue($route->is_active);
$this->assertEquals(75, $route->priority);
}
public function test_feed_has_language_method(): void
{
$language1 = Language::factory()->create();
$language2 = Language::factory()->create();
$feed = Feed::factory()->create();
$channel = PlatformChannel::factory()->create(['language_id' => $language1->id]);
// Attach only language1 to feed
$feed->languages()->attach($language1->id, [
'url' => $feed->url,
'is_active' => true,
'is_primary' => true
]);
$route = Route::create([
'feed_id' => $feed->id,
'platform_channel_id' => $channel->id,
'language_id' => $language1->id,
'is_active' => true,
'priority' => 50
]);
$route->load('feed');
$this->assertTrue($route->feedHasLanguage($language1->id));
$this->assertFalse($route->feedHasLanguage($language2->id));
}
public function test_channel_has_language_method(): void
{
$language1 = Language::factory()->create();
$language2 = Language::factory()->create();
$feed = Feed::factory()->create();
$channel = PlatformChannel::factory()->create(['language_id' => $language1->id]);
$route = Route::create([
'feed_id' => $feed->id,
'platform_channel_id' => $channel->id,
'language_id' => $language1->id,
'is_active' => true,
'priority' => 50
]);
$route->load('platformChannel');
$this->assertTrue($route->channelHasLanguage($language1->id));
$this->assertFalse($route->channelHasLanguage($language2->id));
}
public function test_has_consistent_language_method(): void
{
$language = Language::factory()->create();
$feed = Feed::factory()->create();
$channel = PlatformChannel::factory()->create(['language_id' => $language->id]);
// Attach language to feed
$feed->languages()->attach($language->id, [
'url' => $feed->url,
'is_active' => true,
'is_primary' => true
]);
$route = Route::create([
'feed_id' => $feed->id,
'platform_channel_id' => $channel->id,
'language_id' => $language->id,
'is_active' => true,
'priority' => 50
]);
$route->load(['feed', 'platformChannel']);
$this->assertTrue($route->hasConsistentLanguage());
}
public function test_has_consistent_language_method_returns_false_for_mismatched_languages(): void
{
$language1 = Language::factory()->create();
$language2 = Language::factory()->create();
$feed = Feed::factory()->create();
$channel = PlatformChannel::factory()->create(['language_id' => $language2->id]);
// Attach only language1 to feed, but channel has language2
$feed->languages()->attach($language1->id, [
'url' => $feed->url,
'is_active' => true,
'is_primary' => true
]);
$route = Route::create([
'feed_id' => $feed->id,
'platform_channel_id' => $channel->id,
'language_id' => $language1->id,
'is_active' => true,
'priority' => 50
]);
$route->load(['feed', 'platformChannel']);
$this->assertFalse($route->hasConsistentLanguage());
}
public function test_get_common_languages_method(): void
{
$language1 = Language::factory()->create();
$language2 = Language::factory()->create();
$feed = Feed::factory()->create();
$channel = PlatformChannel::factory()->create(['language_id' => $language1->id]);
// Attach both languages to feed
$feed->languages()->attach([
$language1->id => [
'url' => $feed->url,
'is_active' => true,
'is_primary' => true
],
$language2->id => [
'url' => $feed->url,
'is_active' => true,
'is_primary' => false
]
]);
$route = Route::create([
'feed_id' => $feed->id,
'platform_channel_id' => $channel->id,
'language_id' => $language1->id,
'is_active' => true,
'priority' => 50
]);
$route->load(['feed', 'platformChannel']);
$commonLanguages = $route->getCommonLanguages();
$this->assertEquals([$language1->id], $commonLanguages);
}
public function test_get_common_languages_method_returns_empty_for_no_match(): void
{
$language1 = Language::factory()->create();
$language2 = Language::factory()->create();
$feed = Feed::factory()->create();
$channel = PlatformChannel::factory()->create(['language_id' => $language2->id]);
// Attach only language1 to feed, but channel has language2
$feed->languages()->attach($language1->id, [
'url' => $feed->url,
'is_active' => true,
'is_primary' => true
]);
$route = Route::create([
'feed_id' => $feed->id,
'platform_channel_id' => $channel->id,
'language_id' => $language1->id,
'is_active' => true,
'priority' => 50
]);
$route->load(['feed', 'platformChannel']);
$commonLanguages = $route->getCommonLanguages();
$this->assertEmpty($commonLanguages);
}
}

View file

@ -0,0 +1,189 @@
<?php
namespace Tests\Unit\Requests;
use Domains\Feed\Models\Feed;
use Domains\Feed\Requests\StoreRouteRequest;
use Domains\Platform\Models\PlatformChannel;
use Domains\Settings\Models\Language;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Validator;
use Tests\TestCase;
class StoreRouteRequestTest extends TestCase
{
use RefreshDatabase;
public function test_authorize_returns_true(): void
{
$request = new StoreRouteRequest();
$this->assertTrue($request->authorize());
}
public function test_rules_include_required_fields(): void
{
$request = new StoreRouteRequest();
$rules = $request->rules();
$this->assertArrayHasKey('feed_id', $rules);
$this->assertArrayHasKey('platform_channel_id', $rules);
$this->assertArrayHasKey('language_id', $rules);
$this->assertArrayHasKey('is_active', $rules);
$this->assertArrayHasKey('priority', $rules);
$this->assertStringContainsString('required', $rules['feed_id']);
$this->assertStringContainsString('exists:feeds,id', $rules['feed_id']);
$this->assertStringContainsString('required', $rules['platform_channel_id']);
$this->assertStringContainsString('exists:platform_channels,id', $rules['platform_channel_id']);
$this->assertStringContainsString('required', $rules['language_id']);
$this->assertStringContainsString('exists:languages,id', $rules['language_id']);
}
public function test_validation_passes_with_valid_data(): void
{
$language = Language::factory()->create();
$feed = Feed::factory()->create();
$channel = PlatformChannel::factory()->create(['language_id' => $language->id]);
// Attach language to feed
$feed->languages()->attach($language->id, [
'url' => $feed->url,
'is_active' => true,
'is_primary' => true
]);
$data = [
'feed_id' => $feed->id,
'platform_channel_id' => $channel->id,
'language_id' => $language->id,
'is_active' => true,
'priority' => 50
];
$request = new StoreRouteRequest();
$rules = $request->rules();
$validator = Validator::make($data, $rules);
$this->assertTrue($validator->passes());
}
public function test_validation_fails_without_required_fields(): void
{
$data = [];
$request = new StoreRouteRequest();
$rules = $request->rules();
$validator = Validator::make($data, $rules);
$this->assertFalse($validator->passes());
$this->assertArrayHasKey('feed_id', $validator->errors()->toArray());
$this->assertArrayHasKey('platform_channel_id', $validator->errors()->toArray());
$this->assertArrayHasKey('language_id', $validator->errors()->toArray());
}
public function test_validation_fails_with_invalid_foreign_keys(): void
{
$data = [
'feed_id' => 999,
'platform_channel_id' => 999,
'language_id' => 999,
'is_active' => true,
'priority' => 50
];
$request = new StoreRouteRequest();
$rules = $request->rules();
$validator = Validator::make($data, $rules);
$this->assertFalse($validator->passes());
$this->assertArrayHasKey('feed_id', $validator->errors()->toArray());
$this->assertArrayHasKey('platform_channel_id', $validator->errors()->toArray());
$this->assertArrayHasKey('language_id', $validator->errors()->toArray());
}
public function test_language_consistency_validation_fails_when_feed_lacks_language(): void
{
$language1 = Language::factory()->create();
$language2 = Language::factory()->create();
$feed = Feed::factory()->create();
$channel = PlatformChannel::factory()->create(['language_id' => $language1->id]);
// Attach only language2 to feed, but request language1
$feed->languages()->attach($language2->id, [
'url' => $feed->url,
'is_active' => true,
'is_primary' => true
]);
$data = [
'feed_id' => $feed->id,
'platform_channel_id' => $channel->id,
'language_id' => $language1->id,
'is_active' => true,
'priority' => 50
];
$request = new StoreRouteRequest();
$request->merge($data);
$rules = $request->rules();
$validator = Validator::make($data, $rules);
// Call the custom validation method
$request->withValidator($validator);
$this->assertFalse($validator->passes());
$this->assertStringContainsString('The selected feed does not support this language',
$validator->errors()->first('language_id'));
}
public function test_language_consistency_validation_fails_when_channel_lacks_language(): void
{
$language1 = Language::factory()->create();
$language2 = Language::factory()->create();
$feed = Feed::factory()->create();
$channel = PlatformChannel::factory()->create(['language_id' => $language2->id]);
// Attach language1 to feed, but channel has language2
$feed->languages()->attach($language1->id, [
'url' => $feed->url,
'is_active' => true,
'is_primary' => true
]);
$data = [
'feed_id' => $feed->id,
'platform_channel_id' => $channel->id,
'language_id' => $language1->id,
'is_active' => true,
'priority' => 50
];
$request = new StoreRouteRequest();
$request->merge($data);
$rules = $request->rules();
$validator = Validator::make($data, $rules);
// Call the custom validation method
$request->withValidator($validator);
$this->assertFalse($validator->passes());
$this->assertStringContainsString('The selected channel does not support this language',
$validator->errors()->first('language_id'));
}
public function test_custom_messages(): void
{
$request = new StoreRouteRequest();
$messages = $request->messages();
$this->assertArrayHasKey('feed_id.required', $messages);
$this->assertArrayHasKey('platform_channel_id.required', $messages);
$this->assertArrayHasKey('language_id.required', $messages);
$this->assertEquals('A feed must be selected.', $messages['feed_id.required']);
$this->assertEquals('A platform channel must be selected.', $messages['platform_channel_id.required']);
$this->assertEquals('A language must be selected.', $messages['language_id.required']);
}
}