diff --git a/app/Models/Page.php b/app/Models/Page.php new file mode 100644 index 0000000..210de9d --- /dev/null +++ b/app/Models/Page.php @@ -0,0 +1,51 @@ + */ + use HasFactory; + + protected $fillable = [ + 'url', + 'status', + 'title', + 'instance_id', + 'posted_at', + 'fetched_at', + 'failed_at', + ]; + + protected $casts = [ + 'status' => PageStatusEnum::class, + 'posted_at' => 'datetime', + 'fetched_at' => 'datetime', + 'failed_at' => 'datetime', + ]; + + public function instance(): BelongsTo + { + return $this->belongsTo(Instance::class); + } + + public function outgoingLinks(): HasMany + { + return $this->hasMany(PageLink::class, 'source_page_id'); + } + + public function incomingLinks(): HasMany + { + return $this->hasMany(PageLink::class, 'target_page_id'); + } +} diff --git a/app/Models/PageLink.php b/app/Models/PageLink.php new file mode 100644 index 0000000..a8e67f8 --- /dev/null +++ b/app/Models/PageLink.php @@ -0,0 +1,31 @@ + */ + use HasFactory; + + protected $fillable = [ + 'source_page_id', + 'target_page_id', + ]; + + public function sourcePage(): BelongsTo + { + return $this->belongsTo(Page::class, 'source_page_id'); + } + + public function targetPage(): BelongsTo + { + return $this->belongsTo(Page::class, 'target_page_id'); + } +} diff --git a/database/factories/PageFactory.php b/database/factories/PageFactory.php new file mode 100644 index 0000000..55f62ca --- /dev/null +++ b/database/factories/PageFactory.php @@ -0,0 +1,26 @@ + + */ +class PageFactory extends Factory +{ + /** + * @return array + */ + public function definition(): array + { + return [ + 'url' => fake()->url(), + 'status' => PageStatusEnum::Discovered, + ]; + } +} diff --git a/database/factories/PageLinkFactory.php b/database/factories/PageLinkFactory.php new file mode 100644 index 0000000..57a2b6f --- /dev/null +++ b/database/factories/PageLinkFactory.php @@ -0,0 +1,34 @@ + + */ +class PageLinkFactory extends Factory +{ + public function definition(): array + { + return []; + } + + public function withSource(Page $page): static + { + return $this->state(fn () => [ + 'source_page_id' => $page->id, + ]); + } + + public function withTarget(Page $page): static + { + return $this->state(fn () => [ + 'target_page_id' => $page->id, + ]); + } +} diff --git a/tests/Unit/Models/PageLinkTest.php b/tests/Unit/Models/PageLinkTest.php new file mode 100644 index 0000000..f7ffba2 --- /dev/null +++ b/tests/Unit/Models/PageLinkTest.php @@ -0,0 +1,52 @@ +create(['url' => 'https://source.example.com/post/1']); + $target = Page::factory()->create(['url' => 'https://target.example.com/page/2']); + + $link = PageLink::create([ + 'source_page_id' => $source->id, + 'target_page_id' => $target->id, + ]); + + $fresh = $link->fresh(); + + $this->assertNotNull($fresh); + $this->assertSame($source->id, $fresh->source_page_id); + $this->assertSame($target->id, $fresh->target_page_id); + + $this->assertInstanceOf(Page::class, $fresh->sourcePage); + $this->assertSame($source->id, $fresh->sourcePage->id); + + $this->assertInstanceOf(Page::class, $fresh->targetPage); + $this->assertSame($target->id, $fresh->targetPage->id); + } + + public function test_page_link_factory_with_source_and_target_methods_create_a_link(): void + { + $source = Page::factory()->create(['url' => 'https://source.example.com/post/1']); + $target = Page::factory()->create(['url' => 'https://target.example.com/page/2']); + + $link = PageLink::factory() + ->withSource($source) + ->withTarget($target) + ->create(); + + $this->assertSame($source->id, $link->source_page_id); + $this->assertSame($target->id, $link->target_page_id); + } +} diff --git a/tests/Unit/Models/PageTest.php b/tests/Unit/Models/PageTest.php new file mode 100644 index 0000000..02d6f54 --- /dev/null +++ b/tests/Unit/Models/PageTest.php @@ -0,0 +1,99 @@ + 'https://example.com/article', + 'status' => 'discovered', + 'title' => 'An Example Article', + 'instance_id' => null, + 'posted_at' => null, + 'fetched_at' => null, + ]); + + $fresh = $page->fresh(); + + $this->assertNotNull($fresh); + $this->assertSame('https://example.com/article', $fresh->url); + $this->assertSame('An Example Article', $fresh->title); + $this->assertNull($fresh->instance_id); + } + + public function test_page_instance_relationship_returns_the_owning_instance(): void + { + $instance = Instance::factory() + ->type(InstanceType::Mastodon) + ->enabled() + ->create(); + + $page = Page::create([ + 'url' => 'https://example.com/post/1', + 'status' => 'discovered', + 'instance_id' => $instance->id, + ]); + + $fresh = $page->fresh(); + + $this->assertInstanceOf(Instance::class, $fresh->instance); + $this->assertSame($instance->id, $fresh->instance->id); + } + + public function test_page_outgoing_and_incoming_links_relationships(): void + { + $source = Page::factory()->create(['url' => 'https://example.com/source']); + $target = Page::factory()->create(['url' => 'https://example.com/target']); + + PageLink::create([ + 'source_page_id' => $source->id, + 'target_page_id' => $target->id, + ]); + + $freshSource = $source->fresh(); + $freshTarget = $target->fresh(); + + $this->assertCount(1, $freshSource->outgoingLinks); + $this->assertCount(0, $freshSource->incomingLinks); + $this->assertCount(1, $freshTarget->incomingLinks); + $this->assertCount(0, $freshTarget->outgoingLinks); + + $this->assertSame($source->id, $freshTarget->incomingLinks->first()->source_page_id); + $this->assertSame($target->id, $freshSource->outgoingLinks->first()->target_page_id); + } + + public function test_page_status_is_cast_to_enum(): void + { + $cases = [ + ['string' => 'discovered', 'enum' => PageStatusEnum::Discovered], + ['string' => 'fetched', 'enum' => PageStatusEnum::Fetched], + ['string' => 'failed', 'enum' => PageStatusEnum::Failed], + ]; + + foreach ($cases as ['string' => $raw, 'enum' => $expected]) { + $page = Page::create([ + 'url' => 'https://example.com/' . $raw, + 'status' => $raw, + ]); + + $fresh = $page->fresh(); + + $this->assertInstanceOf(PageStatusEnum::class, $fresh->status, "status '{$raw}' should cast to PageStatusEnum"); + $this->assertSame($expected, $fresh->status, "status '{$raw}' should equal PageStatusEnum::{$expected->name}"); + } + } +}