114 - Select channel communities from the instance instead of typing a slug
This commit is contained in:
parent
358171c8cc
commit
d0d81e524e
25 changed files with 1043 additions and 193 deletions
|
|
@ -10,7 +10,7 @@
|
||||||
|
|
||||||
class CreateChannelAction
|
class CreateChannelAction
|
||||||
{
|
{
|
||||||
public function execute(string $name, int $platformInstanceId, ?int $languageId = null, ?string $description = null): PlatformChannel
|
public function execute(string $name, int $communityId, int $platformInstanceId, ?int $languageId = null, ?string $description = null): PlatformChannel
|
||||||
{
|
{
|
||||||
$platformInstance = PlatformInstance::findOrFail($platformInstanceId);
|
$platformInstance = PlatformInstance::findOrFail($platformInstanceId);
|
||||||
|
|
||||||
|
|
@ -22,10 +22,10 @@ public function execute(string $name, int $platformInstanceId, ?int $languageId
|
||||||
throw new RuntimeException('No active platform accounts found for this instance. Please create a platform account first.');
|
throw new RuntimeException('No active platform accounts found for this instance. Please create a platform account first.');
|
||||||
}
|
}
|
||||||
|
|
||||||
return DB::transaction(function () use ($name, $platformInstanceId, $languageId, $description, $activeAccounts) {
|
return DB::transaction(function () use ($name, $communityId, $platformInstanceId, $languageId, $description, $activeAccounts) {
|
||||||
$channel = PlatformChannel::create([
|
$channel = PlatformChannel::create([
|
||||||
'platform_instance_id' => $platformInstanceId,
|
'platform_instance_id' => $platformInstanceId,
|
||||||
'channel_id' => $name,
|
'channel_id' => $communityId,
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
'display_name' => ucfirst($name),
|
'display_name' => ucfirst($name),
|
||||||
'description' => $description,
|
'description' => $description,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@
|
||||||
use App\Http\Resources\PlatformChannelResource;
|
use App\Http\Resources\PlatformChannelResource;
|
||||||
use App\Models\PlatformAccount;
|
use App\Models\PlatformAccount;
|
||||||
use App\Models\PlatformChannel;
|
use App\Models\PlatformChannel;
|
||||||
|
use App\Models\PlatformInstance;
|
||||||
|
use App\Services\Platform\CommunityDirectory;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Database\UniqueConstraintViolationException;
|
use Illuminate\Database\UniqueConstraintViolationException;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
|
|
@ -40,8 +42,12 @@ public function store(StorePlatformChannelRequest $request, CreateChannelAction
|
||||||
try {
|
try {
|
||||||
$validated = $request->validated();
|
$validated = $request->validated();
|
||||||
|
|
||||||
|
$instance = PlatformInstance::query()->findOrFail((int) $validated['platform_instance_id']);
|
||||||
|
$name = app(CommunityDirectory::class)->name($instance, (int) $validated['channel_id']);
|
||||||
|
|
||||||
$channel = $createChannelAction->execute(
|
$channel = $createChannelAction->execute(
|
||||||
$validated['name'],
|
$name,
|
||||||
|
(int) $validated['channel_id'],
|
||||||
$validated['platform_instance_id'],
|
$validated['platform_instance_id'],
|
||||||
$validated['language_id'] ?? null,
|
$validated['language_id'] ?? null,
|
||||||
$validated['description'] ?? null,
|
$validated['description'] ?? null,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,9 @@
|
||||||
|
|
||||||
namespace App\Http\Requests;
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use App\Models\PlatformInstance;
|
||||||
|
use App\Services\Platform\CommunityDirectory;
|
||||||
|
use Exception;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
|
@ -17,19 +20,22 @@ public function authorize(): bool
|
||||||
*/
|
*/
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
|
try {
|
||||||
|
$communityRules = [
|
||||||
|
Rule::in($this->communityIds()),
|
||||||
|
Rule::unique('platform_channels', 'channel_id')
|
||||||
|
->where('platform_instance_id', $this->input('platform_instance_id')),
|
||||||
|
];
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// Falling through to Rule::in([]) would report the community as non-existent
|
||||||
|
// when the truth is we never reached the instance to check.
|
||||||
|
$message = 'Could not reach this instance to list its communities: '.$e->getMessage();
|
||||||
|
$communityRules = [fn ($attribute, $value, $fail) => $fail($message)];
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'platform_instance_id' => 'required|exists:platform_instances,id',
|
'platform_instance_id' => 'required|exists:platform_instances,id',
|
||||||
// name doubles as the Lemmy community slug (CreateChannelAction copies it
|
'channel_id' => ['required', 'integer', ...$communityRules],
|
||||||
// verbatim into channel_id for community lookup at publish time), so it must
|
|
||||||
// be slug format and unique per instance — matching the Livewire create form.
|
|
||||||
'name' => [
|
|
||||||
'required',
|
|
||||||
'string',
|
|
||||||
'max:255',
|
|
||||||
'regex:/^[a-z0-9_]+$/',
|
|
||||||
Rule::unique('platform_channels', 'name')
|
|
||||||
->where('platform_instance_id', $this->input('platform_instance_id')),
|
|
||||||
],
|
|
||||||
'language_id' => 'nullable|exists:languages,id',
|
'language_id' => 'nullable|exists:languages,id',
|
||||||
'description' => 'nullable|string',
|
'description' => 'nullable|string',
|
||||||
];
|
];
|
||||||
|
|
@ -41,8 +47,24 @@ public function rules(): array
|
||||||
public function messages(): array
|
public function messages(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'name.regex' => 'The name must be a valid community slug (lowercase letters, numbers, and underscores only).',
|
'channel_id.in' => 'That community does not exist on the selected instance.',
|
||||||
'name.unique' => 'A channel with this name already exists for this instance.',
|
'channel_id.unique' => 'A channel for this community already exists.',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, int>
|
||||||
|
*/
|
||||||
|
private function communityIds(): array
|
||||||
|
{
|
||||||
|
$instance = PlatformInstance::query()->find((int) $this->input('platform_instance_id'));
|
||||||
|
|
||||||
|
if (! $instance) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return collect(app(CommunityDirectory::class)->forInstance($instance))
|
||||||
|
->pluck('id')
|
||||||
|
->all();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,9 +68,7 @@ private function syncLemmyChannelPosts(LogSaver $logSaver): void
|
||||||
$api = $this->makeApiService($this->channel->platformInstance->url);
|
$api = $this->makeApiService($this->channel->platformInstance->url);
|
||||||
$token = $this->getAuthToken($api, $account);
|
$token = $this->getAuthToken($api, $account);
|
||||||
|
|
||||||
$communityId = $api->resolveCommunityId($this->channel->channel_id, $token);
|
$api->syncChannelPosts($token, $this->channel, $this->channel->channel_id);
|
||||||
|
|
||||||
$api->syncChannelPosts($token, $this->channel, $communityId);
|
|
||||||
|
|
||||||
$logSaver->info('Channel posts synced successfully', $this->channel);
|
$logSaver->info('Channel posts synced successfully', $this->channel);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@
|
||||||
use App\Models\PlatformAccount;
|
use App\Models\PlatformAccount;
|
||||||
use App\Models\PlatformChannel;
|
use App\Models\PlatformChannel;
|
||||||
use App\Models\PlatformInstance;
|
use App\Models\PlatformInstance;
|
||||||
|
use App\Services\Platform\CommunityDirectory;
|
||||||
|
use Exception;
|
||||||
use Illuminate\Contracts\View\View;
|
use Illuminate\Contracts\View\View;
|
||||||
use Illuminate\Database\UniqueConstraintViolationException;
|
use Illuminate\Database\UniqueConstraintViolationException;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
|
|
@ -19,10 +21,15 @@ class Channels extends Component
|
||||||
|
|
||||||
public bool $showCreateModal = false;
|
public bool $showCreateModal = false;
|
||||||
|
|
||||||
public string $newName = '';
|
public ?int $newCommunityId = null;
|
||||||
|
|
||||||
public ?int $newPlatformInstanceId = null;
|
public ?int $newPlatformInstanceId = null;
|
||||||
|
|
||||||
|
/** @var array<int, array{id: int, name: string, title: string}> */
|
||||||
|
public array $availableCommunities = [];
|
||||||
|
|
||||||
|
public ?string $communityLoadError = null;
|
||||||
|
|
||||||
public ?int $newLanguageId = null;
|
public ?int $newLanguageId = null;
|
||||||
|
|
||||||
public string $newDescription = '';
|
public string $newDescription = '';
|
||||||
|
|
@ -36,11 +43,44 @@ public function toggle(int $channelId): void
|
||||||
|
|
||||||
public function openCreateModal(): void
|
public function openCreateModal(): void
|
||||||
{
|
{
|
||||||
$this->reset(['newName', 'newPlatformInstanceId', 'newLanguageId', 'newDescription']);
|
$this->reset(['newCommunityId', 'newPlatformInstanceId', 'newLanguageId', 'newDescription', 'availableCommunities', 'communityLoadError']);
|
||||||
$this->resetErrorBag();
|
$this->resetErrorBag();
|
||||||
$this->showCreateModal = true;
|
$this->showCreateModal = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function updatedNewPlatformInstanceId(?int $value): void
|
||||||
|
{
|
||||||
|
$this->reset(['newCommunityId', 'availableCommunities', 'communityLoadError']);
|
||||||
|
|
||||||
|
if (! $value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$instance = PlatformInstance::find($value);
|
||||||
|
|
||||||
|
if (! $instance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->availableCommunities = app(CommunityDirectory::class)->forInstance($instance);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->communityLoadError = 'Could not reach this instance to list its communities: '.$e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function refreshCommunities(): void
|
||||||
|
{
|
||||||
|
$instance = $this->newPlatformInstanceId ? PlatformInstance::find($this->newPlatformInstanceId) : null;
|
||||||
|
|
||||||
|
if (! $instance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
app(CommunityDirectory::class)->forget($instance);
|
||||||
|
$this->updatedNewPlatformInstanceId($this->newPlatformInstanceId);
|
||||||
|
}
|
||||||
|
|
||||||
public function closeCreateModal(): void
|
public function closeCreateModal(): void
|
||||||
{
|
{
|
||||||
$this->showCreateModal = false;
|
$this->showCreateModal = false;
|
||||||
|
|
@ -49,36 +89,33 @@ public function closeCreateModal(): void
|
||||||
public function createChannel(CreateChannelAction $action): void
|
public function createChannel(CreateChannelAction $action): void
|
||||||
{
|
{
|
||||||
$this->validate([
|
$this->validate([
|
||||||
// name doubles as the Lemmy community slug (used verbatim as channel_id for
|
'newCommunityId' => [
|
||||||
// community lookup at publish time), so it must be lowercase slug format.
|
|
||||||
'newName' => [
|
|
||||||
'required',
|
'required',
|
||||||
'string',
|
'integer',
|
||||||
'max:255',
|
Rule::in(collect($this->availableCommunities)->pluck('id')->all()),
|
||||||
'regex:/^[a-z0-9_]+$/',
|
Rule::unique('platform_channels', 'channel_id')
|
||||||
Rule::unique('platform_channels', 'name')
|
|
||||||
->where('platform_instance_id', $this->newPlatformInstanceId),
|
->where('platform_instance_id', $this->newPlatformInstanceId),
|
||||||
],
|
],
|
||||||
'newPlatformInstanceId' => 'required|integer|exists:platform_instances,id',
|
'newPlatformInstanceId' => 'required|integer|exists:platform_instances,id',
|
||||||
'newLanguageId' => 'nullable|integer|exists:languages,id',
|
'newLanguageId' => 'nullable|integer|exists:languages,id',
|
||||||
], [
|
], [
|
||||||
'newName.regex' => 'The name must be a valid community slug (lowercase letters, numbers, and underscores only).',
|
'newCommunityId.in' => 'Select a community from this instance.',
|
||||||
'newName.unique' => 'A channel with this name already exists for this instance.',
|
'newCommunityId.unique' => 'A channel for this community already exists.',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$name = collect($this->availableCommunities)->firstWhere('id', $this->newCommunityId)['name'] ?? null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$action->execute(
|
$action->execute(
|
||||||
$this->newName,
|
$name,
|
||||||
|
$this->newCommunityId,
|
||||||
$this->newPlatformInstanceId,
|
$this->newPlatformInstanceId,
|
||||||
$this->newLanguageId,
|
$this->newLanguageId,
|
||||||
// Blade textarea binds an empty string when blank; the action expects null for "no description".
|
// Blade textarea binds an empty string when blank; the action expects null for "no description".
|
||||||
$this->newDescription !== '' ? $this->newDescription : null,
|
$this->newDescription !== '' ? $this->newDescription : null,
|
||||||
);
|
);
|
||||||
} catch (UniqueConstraintViolationException $e) {
|
} catch (UniqueConstraintViolationException $e) {
|
||||||
// Unreachable via this form (the unique rule above catches duplicates first),
|
$this->addError('newCommunityId', 'A channel for this community already exists.');
|
||||||
// but the (platform_instance_id, channel_id) index can still fire if channel_id
|
|
||||||
// ever drifts from name. Surface it as a field error instead of a 500.
|
|
||||||
$this->addError('newName', 'A channel with this name already exists for this instance.');
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
} catch (RuntimeException $e) {
|
} catch (RuntimeException $e) {
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,10 @@
|
||||||
use App\Models\Route;
|
use App\Models\Route;
|
||||||
use App\Models\Setting;
|
use App\Models\Setting;
|
||||||
use App\Services\OnboardingService;
|
use App\Services\OnboardingService;
|
||||||
|
use App\Services\Platform\CommunityDirectory;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Contracts\View\View;
|
use Illuminate\Contracts\View\View;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use Livewire\Attributes\Locked;
|
use Livewire\Attributes\Locked;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
@ -49,7 +51,12 @@ class Onboarding extends Component
|
||||||
public string $feedDescription = '';
|
public string $feedDescription = '';
|
||||||
|
|
||||||
// Channel form
|
// Channel form
|
||||||
public string $channelName = '';
|
public ?int $channelCommunityId = null;
|
||||||
|
|
||||||
|
/** @var array<int, array{id: int, name: string, title: string}> */
|
||||||
|
public array $availableCommunities = [];
|
||||||
|
|
||||||
|
public ?string $communityLoadError = null;
|
||||||
|
|
||||||
public ?int $platformInstanceId = null;
|
public ?int $platformInstanceId = null;
|
||||||
|
|
||||||
|
|
@ -117,10 +124,11 @@ public function mount(): void
|
||||||
// Pre-fill channel form if exists
|
// Pre-fill channel form if exists
|
||||||
$channel = PlatformChannel::where('is_active', true)->first();
|
$channel = PlatformChannel::where('is_active', true)->first();
|
||||||
if ($channel) {
|
if ($channel) {
|
||||||
$this->channelName = $channel->name;
|
|
||||||
$this->platformInstanceId = $channel->platform_instance_id;
|
$this->platformInstanceId = $channel->platform_instance_id;
|
||||||
$this->channelLanguageId = $channel->language_id;
|
$this->channelLanguageId = $channel->language_id;
|
||||||
$this->channelDescription = $channel->description ?? '';
|
$this->channelDescription = $channel->description ?? '';
|
||||||
|
$this->loadCommunities();
|
||||||
|
$this->channelCommunityId = $channel->channel_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pre-fill route form if exists
|
// Pre-fill route form if exists
|
||||||
|
|
@ -252,16 +260,61 @@ public function createFeed(): void
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function updatedPlatformInstanceId(?int $value): void
|
||||||
|
{
|
||||||
|
$this->reset(['channelCommunityId', 'availableCommunities', 'communityLoadError']);
|
||||||
|
|
||||||
|
if ($value) {
|
||||||
|
$this->loadCommunities();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function refreshCommunities(): void
|
||||||
|
{
|
||||||
|
$instance = $this->platformInstanceId ? PlatformInstance::find($this->platformInstanceId) : null;
|
||||||
|
|
||||||
|
if (! $instance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
app(CommunityDirectory::class)->forget($instance);
|
||||||
|
$this->loadCommunities();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function loadCommunities(): void
|
||||||
|
{
|
||||||
|
$this->availableCommunities = [];
|
||||||
|
$this->communityLoadError = null;
|
||||||
|
|
||||||
|
$instance = $this->platformInstanceId ? PlatformInstance::find($this->platformInstanceId) : null;
|
||||||
|
|
||||||
|
if (! $instance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->availableCommunities = app(CommunityDirectory::class)->forInstance($instance);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->communityLoadError = 'Could not reach this instance to list its communities: '.$e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function createChannel(): void
|
public function createChannel(): void
|
||||||
{
|
{
|
||||||
$this->formErrors = [];
|
$this->formErrors = [];
|
||||||
$this->isLoading = true;
|
$this->isLoading = true;
|
||||||
|
|
||||||
$this->validate([
|
$this->validate([
|
||||||
'channelName' => 'required|string|max:255',
|
'channelCommunityId' => [
|
||||||
|
'required',
|
||||||
|
'integer',
|
||||||
|
Rule::in(collect($this->availableCommunities)->pluck('id')->all()),
|
||||||
|
],
|
||||||
'platformInstanceId' => 'required|exists:platform_instances,id',
|
'platformInstanceId' => 'required|exists:platform_instances,id',
|
||||||
'channelLanguageId' => 'required|exists:languages,id',
|
'channelLanguageId' => 'required|exists:languages,id',
|
||||||
'channelDescription' => 'nullable|string|max:1000',
|
'channelDescription' => 'nullable|string|max:1000',
|
||||||
|
], [
|
||||||
|
'channelCommunityId.in' => 'Select a community from this instance.',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// If language changed, reset feed form
|
// If language changed, reset feed form
|
||||||
|
|
@ -274,11 +327,14 @@ public function createChannel(): void
|
||||||
}
|
}
|
||||||
$this->previousChannelLanguageId = $this->channelLanguageId;
|
$this->previousChannelLanguageId = $this->channelLanguageId;
|
||||||
|
|
||||||
|
$name = collect($this->availableCommunities)->firstWhere('id', $this->channelCommunityId)['name'] ?? null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$channel = $this->createChannelAction->execute(
|
$channel = $this->createChannelAction->execute(
|
||||||
$this->channelName,
|
$name,
|
||||||
$this->platformInstanceId,
|
(int) $this->channelCommunityId,
|
||||||
$this->channelLanguageId,
|
(int) $this->platformInstanceId,
|
||||||
|
$this->channelLanguageId !== null ? (int) $this->channelLanguageId : null,
|
||||||
$this->channelDescription ?: null,
|
$this->channelDescription ?: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
* @property int $id
|
* @property int $id
|
||||||
* @property int $platform_instance_id
|
* @property int $platform_instance_id
|
||||||
* @property PlatformInstance $platformInstance
|
* @property PlatformInstance $platformInstance
|
||||||
* @property string $channel_id
|
* @property int $channel_id
|
||||||
* @property string $name
|
* @property string $name
|
||||||
* @property int $language_id
|
* @property int $language_id
|
||||||
* @property Language|null $language
|
* @property Language|null $language
|
||||||
|
|
@ -40,6 +40,7 @@ class PlatformChannel extends Model
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'is_active' => 'boolean',
|
'is_active' => 'boolean',
|
||||||
|
'channel_id' => 'integer',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -84,18 +84,35 @@ public function login(string $username, string $password): ?string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve a PlatformChannel.channel_id to a numeric Lemmy community id.
|
* @return array<int, array{id: int, name: string, title: string}>
|
||||||
*
|
|
||||||
* channel_id holds either a community slug (the usual case — CreateChannelAction
|
|
||||||
* copies `name` into it) or an already-numeric community id. Callers that need the
|
|
||||||
* numeric id should use this rather than reimplementing the check, so the two forms
|
|
||||||
* stay handled identically everywhere.
|
|
||||||
*/
|
*/
|
||||||
public function resolveCommunityId(string $channelId, string $token): int
|
public function listCommunities(?string $token = null): array
|
||||||
{
|
{
|
||||||
return is_numeric($channelId)
|
$request = new LemmyRequest($this->instance, $token);
|
||||||
? (int) $channelId
|
$response = $request->get('community/list', [
|
||||||
: $this->getCommunityId($channelId, $token);
|
'type_' => 'Local',
|
||||||
|
'limit' => 50,
|
||||||
|
'sort' => 'TopAll',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (! $response->successful()) {
|
||||||
|
throw new Exception('Failed to list communities: '.$response->status());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var array<int, array<string, mixed>> $communities */
|
||||||
|
$communities = $response->json('communities') ?? [];
|
||||||
|
|
||||||
|
return collect($communities)
|
||||||
|
->pluck('community')
|
||||||
|
->reject(fn ($community) => ($community['removed'] ?? false) || ($community['deleted'] ?? false))
|
||||||
|
->map(fn ($community) => [
|
||||||
|
'id' => (int) $community['id'],
|
||||||
|
'name' => (string) $community['name'],
|
||||||
|
'title' => (string) ($community['title'] ?? $community['name']),
|
||||||
|
])
|
||||||
|
->sortBy('name')
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCommunityId(string $communityName, string $token): int
|
public function getCommunityId(string $communityName, string $token): int
|
||||||
|
|
|
||||||
|
|
@ -54,13 +54,11 @@ private function createPost(string $token, array $extractedData, PlatformChannel
|
||||||
{
|
{
|
||||||
$languageId = $extractedData['language_id'] ?? null;
|
$languageId = $extractedData['language_id'] ?? null;
|
||||||
|
|
||||||
$communityId = $this->api->resolveCommunityId($channel->channel_id, $token);
|
|
||||||
|
|
||||||
return $this->api->createPost(
|
return $this->api->createPost(
|
||||||
$token,
|
$token,
|
||||||
$extractedData['title'] ?? 'Untitled',
|
$extractedData['title'] ?? 'Untitled',
|
||||||
$extractedData['description'] ?? '',
|
$extractedData['description'] ?? '',
|
||||||
$communityId,
|
$channel->channel_id,
|
||||||
$article->url,
|
$article->url,
|
||||||
$extractedData['thumbnail'] ?? null,
|
$extractedData['thumbnail'] ?? null,
|
||||||
$languageId
|
$languageId
|
||||||
|
|
|
||||||
51
app/Services/Platform/CommunityDirectory.php
Normal file
51
app/Services/Platform/CommunityDirectory.php
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Platform;
|
||||||
|
|
||||||
|
use App\Models\PlatformInstance;
|
||||||
|
use App\Modules\Lemmy\Services\LemmyApiService;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
|
class CommunityDirectory
|
||||||
|
{
|
||||||
|
private const TTL_SECONDS = 86400;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, array{id: int, name: string, title: string}>
|
||||||
|
*/
|
||||||
|
public function forInstance(PlatformInstance $instance): array
|
||||||
|
{
|
||||||
|
return Cache::remember(
|
||||||
|
self::cacheKey($instance),
|
||||||
|
self::TTL_SECONDS,
|
||||||
|
fn () => $this->makeApi($instance->url)->listCommunities()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function forget(PlatformInstance $instance): void
|
||||||
|
{
|
||||||
|
Cache::forget(self::cacheKey($instance));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function has(PlatformInstance $instance, int $communityId): bool
|
||||||
|
{
|
||||||
|
return collect($this->forInstance($instance))
|
||||||
|
->contains(fn (array $community) => $community['id'] === $communityId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function name(PlatformInstance $instance, int $communityId): ?string
|
||||||
|
{
|
||||||
|
return collect($this->forInstance($instance))
|
||||||
|
->firstWhere('id', $communityId)['name'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function makeApi(string $instanceUrl): LemmyApiService
|
||||||
|
{
|
||||||
|
return new LemmyApiService($instanceUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function cacheKey(PlatformInstance $instance): string
|
||||||
|
{
|
||||||
|
return "platform:communities:{$instance->id}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -18,7 +18,7 @@ public function definition(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'platform_instance_id' => PlatformInstance::factory(),
|
'platform_instance_id' => PlatformInstance::factory(),
|
||||||
'channel_id' => $this->faker->slug(2),
|
'channel_id' => $this->faker->unique()->numberBetween(1, 999999),
|
||||||
'name' => $this->faker->words(2, true),
|
'name' => $this->faker->words(2, true),
|
||||||
'display_name' => $this->faker->words(2, true),
|
'display_name' => $this->faker->words(2, true),
|
||||||
'language_id' => Language::factory(),
|
'language_id' => Language::factory(),
|
||||||
|
|
@ -39,7 +39,6 @@ public function community(?string $name = null): static
|
||||||
$communityName = $name ?: $this->faker->word();
|
$communityName = $name ?: $this->faker->word();
|
||||||
|
|
||||||
return $this->state(fn (array $attributes) => [
|
return $this->state(fn (array $attributes) => [
|
||||||
'channel_id' => strtolower($communityName),
|
|
||||||
'name' => $communityName,
|
'name' => $communityName,
|
||||||
'display_name' => ucfirst($communityName),
|
'display_name' => ucfirst($communityName),
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\PlatformAccount;
|
||||||
|
use App\Modules\Lemmy\Services\LemmyApiService;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* channel_id held a community slug, resolved to Lemmy's numeric id on every
|
||||||
|
* publish and every sync. It now holds that id directly; `name` remains the slug.
|
||||||
|
*/
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// Every id is resolved before any DDL runs: these lookups hit the live
|
||||||
|
// instance and MariaDB will not roll back a schema change if one fails.
|
||||||
|
$resolved = DB::table('platform_channels')
|
||||||
|
->orderBy('id')
|
||||||
|
->get()
|
||||||
|
->mapWithKeys(fn (object $channel) => [$channel->id => $this->resolve($channel)]);
|
||||||
|
|
||||||
|
Schema::table('platform_channels', function (Blueprint $table) {
|
||||||
|
$table->dropUnique('platform_channels_channel_id_unique');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('platform_channels', function (Blueprint $table) {
|
||||||
|
$table->unsignedBigInteger('remote_community_id')->nullable()->after('channel_id');
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach ($resolved as $id => $communityId) {
|
||||||
|
DB::table('platform_channels')
|
||||||
|
->where('id', $id)
|
||||||
|
->update(['remote_community_id' => $communityId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Schema::table('platform_channels', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('channel_id');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('platform_channels', function (Blueprint $table) {
|
||||||
|
$table->renameColumn('remote_community_id', 'channel_id');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('platform_channels', function (Blueprint $table) {
|
||||||
|
$table->unsignedBigInteger('channel_id')->nullable(false)->change();
|
||||||
|
$table->unique(['platform_instance_id', 'channel_id'], 'platform_channels_channel_id_unique');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('platform_channels', function (Blueprint $table) {
|
||||||
|
$table->dropUnique('platform_channels_channel_id_unique');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('platform_channels', function (Blueprint $table) {
|
||||||
|
$table->string('channel_id')->change();
|
||||||
|
});
|
||||||
|
|
||||||
|
DB::table('platform_channels')->update(['channel_id' => DB::raw('name')]);
|
||||||
|
|
||||||
|
Schema::table('platform_channels', function (Blueprint $table) {
|
||||||
|
$table->unique(['platform_instance_id', 'channel_id'], 'platform_channels_channel_id_unique');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolve(object $channel): int
|
||||||
|
{
|
||||||
|
if (is_numeric($channel->channel_id)) {
|
||||||
|
return (int) $channel->channel_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$instance = DB::table('platform_instances')->find($channel->platform_instance_id);
|
||||||
|
|
||||||
|
if (! $instance) {
|
||||||
|
throw new RuntimeException("Channel {$channel->id} has no platform instance; cannot resolve its community id.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$account = PlatformAccount::where('instance_url', $instance->url)
|
||||||
|
->where('is_active', true)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (! $account) {
|
||||||
|
throw new RuntimeException("No active account for {$instance->url}; cannot resolve community '{$channel->channel_id}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$api = new LemmyApiService($instance->url);
|
||||||
|
$token = $api->login($account->username, $account->password);
|
||||||
|
|
||||||
|
if (! $token) {
|
||||||
|
throw new RuntimeException("Could not authenticate against {$instance->url} to resolve community '{$channel->channel_id}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $api->getCommunityId($channel->channel_id, $token);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -162,22 +162,11 @@ class="w-full inline-flex justify-center rounded-md border border-gray-300 shado
|
||||||
@if ($showCreateModal)
|
@if ($showCreateModal)
|
||||||
<x-form-modal title="Add Channel" close="closeCreateModal">
|
<x-form-modal title="Add Channel" close="closeCreateModal">
|
||||||
<form wire:submit="createChannel" class="space-y-4">
|
<form wire:submit="createChannel" class="space-y-4">
|
||||||
<div>
|
|
||||||
<label for="new-channel-name" class="block text-sm font-medium text-gray-700">Name</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="new-channel-name"
|
|
||||||
wire:model="newName"
|
|
||||||
class="mt-1 block w-full rounded-md border border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
|
|
||||||
/>
|
|
||||||
@error('newName') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="new-channel-instance" class="block text-sm font-medium text-gray-700">Platform Instance</label>
|
<label for="new-channel-instance" class="block text-sm font-medium text-gray-700">Platform Instance</label>
|
||||||
<select
|
<select
|
||||||
id="new-channel-instance"
|
id="new-channel-instance"
|
||||||
wire:model="newPlatformInstanceId"
|
wire:model.live="newPlatformInstanceId"
|
||||||
class="mt-1 block w-full rounded-md border border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
|
class="mt-1 block w-full rounded-md border border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
|
||||||
>
|
>
|
||||||
<option value="">Select an instance</option>
|
<option value="">Select an instance</option>
|
||||||
|
|
@ -186,8 +175,37 @@ class="mt-1 block w-full rounded-md border border-gray-300 shadow-sm focus:borde
|
||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</select>
|
||||||
@error('newPlatformInstanceId') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
|
@error('newPlatformInstanceId') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
|
||||||
|
@if ($communityLoadError)
|
||||||
|
<p class="mt-1 text-sm text-red-600">{{ $communityLoadError }}</p>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if ($availableCommunities)
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<label for="new-channel-community" class="block text-sm font-medium text-gray-700">Community</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
wire:click="refreshCommunities"
|
||||||
|
class="text-xs text-blue-600 hover:text-blue-800"
|
||||||
|
>
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<select
|
||||||
|
id="new-channel-community"
|
||||||
|
wire:model="newCommunityId"
|
||||||
|
class="mt-1 block w-full rounded-md border border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
|
||||||
|
>
|
||||||
|
<option value="">Select a community</option>
|
||||||
|
@foreach ($availableCommunities as $community)
|
||||||
|
<option value="{{ $community['id'] }}">{{ $community['title'] }} ({{ $community['name'] }})</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
@error('newCommunityId') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="new-channel-language" class="block text-sm font-medium text-gray-700">Language <span class="text-gray-400">(optional)</span></label>
|
<label for="new-channel-language" class="block text-sm font-medium text-gray-700">Language <span class="text-gray-400">(optional)</span></label>
|
||||||
<select
|
<select
|
||||||
|
|
|
||||||
|
|
@ -196,29 +196,13 @@ class="bg-blue-600 text-white py-2 px-6 rounded-md hover:bg-blue-700 transition
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="channelName" class="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Community Name
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="channelName"
|
|
||||||
wire:model="channelName"
|
|
||||||
placeholder="technology"
|
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<p class="text-sm text-gray-500 mt-1">Enter the community name (without the @ or instance)</p>
|
|
||||||
@error('channelName') <p class="text-red-600 text-sm mt-1">{{ $message }}</p> @enderror
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="platformInstanceId" class="block text-sm font-medium text-gray-700 mb-2">
|
<label for="platformInstanceId" class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Platform Instance
|
Platform Instance
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
id="platformInstanceId"
|
id="platformInstanceId"
|
||||||
wire:model="platformInstanceId"
|
wire:model.live="platformInstanceId"
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
|
|
@ -228,8 +212,40 @@ class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none foc
|
||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</select>
|
||||||
@error('platformInstanceId') <p class="text-red-600 text-sm mt-1">{{ $message }}</p> @enderror
|
@error('platformInstanceId') <p class="text-red-600 text-sm mt-1">{{ $message }}</p> @enderror
|
||||||
|
@if ($communityLoadError)
|
||||||
|
<p class="text-red-600 text-sm mt-1">{{ $communityLoadError }}</p>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if ($availableCommunities)
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<label for="channelCommunityId" class="block text-sm font-medium text-gray-700">
|
||||||
|
Community
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
wire:click="refreshCommunities"
|
||||||
|
class="text-xs text-blue-600 hover:text-blue-800"
|
||||||
|
>
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<select
|
||||||
|
id="channelCommunityId"
|
||||||
|
wire:model="channelCommunityId"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="">Select a community</option>
|
||||||
|
@foreach ($availableCommunities as $community)
|
||||||
|
<option value="{{ $community['id'] }}">{{ $community['title'] }} ({{ $community['name'] }})</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
@error('channelCommunityId') <p class="text-red-600 text-sm mt-1">{{ $message }}</p> @enderror
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="channelLanguageId" class="block text-sm font-medium text-gray-700 mb-2">
|
<label for="channelLanguageId" class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Language
|
Language
|
||||||
|
|
|
||||||
82
tests/Feature/CommunityDirectoryTest.php
Normal file
82
tests/Feature/CommunityDirectoryTest.php
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\PlatformInstance;
|
||||||
|
use App\Services\Platform\CommunityDirectory;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class CommunityDirectoryTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
private PlatformInstance $instance;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
Cache::flush();
|
||||||
|
$this->instance = PlatformInstance::factory()->create(['url' => 'https://lemmy.test']);
|
||||||
|
|
||||||
|
Http::fake(['*/api/v3/community/list*' => Http::response([
|
||||||
|
'communities' => [
|
||||||
|
['community' => ['id' => 8, 'name' => 'news', 'title' => 'News']],
|
||||||
|
['community' => ['id' => 42, 'name' => '42', 'title' => 'Forty Two']],
|
||||||
|
],
|
||||||
|
])]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function directory(): CommunityDirectory
|
||||||
|
{
|
||||||
|
return app(CommunityDirectory::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_fetches_communities_for_an_instance(): void
|
||||||
|
{
|
||||||
|
$this->assertSame([42, 8], collect($this->directory()->forInstance($this->instance))->pluck('id')->all());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_only_calls_the_instance_once_per_cache_window(): void
|
||||||
|
{
|
||||||
|
$this->directory()->forInstance($this->instance);
|
||||||
|
$this->directory()->forInstance($this->instance);
|
||||||
|
|
||||||
|
Http::assertSentCount(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_forget_causes_a_refetch(): void
|
||||||
|
{
|
||||||
|
$this->directory()->forInstance($this->instance);
|
||||||
|
$this->directory()->forget($this->instance);
|
||||||
|
$this->directory()->forInstance($this->instance);
|
||||||
|
|
||||||
|
Http::assertSentCount(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_caches_per_instance(): void
|
||||||
|
{
|
||||||
|
$other = PlatformInstance::factory()->create(['url' => 'https://other.test']);
|
||||||
|
|
||||||
|
$this->directory()->forInstance($this->instance);
|
||||||
|
$this->directory()->forInstance($other);
|
||||||
|
|
||||||
|
Http::assertSentCount(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_has_reports_membership(): void
|
||||||
|
{
|
||||||
|
$this->assertTrue($this->directory()->has($this->instance, 8));
|
||||||
|
$this->assertFalse($this->directory()->has($this->instance, 4242));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_a_numerically_named_community_keeps_its_own_id(): void
|
||||||
|
{
|
||||||
|
// The community is named "42" but its id is also 42 by coincidence; the
|
||||||
|
// name must never be read as an id.
|
||||||
|
$this->assertSame('42', $this->directory()->name($this->instance, 42));
|
||||||
|
$this->assertTrue($this->directory()->has($this->instance, 42));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -99,7 +99,7 @@ private function makeListener(): PublishApprovedArticleListener
|
||||||
return new PublishApprovedArticleListener(new PublishRouteArticleAction($fetcher, $service, new NotificationService));
|
return new PublishApprovedArticleListener(new PublishRouteArticleAction($fetcher, $service, new NotificationService));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_clicking_approve_twice_creates_only_one_remote_post(): void
|
public function test_clicking_approve_twice_dispatches_only_one_event(): void
|
||||||
{
|
{
|
||||||
Event::fake([RouteArticleApproved::class]);
|
Event::fake([RouteArticleApproved::class]);
|
||||||
|
|
||||||
|
|
@ -112,6 +112,25 @@ public function test_clicking_approve_twice_creates_only_one_remote_post(): void
|
||||||
Event::assertDispatchedTimes(RouteArticleApproved::class, 1);
|
Event::assertDispatchedTimes(RouteArticleApproved::class, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_clicking_approve_twice_creates_only_one_remote_post(): void
|
||||||
|
{
|
||||||
|
[$routeArticle, $channel, $article] = $this->fixture;
|
||||||
|
|
||||||
|
$listener = $this->makeListener();
|
||||||
|
|
||||||
|
$routeArticle->approve();
|
||||||
|
$listener->handle(new RouteArticleApproved($routeArticle->fresh()));
|
||||||
|
|
||||||
|
$routeArticle->approve();
|
||||||
|
$listener->handle(new RouteArticleApproved($routeArticle->fresh()));
|
||||||
|
|
||||||
|
$this->assertSame(1, $this->remoteCalls, 'A second approval must not post to Lemmy again.');
|
||||||
|
|
||||||
|
$this->assertSame(1, ArticlePublication::where('article_id', $article->id)
|
||||||
|
->where('platform_channel_id', $channel->id)
|
||||||
|
->count());
|
||||||
|
}
|
||||||
|
|
||||||
public function test_two_queued_listeners_create_only_one_remote_post(): void
|
public function test_two_queued_listeners_create_only_one_remote_post(): void
|
||||||
{
|
{
|
||||||
[$routeArticle, $channel, $article] = $this->fixture;
|
[$routeArticle, $channel, $article] = $this->fixture;
|
||||||
|
|
@ -153,6 +172,50 @@ public function test_two_queued_listeners_create_only_one_remote_post(): void
|
||||||
->count());
|
->count());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pins the #123 mechanism. Before b2d504f, approve() had no isApproved()
|
||||||
|
* guard, so two calls dispatched two events. Two stale instances of the same
|
||||||
|
* row reproduce that: each still reads PENDING, so the guard cannot fire and
|
||||||
|
* the pre-fix code path runs.
|
||||||
|
*/
|
||||||
|
public function test_without_the_approved_guard_two_approvals_dispatch_two_events(): void
|
||||||
|
{
|
||||||
|
Event::fake([RouteArticleApproved::class]);
|
||||||
|
|
||||||
|
[$routeArticle] = $this->fixture;
|
||||||
|
|
||||||
|
$first = RouteArticle::find($routeArticle->id);
|
||||||
|
$second = RouteArticle::find($routeArticle->id);
|
||||||
|
|
||||||
|
$this->assertTrue($second->isPending(), 'Both instances must start pending for the race to be reproduced.');
|
||||||
|
|
||||||
|
$first->approve();
|
||||||
|
$second->approve();
|
||||||
|
|
||||||
|
Event::assertDispatchedTimes(RouteArticleApproved::class, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_two_stale_approvals_still_create_only_one_remote_post(): void
|
||||||
|
{
|
||||||
|
[$routeArticle, $channel, $article] = $this->fixture;
|
||||||
|
|
||||||
|
$first = RouteArticle::find($routeArticle->id);
|
||||||
|
$second = RouteArticle::find($routeArticle->id);
|
||||||
|
|
||||||
|
$first->approve();
|
||||||
|
$second->approve();
|
||||||
|
|
||||||
|
$listener = $this->makeListener();
|
||||||
|
$listener->handle(new RouteArticleApproved($first));
|
||||||
|
$listener->handle(new RouteArticleApproved($second));
|
||||||
|
|
||||||
|
$this->assertSame(1, $this->remoteCalls, 'The lock must hold even when two events get through.');
|
||||||
|
|
||||||
|
$this->assertSame(1, ArticlePublication::where('article_id', $article->id)
|
||||||
|
->where('platform_channel_id', $channel->id)
|
||||||
|
->count());
|
||||||
|
}
|
||||||
|
|
||||||
public function test_a_single_approval_publishes_exactly_once(): void
|
public function test_a_single_approval_publishes_exactly_once(): void
|
||||||
{
|
{
|
||||||
[$routeArticle, $channel, $article] = $this->fixture;
|
[$routeArticle, $channel, $article] = $this->fixture;
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,32 @@
|
||||||
use App\Models\PlatformChannel;
|
use App\Models\PlatformChannel;
|
||||||
use App\Models\PlatformInstance;
|
use App\Models\PlatformInstance;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
class PlatformChannelsControllerTest extends TestCase
|
class PlatformChannelsControllerTest extends TestCase
|
||||||
{
|
{
|
||||||
use RefreshDatabase;
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
private bool $instanceReachable = true;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
Cache::flush();
|
||||||
|
// A stub registered here cannot be overridden by a later Http::fake() for the
|
||||||
|
// same pattern, so tests toggle the outcome through $instanceReachable instead.
|
||||||
|
Http::fake(['*/api/v3/community/list*' => fn () => $this->instanceReachable
|
||||||
|
? Http::response([
|
||||||
|
'communities' => [
|
||||||
|
['community' => ['id' => 8, 'name' => 'test_channel', 'title' => 'Test Channel']],
|
||||||
|
['community' => ['id' => 9, 'name' => 'tech_news', 'title' => 'Tech News']],
|
||||||
|
],
|
||||||
|
])
|
||||||
|
: Http::response([], 503)]);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_index_returns_successful_response(): void
|
public function test_index_returns_successful_response(): void
|
||||||
{
|
{
|
||||||
$instance = PlatformInstance::factory()->create();
|
$instance = PlatformInstance::factory()->create();
|
||||||
|
|
@ -56,7 +76,7 @@ public function test_store_creates_platform_channel_successfully(): void
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
'platform_instance_id' => $instance->id,
|
'platform_instance_id' => $instance->id,
|
||||||
'name' => 'test_channel',
|
'channel_id' => 8,
|
||||||
'description' => 'A test channel',
|
'description' => 'A test channel',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -85,8 +105,7 @@ public function test_store_creates_platform_channel_successfully(): void
|
||||||
|
|
||||||
$this->assertDatabaseHas('platform_channels', [
|
$this->assertDatabaseHas('platform_channels', [
|
||||||
'platform_instance_id' => $instance->id,
|
'platform_instance_id' => $instance->id,
|
||||||
'channel_id' => 'test_channel',
|
'channel_id' => 8,
|
||||||
'name' => 'test_channel',
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,14 +114,14 @@ public function test_store_validates_required_fields(): void
|
||||||
$response = $this->postJson('/api/v1/platform-channels', []);
|
$response = $this->postJson('/api/v1/platform-channels', []);
|
||||||
|
|
||||||
$response->assertStatus(422)
|
$response->assertStatus(422)
|
||||||
->assertJsonValidationErrors(['platform_instance_id', 'name']);
|
->assertJsonValidationErrors(['platform_instance_id', 'channel_id']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_store_validates_platform_instance_exists(): void
|
public function test_store_validates_platform_instance_exists(): void
|
||||||
{
|
{
|
||||||
$data = [
|
$data = [
|
||||||
'platform_instance_id' => 999,
|
'platform_instance_id' => 999,
|
||||||
'name' => 'Test Channel',
|
'channel_id' => 8,
|
||||||
];
|
];
|
||||||
|
|
||||||
$response = $this->postJson('/api/v1/platform-channels', $data);
|
$response = $this->postJson('/api/v1/platform-channels', $data);
|
||||||
|
|
@ -111,44 +130,62 @@ public function test_store_validates_platform_instance_exists(): void
|
||||||
->assertJsonValidationErrors(['platform_instance_id']);
|
->assertJsonValidationErrors(['platform_instance_id']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_store_rejects_non_slug_name(): void
|
public function test_store_rejects_community_not_on_instance(): void
|
||||||
{
|
{
|
||||||
$instance = PlatformInstance::factory()->create();
|
$instance = PlatformInstance::factory()->create();
|
||||||
|
|
||||||
// name is copied verbatim into channel_id and used as the Lemmy community
|
|
||||||
// reference, so non-slug values must be rejected at the API boundary too.
|
|
||||||
$response = $this->postJson('/api/v1/platform-channels', [
|
$response = $this->postJson('/api/v1/platform-channels', [
|
||||||
'platform_instance_id' => $instance->id,
|
'platform_instance_id' => $instance->id,
|
||||||
'name' => 'Tech News',
|
'channel_id' => 4242,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response->assertStatus(422)
|
$response->assertStatus(422)
|
||||||
->assertJsonValidationErrors(['name']);
|
->assertJsonValidationErrors(['channel_id']);
|
||||||
|
|
||||||
$this->assertDatabaseCount('platform_channels', 0);
|
$this->assertDatabaseCount('platform_channels', 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_store_rejects_duplicate_name_for_same_instance(): void
|
public function test_store_distinguishes_an_unreachable_instance_from_a_missing_community(): void
|
||||||
|
{
|
||||||
|
$this->instanceReachable = false;
|
||||||
|
|
||||||
|
$instance = PlatformInstance::factory()->create();
|
||||||
|
PlatformAccount::factory()->create([
|
||||||
|
'instance_url' => $instance->url,
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->postJson('/api/v1/platform-channels', [
|
||||||
|
'platform_instance_id' => $instance->id,
|
||||||
|
'channel_id' => 8,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422);
|
||||||
|
$this->assertStringContainsString('Could not reach this instance', $response->json('message'));
|
||||||
|
$this->assertDatabaseCount('platform_channels', 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_store_rejects_duplicate_community_for_same_instance(): void
|
||||||
{
|
{
|
||||||
$instance = PlatformInstance::factory()->create();
|
$instance = PlatformInstance::factory()->create();
|
||||||
PlatformChannel::factory()->create([
|
PlatformChannel::factory()->create([
|
||||||
'platform_instance_id' => $instance->id,
|
'platform_instance_id' => $instance->id,
|
||||||
'name' => 'tech_news',
|
'name' => 'tech_news',
|
||||||
'channel_id' => 'tech_news',
|
'channel_id' => 9,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response = $this->postJson('/api/v1/platform-channels', [
|
$response = $this->postJson('/api/v1/platform-channels', [
|
||||||
'platform_instance_id' => $instance->id,
|
'platform_instance_id' => $instance->id,
|
||||||
'name' => 'tech_news',
|
'channel_id' => 9,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response->assertStatus(422)
|
$response->assertStatus(422)
|
||||||
->assertJsonValidationErrors(['name']);
|
->assertJsonValidationErrors(['channel_id']);
|
||||||
|
|
||||||
$this->assertDatabaseCount('platform_channels', 1);
|
$this->assertDatabaseCount('platform_channels', 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_store_allows_same_name_on_different_instance(): void
|
public function test_store_allows_same_community_on_different_instance(): void
|
||||||
{
|
{
|
||||||
$instanceA = PlatformInstance::factory()->create();
|
$instanceA = PlatformInstance::factory()->create();
|
||||||
$instanceB = PlatformInstance::factory()->create();
|
$instanceB = PlatformInstance::factory()->create();
|
||||||
|
|
@ -161,12 +198,12 @@ public function test_store_allows_same_name_on_different_instance(): void
|
||||||
PlatformChannel::factory()->create([
|
PlatformChannel::factory()->create([
|
||||||
'platform_instance_id' => $instanceA->id,
|
'platform_instance_id' => $instanceA->id,
|
||||||
'name' => 'tech_news',
|
'name' => 'tech_news',
|
||||||
'channel_id' => 'tech_news',
|
'channel_id' => 9,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response = $this->postJson('/api/v1/platform-channels', [
|
$response = $this->postJson('/api/v1/platform-channels', [
|
||||||
'platform_instance_id' => $instanceB->id,
|
'platform_instance_id' => $instanceB->id,
|
||||||
'name' => 'tech_news',
|
'channel_id' => 9,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response->assertStatus(201);
|
$response->assertStatus(201);
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,9 @@
|
||||||
use App\Models\PlatformChannel;
|
use App\Models\PlatformChannel;
|
||||||
use App\Models\PlatformInstance;
|
use App\Models\PlatformInstance;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Http\Client\Factory;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
|
@ -15,6 +18,23 @@ class ChannelsTest extends TestCase
|
||||||
{
|
{
|
||||||
use RefreshDatabase;
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
Cache::flush();
|
||||||
|
$this->fakeCommunities();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function fakeCommunities(): void
|
||||||
|
{
|
||||||
|
Http::fake(['*/api/v3/community/list*' => Http::response([
|
||||||
|
'communities' => [
|
||||||
|
['community' => ['id' => 8, 'name' => 'tech_community', 'title' => 'Tech Community']],
|
||||||
|
['community' => ['id' => 9, 'name' => 'other_community', 'title' => 'Other Community']],
|
||||||
|
],
|
||||||
|
])]);
|
||||||
|
}
|
||||||
|
|
||||||
private function instanceWithActiveAccount(): PlatformInstance
|
private function instanceWithActiveAccount(): PlatformInstance
|
||||||
{
|
{
|
||||||
$instance = PlatformInstance::factory()->create(['url' => 'https://lemmy.world']);
|
$instance = PlatformInstance::factory()->create(['url' => 'https://lemmy.world']);
|
||||||
|
|
@ -50,7 +70,7 @@ public function test_open_create_modal_shows_modal(): void
|
||||||
->assertSet('showCreateModal', true);
|
->assertSet('showCreateModal', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_create_channel_requires_name(): void
|
public function test_create_channel_requires_community(): void
|
||||||
{
|
{
|
||||||
$instance = $this->instanceWithActiveAccount();
|
$instance = $this->instanceWithActiveAccount();
|
||||||
|
|
||||||
|
|
@ -58,14 +78,44 @@ public function test_create_channel_requires_name(): void
|
||||||
->call('openCreateModal')
|
->call('openCreateModal')
|
||||||
->set('newPlatformInstanceId', $instance->id)
|
->set('newPlatformInstanceId', $instance->id)
|
||||||
->call('createChannel')
|
->call('createChannel')
|
||||||
->assertHasErrors(['newName' => 'required']);
|
->assertHasErrors(['newCommunityId' => 'required']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_selecting_an_instance_loads_its_communities(): void
|
||||||
|
{
|
||||||
|
$instance = $this->instanceWithActiveAccount();
|
||||||
|
|
||||||
|
Livewire::test(Channels::class)
|
||||||
|
->call('openCreateModal')
|
||||||
|
->set('newPlatformInstanceId', $instance->id)
|
||||||
|
->assertSet('communityLoadError', null)
|
||||||
|
->assertCount('availableCommunities', 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_unreachable_instance_surfaces_an_error_and_no_communities(): void
|
||||||
|
{
|
||||||
|
$instance = PlatformInstance::factory()->create(['url' => 'https://unreachable.test']);
|
||||||
|
PlatformAccount::factory()->create(['instance_url' => 'https://unreachable.test', 'is_active' => true]);
|
||||||
|
Cache::flush();
|
||||||
|
|
||||||
|
// Http::fake() merges stubs, so setUp's success stub would still win —
|
||||||
|
// swap the whole fake out instead.
|
||||||
|
app()->forgetInstance(Factory::class);
|
||||||
|
Http::swap(new Factory);
|
||||||
|
Http::fake(['*' => Http::response('nope', 500)]);
|
||||||
|
|
||||||
|
Livewire::test(Channels::class)
|
||||||
|
->call('openCreateModal')
|
||||||
|
->set('newPlatformInstanceId', $instance->id)
|
||||||
|
->assertSet('availableCommunities', [])
|
||||||
|
->assertNotSet('communityLoadError', null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_create_channel_requires_platform_instance(): void
|
public function test_create_channel_requires_platform_instance(): void
|
||||||
{
|
{
|
||||||
Livewire::test(Channels::class)
|
Livewire::test(Channels::class)
|
||||||
->call('openCreateModal')
|
->call('openCreateModal')
|
||||||
->set('newName', 'tech_community')
|
->set('newCommunityId', 8)
|
||||||
->call('createChannel')
|
->call('createChannel')
|
||||||
->assertHasErrors(['newPlatformInstanceId' => 'required']);
|
->assertHasErrors(['newPlatformInstanceId' => 'required']);
|
||||||
}
|
}
|
||||||
|
|
@ -77,8 +127,8 @@ public function test_create_channel_succeeds_and_attaches_account(): void
|
||||||
|
|
||||||
Livewire::test(Channels::class)
|
Livewire::test(Channels::class)
|
||||||
->call('openCreateModal')
|
->call('openCreateModal')
|
||||||
->set('newName', 'tech_community')
|
|
||||||
->set('newPlatformInstanceId', $instance->id)
|
->set('newPlatformInstanceId', $instance->id)
|
||||||
|
->set('newCommunityId', 8)
|
||||||
->set('newLanguageId', $language->id)
|
->set('newLanguageId', $language->id)
|
||||||
->set('newDescription', 'A tech community')
|
->set('newDescription', 'A tech community')
|
||||||
->call('createChannel')
|
->call('createChannel')
|
||||||
|
|
@ -104,8 +154,8 @@ public function test_create_channel_leaves_description_null_when_blank(): void
|
||||||
|
|
||||||
Livewire::test(Channels::class)
|
Livewire::test(Channels::class)
|
||||||
->call('openCreateModal')
|
->call('openCreateModal')
|
||||||
->set('newName', 'tech_community')
|
|
||||||
->set('newPlatformInstanceId', $instance->id)
|
->set('newPlatformInstanceId', $instance->id)
|
||||||
|
->set('newCommunityId', 8)
|
||||||
->call('createChannel')
|
->call('createChannel')
|
||||||
->assertHasNoErrors();
|
->assertHasNoErrors();
|
||||||
|
|
||||||
|
|
@ -115,40 +165,41 @@ public function test_create_channel_leaves_description_null_when_blank(): void
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_create_channel_rejects_non_slug_name(): void
|
public function test_create_channel_rejects_a_community_not_on_the_instance(): void
|
||||||
{
|
{
|
||||||
$instance = $this->instanceWithActiveAccount();
|
$instance = $this->instanceWithActiveAccount();
|
||||||
|
|
||||||
Livewire::test(Channels::class)
|
Livewire::test(Channels::class)
|
||||||
->call('openCreateModal')
|
->call('openCreateModal')
|
||||||
->set('newName', 'Tech News')
|
|
||||||
->set('newPlatformInstanceId', $instance->id)
|
->set('newPlatformInstanceId', $instance->id)
|
||||||
|
->set('newCommunityId', 4242)
|
||||||
->call('createChannel')
|
->call('createChannel')
|
||||||
->assertHasErrors(['newName' => 'regex']);
|
->assertHasErrors(['newCommunityId' => 'in']);
|
||||||
|
|
||||||
$this->assertDatabaseCount('platform_channels', 0);
|
$this->assertDatabaseCount('platform_channels', 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_create_channel_rejects_duplicate_name_on_same_instance(): void
|
public function test_create_channel_rejects_duplicate_community_on_same_instance(): void
|
||||||
{
|
{
|
||||||
$instance = $this->instanceWithActiveAccount();
|
$instance = $this->instanceWithActiveAccount();
|
||||||
PlatformChannel::factory()->create([
|
PlatformChannel::factory()->create([
|
||||||
'platform_instance_id' => $instance->id,
|
'platform_instance_id' => $instance->id,
|
||||||
'name' => 'tech_community',
|
'name' => 'tech_community',
|
||||||
|
'channel_id' => 8,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Livewire::test(Channels::class)
|
Livewire::test(Channels::class)
|
||||||
->call('openCreateModal')
|
->call('openCreateModal')
|
||||||
->set('newName', 'tech_community')
|
|
||||||
->set('newPlatformInstanceId', $instance->id)
|
->set('newPlatformInstanceId', $instance->id)
|
||||||
|
->set('newCommunityId', 8)
|
||||||
->call('createChannel')
|
->call('createChannel')
|
||||||
->assertHasErrors(['newName' => 'unique'])
|
->assertHasErrors(['newCommunityId' => 'unique'])
|
||||||
->assertSet('showCreateModal', true);
|
->assertSet('showCreateModal', true);
|
||||||
|
|
||||||
$this->assertDatabaseCount('platform_channels', 1);
|
$this->assertDatabaseCount('platform_channels', 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_create_channel_allows_same_name_on_different_instance(): void
|
public function test_create_channel_allows_same_community_on_different_instance(): void
|
||||||
{
|
{
|
||||||
$instanceA = $this->instanceWithActiveAccount();
|
$instanceA = $this->instanceWithActiveAccount();
|
||||||
$instanceB = PlatformInstance::factory()->create(['url' => 'https://lemmy.other']);
|
$instanceB = PlatformInstance::factory()->create(['url' => 'https://lemmy.other']);
|
||||||
|
|
@ -159,12 +210,13 @@ public function test_create_channel_allows_same_name_on_different_instance(): vo
|
||||||
PlatformChannel::factory()->create([
|
PlatformChannel::factory()->create([
|
||||||
'platform_instance_id' => $instanceB->id,
|
'platform_instance_id' => $instanceB->id,
|
||||||
'name' => 'tech_community',
|
'name' => 'tech_community',
|
||||||
|
'channel_id' => 8,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Livewire::test(Channels::class)
|
Livewire::test(Channels::class)
|
||||||
->call('openCreateModal')
|
->call('openCreateModal')
|
||||||
->set('newName', 'tech_community')
|
|
||||||
->set('newPlatformInstanceId', $instanceA->id)
|
->set('newPlatformInstanceId', $instanceA->id)
|
||||||
|
->set('newCommunityId', 8)
|
||||||
->call('createChannel')
|
->call('createChannel')
|
||||||
->assertHasNoErrors();
|
->assertHasNoErrors();
|
||||||
|
|
||||||
|
|
@ -178,8 +230,8 @@ public function test_create_channel_surfaces_no_active_accounts_error(): void
|
||||||
|
|
||||||
Livewire::test(Channels::class)
|
Livewire::test(Channels::class)
|
||||||
->call('openCreateModal')
|
->call('openCreateModal')
|
||||||
->set('newName', 'tech_community')
|
|
||||||
->set('newPlatformInstanceId', $instance->id)
|
->set('newPlatformInstanceId', $instance->id)
|
||||||
|
->set('newCommunityId', 8)
|
||||||
->call('createChannel')
|
->call('createChannel')
|
||||||
->assertHasErrors('newPlatformInstanceId')
|
->assertHasErrors('newPlatformInstanceId')
|
||||||
->assertSet('showCreateModal', true);
|
->assertSet('showCreateModal', true);
|
||||||
|
|
|
||||||
202
tests/Feature/Livewire/OnboardingTest.php
Normal file
202
tests/Feature/Livewire/OnboardingTest.php
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Livewire;
|
||||||
|
|
||||||
|
use App\Jobs\SyncChannelPostsJob;
|
||||||
|
use App\Livewire\Onboarding;
|
||||||
|
use App\Models\Language;
|
||||||
|
use App\Models\PlatformAccount;
|
||||||
|
use App\Models\PlatformChannel;
|
||||||
|
use App\Models\PlatformInstance;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Http\Client\Factory;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Illuminate\Support\Facades\Queue;
|
||||||
|
use Livewire\Livewire;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class OnboardingTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
Cache::flush();
|
||||||
|
Queue::fake();
|
||||||
|
$this->fakeCommunities();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function fakeCommunities(): void
|
||||||
|
{
|
||||||
|
Http::fake(['*/api/v3/community/list*' => Http::response([
|
||||||
|
'communities' => [
|
||||||
|
['community' => ['id' => 8, 'name' => 'tech_community', 'title' => 'Tech Community']],
|
||||||
|
['community' => ['id' => 9, 'name' => 'other_community', 'title' => 'Other Community']],
|
||||||
|
],
|
||||||
|
])]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function instanceWithActiveAccount(): PlatformInstance
|
||||||
|
{
|
||||||
|
$instance = PlatformInstance::factory()->create(['url' => 'https://lemmy.world']);
|
||||||
|
PlatformAccount::factory()->create([
|
||||||
|
'instance_url' => 'https://lemmy.world',
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_selecting_an_instance_loads_its_communities(): void
|
||||||
|
{
|
||||||
|
$instance = $this->instanceWithActiveAccount();
|
||||||
|
|
||||||
|
Livewire::test(Onboarding::class)
|
||||||
|
->set('platformInstanceId', $instance->id)
|
||||||
|
->assertSet('communityLoadError', null)
|
||||||
|
->assertCount('availableCommunities', 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_changing_the_instance_clears_the_previous_selection(): void
|
||||||
|
{
|
||||||
|
$instance = $this->instanceWithActiveAccount();
|
||||||
|
$other = PlatformInstance::factory()->create(['url' => 'https://other.test']);
|
||||||
|
|
||||||
|
Livewire::test(Onboarding::class)
|
||||||
|
->set('platformInstanceId', $instance->id)
|
||||||
|
->set('channelCommunityId', 8)
|
||||||
|
->set('platformInstanceId', $other->id)
|
||||||
|
->assertSet('channelCommunityId', null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_unreachable_instance_surfaces_an_error_and_no_communities(): void
|
||||||
|
{
|
||||||
|
$instance = PlatformInstance::factory()->create(['url' => 'https://unreachable.test']);
|
||||||
|
PlatformAccount::factory()->create(['instance_url' => 'https://unreachable.test', 'is_active' => true]);
|
||||||
|
Cache::flush();
|
||||||
|
|
||||||
|
// Http::fake() merges stubs, so setUp's success stub would still win —
|
||||||
|
// swap the whole fake out instead.
|
||||||
|
app()->forgetInstance(Factory::class);
|
||||||
|
Http::swap(new Factory);
|
||||||
|
Http::fake(['*' => Http::response('nope', 500)]);
|
||||||
|
|
||||||
|
Livewire::test(Onboarding::class)
|
||||||
|
->set('platformInstanceId', $instance->id)
|
||||||
|
->assertSet('availableCommunities', [])
|
||||||
|
->assertNotSet('communityLoadError', null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_refresh_communities_refetches_from_the_instance(): void
|
||||||
|
{
|
||||||
|
$instance = $this->instanceWithActiveAccount();
|
||||||
|
|
||||||
|
Livewire::test(Onboarding::class)
|
||||||
|
->set('platformInstanceId', $instance->id)
|
||||||
|
->call('refreshCommunities')
|
||||||
|
->assertCount('availableCommunities', 2);
|
||||||
|
|
||||||
|
Http::assertSentCount(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_create_channel_requires_a_community(): void
|
||||||
|
{
|
||||||
|
$instance = $this->instanceWithActiveAccount();
|
||||||
|
$language = Language::factory()->create();
|
||||||
|
|
||||||
|
Livewire::test(Onboarding::class)
|
||||||
|
->set('platformInstanceId', $instance->id)
|
||||||
|
->set('channelLanguageId', $language->id)
|
||||||
|
->call('createChannel')
|
||||||
|
->assertHasErrors(['channelCommunityId' => 'required']);
|
||||||
|
|
||||||
|
$this->assertDatabaseCount('platform_channels', 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_create_channel_rejects_a_community_not_on_the_instance(): void
|
||||||
|
{
|
||||||
|
$instance = $this->instanceWithActiveAccount();
|
||||||
|
$language = Language::factory()->create();
|
||||||
|
|
||||||
|
Livewire::test(Onboarding::class)
|
||||||
|
->set('platformInstanceId', $instance->id)
|
||||||
|
->set('channelCommunityId', 4242)
|
||||||
|
->set('channelLanguageId', $language->id)
|
||||||
|
->call('createChannel')
|
||||||
|
->assertHasErrors(['channelCommunityId' => 'in']);
|
||||||
|
|
||||||
|
$this->assertDatabaseCount('platform_channels', 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_create_channel_stores_the_numeric_id_and_the_community_name(): void
|
||||||
|
{
|
||||||
|
$instance = $this->instanceWithActiveAccount();
|
||||||
|
$language = Language::factory()->create();
|
||||||
|
|
||||||
|
Livewire::test(Onboarding::class)
|
||||||
|
->set('platformInstanceId', $instance->id)
|
||||||
|
->set('channelCommunityId', 8)
|
||||||
|
->set('channelLanguageId', $language->id)
|
||||||
|
->set('channelDescription', 'A tech community')
|
||||||
|
->call('createChannel')
|
||||||
|
->assertHasNoErrors();
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('platform_channels', [
|
||||||
|
'platform_instance_id' => $instance->id,
|
||||||
|
'channel_id' => 8,
|
||||||
|
'name' => 'tech_community',
|
||||||
|
'description' => 'A tech community',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_create_channel_advances_the_wizard_and_syncs_existing_posts(): void
|
||||||
|
{
|
||||||
|
$instance = $this->instanceWithActiveAccount();
|
||||||
|
$language = Language::factory()->create();
|
||||||
|
|
||||||
|
Livewire::test(Onboarding::class)
|
||||||
|
->set('step', 3)
|
||||||
|
->set('platformInstanceId', $instance->id)
|
||||||
|
->set('channelCommunityId', 8)
|
||||||
|
->set('channelLanguageId', $language->id)
|
||||||
|
->call('createChannel')
|
||||||
|
->assertHasNoErrors()
|
||||||
|
->assertSet('step', 4);
|
||||||
|
|
||||||
|
Queue::assertPushed(SyncChannelPostsJob::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_create_channel_reports_when_the_instance_has_no_active_account(): void
|
||||||
|
{
|
||||||
|
$instance = PlatformInstance::factory()->create(['url' => 'https://lemmy.world']);
|
||||||
|
$language = Language::factory()->create();
|
||||||
|
|
||||||
|
Livewire::test(Onboarding::class)
|
||||||
|
->set('platformInstanceId', $instance->id)
|
||||||
|
->set('channelCommunityId', 8)
|
||||||
|
->set('channelLanguageId', $language->id)
|
||||||
|
->call('createChannel')
|
||||||
|
->assertHasNoErrors()
|
||||||
|
->assertSet('formErrors.general', 'No active platform accounts found for this instance. Please create a platform account first.');
|
||||||
|
|
||||||
|
$this->assertDatabaseCount('platform_channels', 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_mount_prefills_the_community_select_from_an_existing_channel(): void
|
||||||
|
{
|
||||||
|
$instance = $this->instanceWithActiveAccount();
|
||||||
|
PlatformChannel::factory()->create([
|
||||||
|
'platform_instance_id' => $instance->id,
|
||||||
|
'channel_id' => 8,
|
||||||
|
'name' => 'tech_community',
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Livewire::test(Onboarding::class)
|
||||||
|
->assertSet('platformInstanceId', $instance->id)
|
||||||
|
->assertSet('channelCommunityId', 8)
|
||||||
|
->assertCount('availableCommunities', 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
110
tests/Feature/StoreCommunityIdMigrationTest.php
Normal file
110
tests/Feature/StoreCommunityIdMigrationTest.php
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\PlatformAccount;
|
||||||
|
use App\Models\PlatformInstance;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class StoreCommunityIdMigrationTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
private function runMigration(): void
|
||||||
|
{
|
||||||
|
$migration = require database_path('migrations/2024_01_01_000015_store_numeric_community_id_on_platform_channels.php');
|
||||||
|
|
||||||
|
$migration->up();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function restoreSlugColumn(): void
|
||||||
|
{
|
||||||
|
Schema::table('platform_channels', function (Blueprint $table) {
|
||||||
|
$table->dropUnique('platform_channels_channel_id_unique');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('platform_channels', function (Blueprint $table) {
|
||||||
|
$table->string('channel_id')->change();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('platform_channels', function (Blueprint $table) {
|
||||||
|
$table->unique(['platform_instance_id', 'channel_id'], 'platform_channels_channel_id_unique');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function seedChannel(PlatformInstance $instance, string $slug): int
|
||||||
|
{
|
||||||
|
return DB::table('platform_channels')->insertGetId([
|
||||||
|
'platform_instance_id' => $instance->id,
|
||||||
|
'name' => $slug,
|
||||||
|
'display_name' => ucfirst($slug),
|
||||||
|
'channel_id' => $slug,
|
||||||
|
'is_active' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function instanceWithAccount(): PlatformInstance
|
||||||
|
{
|
||||||
|
$instance = PlatformInstance::factory()->create(['url' => 'https://lemmy.test']);
|
||||||
|
PlatformAccount::factory()->create(['instance_url' => 'https://lemmy.test', 'is_active' => true]);
|
||||||
|
|
||||||
|
return $instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_replaces_the_slug_with_the_resolved_community_id(): void
|
||||||
|
{
|
||||||
|
$this->restoreSlugColumn();
|
||||||
|
DB::table('platform_channels')->delete();
|
||||||
|
|
||||||
|
$instance = $this->instanceWithAccount();
|
||||||
|
$id = $this->seedChannel($instance, 'news');
|
||||||
|
|
||||||
|
Http::fake([
|
||||||
|
'*/api/v3/user/login*' => Http::response(['jwt' => 'token']),
|
||||||
|
'*/api/v3/community*' => Http::response(['community_view' => ['community' => ['id' => 8]]]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->runMigration();
|
||||||
|
|
||||||
|
$this->assertSame(8, (int) DB::table('platform_channels')->where('id', $id)->value('channel_id'));
|
||||||
|
$this->assertSame('news', DB::table('platform_channels')->where('id', $id)->value('name'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_aborts_when_a_community_cannot_be_resolved(): void
|
||||||
|
{
|
||||||
|
$this->restoreSlugColumn();
|
||||||
|
DB::table('platform_channels')->delete();
|
||||||
|
|
||||||
|
$instance = $this->instanceWithAccount();
|
||||||
|
$this->seedChannel($instance, 'gone');
|
||||||
|
|
||||||
|
Http::fake([
|
||||||
|
'*/api/v3/user/login*' => Http::response(['jwt' => 'token']),
|
||||||
|
'*/api/v3/community*' => Http::response('not found', 404),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->expectException(\Exception::class);
|
||||||
|
|
||||||
|
$this->runMigration();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_aborts_when_the_instance_has_no_active_account(): void
|
||||||
|
{
|
||||||
|
$this->restoreSlugColumn();
|
||||||
|
DB::table('platform_channels')->delete();
|
||||||
|
|
||||||
|
$instance = PlatformInstance::factory()->create(['url' => 'https://no-account.test']);
|
||||||
|
$this->seedChannel($instance, 'news');
|
||||||
|
|
||||||
|
$this->expectException(\RuntimeException::class);
|
||||||
|
|
||||||
|
$this->runMigration();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -31,11 +31,14 @@ public function test_creates_channel_and_attaches_account(): void
|
||||||
]);
|
]);
|
||||||
$language = Language::factory()->create();
|
$language = Language::factory()->create();
|
||||||
|
|
||||||
$channel = $this->action->execute('test_community', $instance->id, $language->id, 'A description');
|
$channel = $this->action->execute(
|
||||||
|
'test_community',
|
||||||
|
8,
|
||||||
|
$instance->id, $language->id, 'A description');
|
||||||
|
|
||||||
$this->assertInstanceOf(PlatformChannel::class, $channel);
|
$this->assertInstanceOf(PlatformChannel::class, $channel);
|
||||||
$this->assertEquals('test_community', $channel->name);
|
$this->assertEquals('test_community', $channel->name);
|
||||||
$this->assertEquals('test_community', $channel->channel_id);
|
$this->assertSame(8, $channel->channel_id);
|
||||||
$this->assertEquals('Test_community', $channel->display_name);
|
$this->assertEquals('Test_community', $channel->display_name);
|
||||||
$this->assertEquals($instance->id, $channel->platform_instance_id);
|
$this->assertEquals($instance->id, $channel->platform_instance_id);
|
||||||
$this->assertEquals($language->id, $channel->language_id);
|
$this->assertEquals($language->id, $channel->language_id);
|
||||||
|
|
@ -55,7 +58,10 @@ public function test_creates_channel_without_language(): void
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$channel = $this->action->execute('test_community', $instance->id);
|
$channel = $this->action->execute(
|
||||||
|
'test_community',
|
||||||
|
8,
|
||||||
|
$instance->id);
|
||||||
|
|
||||||
$this->assertNull($channel->language_id);
|
$this->assertNull($channel->language_id);
|
||||||
}
|
}
|
||||||
|
|
@ -72,7 +78,10 @@ public function test_fails_when_no_active_accounts(): void
|
||||||
$this->expectException(\RuntimeException::class);
|
$this->expectException(\RuntimeException::class);
|
||||||
$this->expectExceptionMessage('No active platform accounts found for this instance');
|
$this->expectExceptionMessage('No active platform accounts found for this instance');
|
||||||
|
|
||||||
$this->action->execute('test_community', $instance->id);
|
$this->action->execute(
|
||||||
|
'test_community',
|
||||||
|
8,
|
||||||
|
$instance->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_fails_when_no_accounts_at_all(): void
|
public function test_fails_when_no_accounts_at_all(): void
|
||||||
|
|
@ -81,6 +90,9 @@ public function test_fails_when_no_accounts_at_all(): void
|
||||||
|
|
||||||
$this->expectException(\RuntimeException::class);
|
$this->expectException(\RuntimeException::class);
|
||||||
|
|
||||||
$this->action->execute('test_community', $instance->id);
|
$this->action->execute(
|
||||||
|
'test_community',
|
||||||
|
8,
|
||||||
|
$instance->id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -133,19 +133,15 @@ public function test_handle_logs_start_message(): void
|
||||||
$this->assertTrue(true);
|
$this->assertTrue(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_sync_resolves_non_numeric_channel_id_via_get_community_id(): void
|
public function test_sync_passes_the_stored_community_id(): void
|
||||||
{
|
{
|
||||||
[$channel, $account] = $this->makeSyncableChannel('tech_news');
|
[$channel, $account] = $this->makeSyncableChannel(42);
|
||||||
|
|
||||||
$apiMock = Mockery::mock(LemmyApiService::class);
|
$apiMock = Mockery::mock(LemmyApiService::class);
|
||||||
$apiMock->shouldReceive('login')
|
$apiMock->shouldReceive('login')
|
||||||
->once()
|
->once()
|
||||||
->with($account->username, $account->password)
|
->with($account->username, $account->password)
|
||||||
->andReturn('token');
|
->andReturn('token');
|
||||||
$apiMock->shouldReceive('resolveCommunityId')
|
|
||||||
->once()
|
|
||||||
->with('tech_news', 'token')
|
|
||||||
->andReturn(42);
|
|
||||||
$apiMock->shouldReceive('syncChannelPosts')
|
$apiMock->shouldReceive('syncChannelPosts')
|
||||||
->once()
|
->once()
|
||||||
->with('token', Mockery::on(fn ($arg) => $arg->is($channel)), 42);
|
->with('token', Mockery::on(fn ($arg) => $arg->is($channel)), 42);
|
||||||
|
|
@ -161,22 +157,15 @@ public function test_sync_resolves_non_numeric_channel_id_via_get_community_id()
|
||||||
$this->addToAssertionCount(1);
|
$this->addToAssertionCount(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_sync_passes_resolved_community_id_to_sync_channel_posts(): void
|
public function test_sync_uses_channel_id_without_a_lookup(): void
|
||||||
{
|
{
|
||||||
[$channel, $account] = $this->makeSyncableChannel('42');
|
[$channel, $account] = $this->makeSyncableChannel(42);
|
||||||
|
|
||||||
$apiMock = Mockery::mock(LemmyApiService::class);
|
$apiMock = Mockery::mock(LemmyApiService::class);
|
||||||
$apiMock->shouldReceive('login')
|
$apiMock->shouldReceive('login')
|
||||||
->once()
|
->once()
|
||||||
->with($account->username, $account->password)
|
->with($account->username, $account->password)
|
||||||
->andReturn('token');
|
->andReturn('token');
|
||||||
// The slug-vs-numeric branch itself now lives in LemmyApiService::resolveCommunityId
|
|
||||||
// and is covered by LemmyApiServiceTest; here we only assert the job forwards
|
|
||||||
// whatever that resolution returns.
|
|
||||||
$apiMock->shouldReceive('resolveCommunityId')
|
|
||||||
->once()
|
|
||||||
->with('42', 'token')
|
|
||||||
->andReturn(42);
|
|
||||||
$apiMock->shouldReceive('syncChannelPosts')
|
$apiMock->shouldReceive('syncChannelPosts')
|
||||||
->once()
|
->once()
|
||||||
->with('token', Mockery::on(fn ($arg) => $arg->is($channel)), 42);
|
->with('token', Mockery::on(fn ($arg) => $arg->is($channel)), 42);
|
||||||
|
|
@ -195,7 +184,7 @@ public function test_sync_passes_resolved_community_id_to_sync_channel_posts():
|
||||||
/**
|
/**
|
||||||
* @return array{0: PlatformChannel, 1: PlatformAccount}
|
* @return array{0: PlatformChannel, 1: PlatformAccount}
|
||||||
*/
|
*/
|
||||||
private function makeSyncableChannel(string $channelId): array
|
private function makeSyncableChannel(int $channelId): array
|
||||||
{
|
{
|
||||||
$platformInstance = PlatformInstance::factory()->create([
|
$platformInstance = PlatformInstance::factory()->create([
|
||||||
'platform' => PlatformEnum::LEMMY,
|
'platform' => PlatformEnum::LEMMY,
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ public function test_channel_id_is_unique_per_platform_instance(): void
|
||||||
PlatformChannel::factory()->create([
|
PlatformChannel::factory()->create([
|
||||||
'platform_instance_id' => $instance->id,
|
'platform_instance_id' => $instance->id,
|
||||||
'name' => 'tech',
|
'name' => 'tech',
|
||||||
'channel_id' => 'tech',
|
'channel_id' => 7,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->expectException(UniqueConstraintViolationException::class);
|
$this->expectException(UniqueConstraintViolationException::class);
|
||||||
|
|
@ -60,7 +60,7 @@ public function test_channel_id_is_unique_per_platform_instance(): void
|
||||||
PlatformChannel::factory()->create([
|
PlatformChannel::factory()->create([
|
||||||
'platform_instance_id' => $instance->id,
|
'platform_instance_id' => $instance->id,
|
||||||
'name' => 'tech_alias',
|
'name' => 'tech_alias',
|
||||||
'channel_id' => 'tech',
|
'channel_id' => 7,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,15 +71,15 @@ public function test_same_channel_id_allowed_across_different_instances(): void
|
||||||
|
|
||||||
PlatformChannel::factory()->create([
|
PlatformChannel::factory()->create([
|
||||||
'platform_instance_id' => $instanceA->id,
|
'platform_instance_id' => $instanceA->id,
|
||||||
'channel_id' => 'tech',
|
'channel_id' => 7,
|
||||||
]);
|
]);
|
||||||
$second = PlatformChannel::factory()->create([
|
$second = PlatformChannel::factory()->create([
|
||||||
'platform_instance_id' => $instanceB->id,
|
'platform_instance_id' => $instanceB->id,
|
||||||
'channel_id' => 'tech',
|
'channel_id' => 7,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseCount('platform_channels', 2);
|
$this->assertDatabaseCount('platform_channels', 2);
|
||||||
$this->assertEquals('tech', $second->channel_id);
|
$this->assertSame(7, $second->channel_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_belongs_to_platform_instance_relationship(): void
|
public function test_belongs_to_platform_instance_relationship(): void
|
||||||
|
|
@ -259,7 +259,7 @@ public function test_channel_creation_with_factory(): void
|
||||||
$this->assertInstanceOf(PlatformChannel::class, $channel);
|
$this->assertInstanceOf(PlatformChannel::class, $channel);
|
||||||
$this->assertNotNull($channel->platform_instance_id);
|
$this->assertNotNull($channel->platform_instance_id);
|
||||||
$this->assertIsString($channel->name);
|
$this->assertIsString($channel->name);
|
||||||
$this->assertIsString($channel->channel_id);
|
$this->assertIsInt($channel->channel_id);
|
||||||
$this->assertIsBool($channel->is_active);
|
$this->assertIsBool($channel->is_active);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -272,7 +272,7 @@ public function test_channel_creation_with_explicit_values(): void
|
||||||
'platform_instance_id' => $instance->id,
|
'platform_instance_id' => $instance->id,
|
||||||
'name' => 'test_channel',
|
'name' => 'test_channel',
|
||||||
'display_name' => 'Test Channel',
|
'display_name' => 'Test Channel',
|
||||||
'channel_id' => 'channel_123',
|
'channel_id' => 123,
|
||||||
'description' => 'A test channel',
|
'description' => 'A test channel',
|
||||||
'language_id' => $language->id,
|
'language_id' => $language->id,
|
||||||
'is_active' => false,
|
'is_active' => false,
|
||||||
|
|
@ -281,7 +281,7 @@ public function test_channel_creation_with_explicit_values(): void
|
||||||
$this->assertEquals($instance->id, $channel->platform_instance_id);
|
$this->assertEquals($instance->id, $channel->platform_instance_id);
|
||||||
$this->assertEquals('test_channel', $channel->name);
|
$this->assertEquals('test_channel', $channel->name);
|
||||||
$this->assertEquals('Test Channel', $channel->display_name);
|
$this->assertEquals('Test Channel', $channel->display_name);
|
||||||
$this->assertEquals('channel_123', $channel->channel_id);
|
$this->assertSame(123, $channel->channel_id);
|
||||||
$this->assertEquals('A test channel', $channel->description);
|
$this->assertEquals('A test channel', $channel->description);
|
||||||
$this->assertEquals($language->id, $channel->language_id);
|
$this->assertEquals($language->id, $channel->language_id);
|
||||||
$this->assertFalse($channel->is_active);
|
$this->assertFalse($channel->is_active);
|
||||||
|
|
|
||||||
|
|
@ -170,35 +170,40 @@ public function test_get_community_id_success(): void
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_resolve_community_id_looks_up_non_numeric_channel_id(): void
|
public function test_list_communities_returns_local_communities(): void
|
||||||
{
|
{
|
||||||
Http::fake([
|
Http::fake(['*' => Http::response(['communities' => [
|
||||||
'*' => Http::response([
|
['community' => ['id' => 8, 'name' => 'news', 'title' => 'News']],
|
||||||
'community_view' => [
|
['community' => ['id' => 9, 'name' => 'memes', 'title' => 'Memes']],
|
||||||
'community' => ['id' => 123],
|
]], 200)]);
|
||||||
],
|
|
||||||
], 200),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$service = new LemmyApiService('lemmy.world');
|
$communities = (new LemmyApiService('lemmy.world'))->listCommunities();
|
||||||
$id = $service->resolveCommunityId('test-community', 'token');
|
|
||||||
|
|
||||||
$this->assertSame(123, $id);
|
$this->assertSame(
|
||||||
|
[['id' => 9, 'name' => 'memes', 'title' => 'Memes'], ['id' => 8, 'name' => 'news', 'title' => 'News']],
|
||||||
Http::assertSent(fn ($request) => str_contains($request->url(), 'name=test-community'));
|
$communities
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_resolve_community_id_uses_numeric_channel_id_without_lookup(): void
|
public function test_list_communities_omits_removed_and_deleted(): void
|
||||||
{
|
{
|
||||||
Http::fake();
|
Http::fake(['*' => Http::response(['communities' => [
|
||||||
|
['community' => ['id' => 8, 'name' => 'news', 'title' => 'News']],
|
||||||
|
['community' => ['id' => 9, 'name' => 'gone', 'title' => 'Gone', 'removed' => true]],
|
||||||
|
['community' => ['id' => 10, 'name' => 'bye', 'title' => 'Bye', 'deleted' => true]],
|
||||||
|
]], 200)]);
|
||||||
|
|
||||||
$service = new LemmyApiService('lemmy.world');
|
$this->assertSame([8], collect((new LemmyApiService('lemmy.world'))->listCommunities())->pluck('id')->all());
|
||||||
$id = $service->resolveCommunityId('42', 'token');
|
}
|
||||||
|
|
||||||
$this->assertSame(42, $id);
|
public function test_list_communities_throws_on_unsuccessful_response(): void
|
||||||
|
{
|
||||||
|
Http::fake(['*' => Http::response('nope', 500)]);
|
||||||
|
|
||||||
// A numeric channel_id is already the community id — no lookup should happen.
|
$this->expectException(Exception::class);
|
||||||
Http::assertNothingSent();
|
$this->expectExceptionMessage('Failed to list communities: 500');
|
||||||
|
|
||||||
|
(new LemmyApiService('lemmy.world'))->listCommunities();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_get_community_id_throws_on_unsuccessful_response(): void
|
public function test_get_community_id_throws_on_unsuccessful_response(): void
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ public function test_publish_to_channel_with_all_data(): void
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$channel = PlatformChannel::factory()->make([
|
$channel = PlatformChannel::factory()->make([
|
||||||
'channel_id' => '42',
|
'channel_id' => 42,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$extractedData = [
|
$extractedData = [
|
||||||
|
|
@ -76,10 +76,6 @@ public function test_publish_to_channel_with_all_data(): void
|
||||||
|
|
||||||
// Mock LemmyApiService
|
// Mock LemmyApiService
|
||||||
$apiMock = Mockery::mock(LemmyApiService::class);
|
$apiMock = Mockery::mock(LemmyApiService::class);
|
||||||
$apiMock->shouldReceive('resolveCommunityId')
|
|
||||||
->once()
|
|
||||||
->with('42', 'test-token')
|
|
||||||
->andReturn(42);
|
|
||||||
$apiMock->shouldReceive('createPost')
|
$apiMock->shouldReceive('createPost')
|
||||||
->once()
|
->once()
|
||||||
->with(
|
->with(
|
||||||
|
|
@ -117,7 +113,7 @@ public function test_publish_to_channel_with_minimal_data(): void
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$channel = PlatformChannel::factory()->make([
|
$channel = PlatformChannel::factory()->make([
|
||||||
'channel_id' => '24',
|
'channel_id' => 24,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$extractedData = [];
|
$extractedData = [];
|
||||||
|
|
@ -133,10 +129,6 @@ public function test_publish_to_channel_with_minimal_data(): void
|
||||||
|
|
||||||
// Mock LemmyApiService
|
// Mock LemmyApiService
|
||||||
$apiMock = Mockery::mock(LemmyApiService::class);
|
$apiMock = Mockery::mock(LemmyApiService::class);
|
||||||
$apiMock->shouldReceive('resolveCommunityId')
|
|
||||||
->once()
|
|
||||||
->with('24', 'minimal-token')
|
|
||||||
->andReturn(24);
|
|
||||||
$apiMock->shouldReceive('createPost')
|
$apiMock->shouldReceive('createPost')
|
||||||
->once()
|
->once()
|
||||||
->with(
|
->with(
|
||||||
|
|
@ -174,7 +166,7 @@ public function test_publish_to_channel_without_thumbnail(): void
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$channel = PlatformChannel::factory()->make([
|
$channel = PlatformChannel::factory()->make([
|
||||||
'channel_id' => '33',
|
'channel_id' => 33,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$extractedData = [
|
$extractedData = [
|
||||||
|
|
@ -193,10 +185,6 @@ public function test_publish_to_channel_without_thumbnail(): void
|
||||||
|
|
||||||
// Mock LemmyApiService
|
// Mock LemmyApiService
|
||||||
$apiMock = Mockery::mock(LemmyApiService::class);
|
$apiMock = Mockery::mock(LemmyApiService::class);
|
||||||
$apiMock->shouldReceive('resolveCommunityId')
|
|
||||||
->once()
|
|
||||||
->with('33', 'no-thumb-token')
|
|
||||||
->andReturn(33);
|
|
||||||
$apiMock->shouldReceive('createPost')
|
$apiMock->shouldReceive('createPost')
|
||||||
->once()
|
->once()
|
||||||
->with(
|
->with(
|
||||||
|
|
@ -260,7 +248,7 @@ public function test_publish_to_channel_throws_api_exception(): void
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$channel = PlatformChannel::factory()->make([
|
$channel = PlatformChannel::factory()->make([
|
||||||
'channel_id' => '42',
|
'channel_id' => 42,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$extractedData = [
|
$extractedData = [
|
||||||
|
|
@ -278,10 +266,6 @@ public function test_publish_to_channel_throws_api_exception(): void
|
||||||
|
|
||||||
// Mock LemmyApiService to throw exception
|
// Mock LemmyApiService to throw exception
|
||||||
$apiMock = Mockery::mock(LemmyApiService::class);
|
$apiMock = Mockery::mock(LemmyApiService::class);
|
||||||
$apiMock->shouldReceive('resolveCommunityId')
|
|
||||||
->once()
|
|
||||||
->with('42', 'test-token')
|
|
||||||
->andReturn(42);
|
|
||||||
$apiMock->shouldReceive('createPost')
|
$apiMock->shouldReceive('createPost')
|
||||||
->once()
|
->once()
|
||||||
->andThrow(new Exception('API Error'));
|
->andThrow(new Exception('API Error'));
|
||||||
|
|
@ -311,7 +295,7 @@ public function test_publish_to_channel_forwards_resolved_community_id_to_create
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$channel = PlatformChannel::factory()->make([
|
$channel = PlatformChannel::factory()->make([
|
||||||
'channel_id' => 'string-42',
|
'channel_id' => 42,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$extractedData = [
|
$extractedData = [
|
||||||
|
|
@ -327,10 +311,6 @@ public function test_publish_to_channel_forwards_resolved_community_id_to_create
|
||||||
|
|
||||||
// Mock LemmyApiService - should resolve non-numeric channel_id to a community id
|
// Mock LemmyApiService - should resolve non-numeric channel_id to a community id
|
||||||
$apiMock = Mockery::mock(LemmyApiService::class);
|
$apiMock = Mockery::mock(LemmyApiService::class);
|
||||||
$apiMock->shouldReceive('resolveCommunityId')
|
|
||||||
->once()
|
|
||||||
->with('string-42', 'token')
|
|
||||||
->andReturn(42);
|
|
||||||
$apiMock->shouldReceive('createPost')
|
$apiMock->shouldReceive('createPost')
|
||||||
->once()
|
->once()
|
||||||
->with(
|
->with(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue