Skip to content

Commit 3df38ae

Browse files
committed
[Demo] Add tests for FeedLoader
1 parent 3645393 commit 3df38ae

File tree

2 files changed

+334
-0
lines changed

2 files changed

+334
-0
lines changed

demo/tests/Blog/FeedLoaderTest.php

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace App\Tests\Blog;
13+
14+
use App\Blog\FeedLoader;
15+
use App\Blog\Post;
16+
use PHPUnit\Framework\Attributes\CoversClass;
17+
use PHPUnit\Framework\Attributes\UsesClass;
18+
use PHPUnit\Framework\TestCase;
19+
use Symfony\AI\Store\Document\TextDocument;
20+
use Symfony\AI\Store\Exception\InvalidArgumentException;
21+
use Symfony\Component\HttpClient\MockHttpClient;
22+
use Symfony\Component\HttpClient\Response\MockResponse;
23+
use Symfony\Component\Uid\Uuid;
24+
25+
#[CoversClass(FeedLoader::class)]
26+
#[UsesClass(Post::class)]
27+
final class FeedLoaderTest extends TestCase
28+
{
29+
public function testLoadWithValidFeedUrl()
30+
{
31+
$loader = new FeedLoader(new MockHttpClient(MockResponse::fromFile(__DIR__.'/fixtures/symfony-feed.xml')));
32+
$documents = iterator_to_array($loader->load('https://feeds.feedburner.com/symfony/blog'));
33+
34+
$this->assertCount(2, $documents);
35+
36+
// Test first document
37+
$firstDocument = $documents[0];
38+
$this->assertInstanceOf(TextDocument::class, $firstDocument);
39+
40+
$expectedFirstUuid = Uuid::v5(Uuid::fromString('6ba7b810-9dad-11d1-80b4-00c04fd430c8'), 'Save the date, SymfonyDay Montreal 2026!');
41+
$this->assertEquals($expectedFirstUuid, $firstDocument->id);
42+
43+
$this->assertStringContainsString('Title: Save the date, SymfonyDay Montreal 2026!', $firstDocument->text);
44+
$this->assertStringContainsString('From: Paola Suárez on 2025-09-11', $firstDocument->text);
45+
$this->assertStringContainsString("We're thrilled to announce that SymfonyDay Montreal is happening on June 4, 2026!", $firstDocument->text);
46+
$this->assertStringContainsString('Mark your calendars, tell your friends', $firstDocument->text);
47+
48+
$firstMetadata = $firstDocument->metadata->toArray();
49+
$this->assertSame($expectedFirstUuid->toRfc4122(), $firstMetadata['id']);
50+
$this->assertSame('Save the date, SymfonyDay Montreal 2026!', $firstMetadata['title']);
51+
$this->assertSame('https://symfony.com/blog/save-the-date-symfonyday-montreal-2026?utm_source=Symfony%20Blog%20Feed&utm_medium=feed', $firstMetadata['link']);
52+
$this->assertStringContainsString("We're thrilled to announce that SymfonyDay Montreal is happening on June 4, 2026!", $firstMetadata['description']);
53+
$this->assertStringContainsString('Mark your calendars, tell your friends', $firstMetadata['content']);
54+
$this->assertSame('Paola Suárez', $firstMetadata['author']);
55+
$this->assertSame('2025-09-11', $firstMetadata['date']);
56+
57+
// Test second document
58+
$secondDocument = $documents[1];
59+
$this->assertInstanceOf(TextDocument::class, $secondDocument);
60+
61+
$expectedSecondUuid = Uuid::v5(Uuid::fromString('6ba7b810-9dad-11d1-80b4-00c04fd430c8'), 'SymfonyCon Amsterdam 2025: Call for IT student volunteers: Volunteer, Learn & Connect!');
62+
$this->assertEquals($expectedSecondUuid, $secondDocument->id);
63+
64+
$this->assertStringContainsString('Title: SymfonyCon Amsterdam 2025: Call for IT student volunteers: Volunteer, Learn & Connect!', $secondDocument->text);
65+
$this->assertStringContainsString('From: Paola Suárez on 2025-09-10', $secondDocument->text);
66+
$this->assertStringContainsString('🎓SymfonyCon Amsterdam 2025: Call for IT Student Volunteers!', $secondDocument->text);
67+
68+
$secondMetadata = $secondDocument->metadata->toArray();
69+
$this->assertSame($expectedSecondUuid->toRfc4122(), $secondMetadata['id']);
70+
$this->assertSame('SymfonyCon Amsterdam 2025: Call for IT student volunteers: Volunteer, Learn & Connect!', $secondMetadata['title']);
71+
$this->assertSame('https://symfony.com/blog/symfonycon-amsterdam-2025-call-for-it-student-volunteers-volunteer-learn-and-connect?utm_source=Symfony%20Blog%20Feed&utm_medium=feed', $secondMetadata['link']);
72+
$this->assertStringContainsString('🎓SymfonyCon Amsterdam 2025: Call for IT Student Volunteers!', $secondMetadata['description']);
73+
$this->assertStringContainsString('🎓SymfonyCon Amsterdam 2025: Call for IT Student Volunteers!', $secondMetadata['content']);
74+
$this->assertSame('Paola Suárez', $secondMetadata['author']);
75+
$this->assertSame('2025-09-10', $secondMetadata['date']);
76+
}
77+
78+
public function testLoadWithNullSource()
79+
{
80+
$loader = new FeedLoader(new MockHttpClient([]));
81+
82+
$this->expectException(InvalidArgumentException::class);
83+
$this->expectExceptionMessage('FeedLoader requires a RSS feed URL as source, null given.');
84+
85+
iterator_to_array($loader->load(null));
86+
}
87+
88+
public function testLoadWithEmptyFeed()
89+
{
90+
$emptyFeedXml = <<<XML
91+
<?xml version="1.0" encoding="UTF-8" ?>
92+
<rss version="2.0">
93+
<channel>
94+
<title>Empty Feed</title>
95+
<link>https://example.com/</link>
96+
<description>An empty RSS feed</description>
97+
</channel>
98+
</rss>
99+
XML;
100+
101+
$loader = new FeedLoader(new MockHttpClient(new MockResponse($emptyFeedXml)));
102+
$documents = iterator_to_array($loader->load('https://example.com/feed.xml'));
103+
104+
$this->assertCount(0, $documents);
105+
}
106+
107+
public function testLoadWithHttpError()
108+
{
109+
$loader = new FeedLoader(new MockHttpClient(new MockResponse('', ['http_code' => 404])));
110+
111+
$this->expectException(\Symfony\Contracts\HttpClient\Exception\ClientException::class);
112+
113+
iterator_to_array($loader->load('https://example.com/non-existent-feed.xml'));
114+
}
115+
116+
public function testLoadWithMalformedXml()
117+
{
118+
$malformedXml = '<?xml version="1.0" encoding="UTF-8" ?><rss><channel><title>Test</title>';
119+
120+
$loader = new FeedLoader(new MockHttpClient(new MockResponse($malformedXml)));
121+
122+
$this->expectException(\Exception::class);
123+
124+
iterator_to_array($loader->load('https://example.com/malformed-feed.xml'));
125+
}
126+
127+
public function testLoadReturnsIterableOfTextDocuments()
128+
{
129+
$loader = new FeedLoader(new MockHttpClient(MockResponse::fromFile(__DIR__.'/fixtures/symfony-feed.xml')));
130+
$result = $loader->load('https://feeds.feedburner.com/symfony/blog');
131+
132+
$this->assertIsIterable($result);
133+
134+
foreach ($result as $document) {
135+
$this->assertInstanceOf(TextDocument::class, $document);
136+
$this->assertInstanceOf(Uuid::class, $document->id);
137+
$this->assertIsString($document->text);
138+
$this->assertNotEmpty($document->text);
139+
$this->assertIsArray($document->metadata->toArray());
140+
}
141+
}
142+
143+
public function testLoadGeneratesConsistentUuids()
144+
{
145+
$loader = new FeedLoader(new MockHttpClient(MockResponse::fromFile(__DIR__.'/fixtures/symfony-feed.xml')));
146+
$documents1 = iterator_to_array($loader->load('https://feeds.feedburner.com/symfony/blog'));
147+
148+
// Load same feed again
149+
$loader2 = new FeedLoader(new MockHttpClient(MockResponse::fromFile(__DIR__.'/fixtures/symfony-feed.xml')));
150+
$documents2 = iterator_to_array($loader2->load('https://feeds.feedburner.com/symfony/blog'));
151+
152+
$this->assertCount(2, $documents1);
153+
$this->assertCount(2, $documents2);
154+
155+
// UUIDs should be identical for same content
156+
$this->assertEquals($documents1[0]->id, $documents2[0]->id);
157+
$this->assertEquals($documents1[1]->id, $documents2[1]->id);
158+
}
159+
}
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
<?xml version="1.0" encoding="UTF-8" ?>
2+
<rss version="2.0"
3+
xmlns:content="http://purl.org/rss/1.0/modules/content/"
4+
xmlns:dc="http://purl.org/dc/elements/1.1/"
5+
xmlns:atom="http://www.w3.org/2005/Atom"
6+
>
7+
<channel>
8+
<title>Symfony Blog</title>
9+
<atom:link href="https://feeds.feedburner.com/symfony/blog" rel="self" type="application/rss+xml" />
10+
<link>https://symfony.com/blog/</link>
11+
<description>Most recent posts published on the Symfony project blog</description>
12+
<pubDate>Fri, 12 Sep 2025 14:25:38 +0200</pubDate>
13+
<lastBuildDate>Thu, 11 Sep 2025 14:30:00 +0200</lastBuildDate>
14+
<language>en</language>
15+
<item>
16+
<title><![CDATA[Save the date, SymfonyDay Montreal 2026!]]></title>
17+
<link>https://symfony.com/blog/save-the-date-symfonyday-montreal-2026?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed</link>
18+
<description>
19+
20+
21+
22+
We're thrilled to announce that SymfonyDay Montreal is happening on June 4, 2026! 🎉
23+
24+
Mark your calendars, tell your friends, and get ready for a day full of inspiring talks, networking opportunities, and the vibrant energy of the Symfony community…</description>
25+
<content:encoded><![CDATA[
26+
<p><a class="block text-center" href="https://live.symfony.com/2026-montreal" title="Blog 1200X440Px">
27+
<img src="https://symfony.com/uploads/assets/blog/BLOG-1200x440px.png" alt="Blog 1200X440Px">
28+
</a></p>
29+
30+
<p>We're thrilled to announce that SymfonyDay Montreal is happening on <strong>June 4, 2026! 🎉</strong></p>
31+
32+
<p>Mark your calendars, tell your friends, and get ready for a day full of inspiring talks, networking opportunities, and the vibrant energy of the Symfony community in Canada! 🍁</p>
33+
34+
<p>🎟️ Register now to secure your seat, <a href="https://live.symfony.com/2026-montreal/registration/"><strong>here!</strong></a></p>
35+
36+
<hr />
37+
38+
<p>📝<strong>Call for papers is open!</strong></p>
39+
40+
<p><strong><a href="https://live.symfony.com/2026-montreal/cfp">Submit your talk</a></strong> for SymfonyDay Montreal 2026! Every selected speaker will receive a complimentary conference ticket and a speaker gift! Speakers who do not live in the conference city will also have their travel and accommodation expenses covered.</p>
41+
42+
<p>If you have never spoken at a conference before, you can take advantage of our <strong><a href="https://symfony.com/doc/current/contributing/community/speaker-mentoring.html">speaker mentoring program!</a></strong> Experienced speakers will gladly assist you in preparing for your conference, whether it involves creating your slides or rehearsing your talk. Feel free to seek advice in the #diversity or #speaker-mentoring channels on the <strong><a href="https://symfony.com/slack">Symfony Devs Slack</a></strong> or include comments requesting guidance when submitting your talk proposal.</p>
43+
44+
<hr />
45+
46+
<p>🤝 <strong>Want to support the event?</strong></p>
47+
48+
<p>Become a sponsor and showcase your brand to an engaged audience of developers.</p>
49+
50+
<p>⚡Contact Hadrien Cren by <a href="&#x6d;&#x61;&#x69;&#108;&#116;&#111;&#58;e&#x76;&#x65;&#x6e;&#x74;&#115;&#64;&#115;y&#x6d;&#x66;&#x6f;&#x6e;&#121;&#46;&#99;&#111;&#x6d;">email</a> to learn more about the options.</p>
51+
52+
<p><strong>We can't wait to see you there!</strong></p>
53+
54+
<p><a class="block text-center" href="https://linktr.ee/symfony">
55+
<img src="https://symfony.com/uploads/assets/blog/Banner-BLOG.png" alt="Banner Blog">
56+
</a></p>
57+
58+
<hr style="margin-bottom: 5px" />
59+
<div style="font-size: 90%">
60+
<a href="https://symfony.com/sponsor">Sponsor</a> the Symfony project.
61+
</div>
62+
]]></content:encoded>
63+
<guid isPermaLink="false">https://symfony.com/blog/save-the-date-symfonyday-montreal-2026?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed</guid>
64+
<dc:creator><![CDATA[ Paola Suárez ]]></dc:creator>
65+
<pubDate>Thu, 11 Sep 2025 14:30:00 +0200</pubDate>
66+
<comments>https://symfony.com/blog/save-the-date-symfonyday-montreal-2026?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed#comments-list</comments>
67+
</item>
68+
<item>
69+
<title><![CDATA[SymfonyCon Amsterdam 2025: Call for IT student volunteers: Volunteer, Learn & Connect!]]></title>
70+
<link>https://symfony.com/blog/symfonycon-amsterdam-2025-call-for-it-student-volunteers-volunteer-learn-and-connect?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed</link>
71+
<description>
72+
73+
74+
75+
🎓SymfonyCon Amsterdam 2025: Call for IT Student Volunteers!
76+
77+
Are you an IT or computer science student looking to gain hands-on experience, grow your network, and take part in an international tech event? 🎤✨
78+
Symfony is looking for motivated student…</description>
79+
<content:encoded><![CDATA[
80+
<p><a class="block text-center" href="https://live.symfony.com/2025-amsterdam-con/">
81+
<img src="https://symfony.com/uploads/assets/blog/NL-BLOG-Banner.png" alt="Nl Blog Banner">
82+
</a></p>
83+
84+
<p>🎓<strong>SymfonyCon Amsterdam 2025: Call for IT Student Volunteers!</strong></p>
85+
86+
<p>Are you an IT or computer science student looking to gain hands-on experience, grow your network, and take part in an international tech event? 🎤✨
87+
Symfony is looking for motivated student volunteers to join the adventure at SymfonyCon Amsterdam 2025!</p>
88+
89+
<p>📍<strong>Where:</strong> Amsterdam, The Netherlands</p>
90+
91+
<p>🗓️<strong>When:</strong> November 28–29, 2025</p>
92+
93+
<p>Volunteering with us means helping out for one day and getting a <strong>free ticket</strong> to attend the conference the other day, so you don't miss out on the action!</p>
94+
95+
<hr />
96+
97+
<p>💼 <strong>Volunteer Tasks</strong></p>
98+
99+
<p>As a volunteer, you'll be assigned to:</p>
100+
101+
<p>✔️Welcome attendees</p>
102+
103+
<p>✔️Provide directions and information</p>
104+
105+
<p>✔️Help ensure everything runs smoothly!</p>
106+
107+
<hr />
108+
109+
<p>✅ <strong>Requirements</strong></p>
110+
111+
<p>To apply, you must:</p>
112+
113+
<p>✔️Be a student in computer science / IT (all backgrounds welcome!)</p>
114+
115+
<p>✔️Be curious about careers in development or tech</p>
116+
117+
<p>✔️Be available for at least one full day during the event</p>
118+
119+
<p>✔️Speak English (conference language)</p>
120+
121+
<hr />
122+
123+
<p>🎁 <strong>What You Get</strong></p>
124+
125+
<p>Free access to the full 2-day SymfonyCon Amsterdam 2025 conference</p>
126+
127+
<p>✔️Opportunities to network with developers, speakers, and community leaders</p>
128+
129+
<p>✔️Delicious lunches included</p>
130+
131+
<p>✔️An unforgettable experience in an inclusive and friendly environment!</p>
132+
133+
<hr />
134+
135+
<p>🚫 <strong>Please note:</strong></p>
136+
137+
<p>We are unable to cover:</p>
138+
139+
<p>✔️Travel or accommodation costs ✔️Dinners or evening expenses</p>
140+
141+
<hr />
142+
143+
<p>💌 <strong>How to Apply:</strong></p>
144+
145+
<p>Interested? Send us an email at <em>[email protected]</em> with:</p>
146+
147+
<p>✔️Your name, school, and area of study</p>
148+
149+
<p>✔️The reason you'd like to volunteer</p>
150+
151+
<p>✔️Confirmation of your availability on November 28 or 29, 2025</p>
152+
153+
<hr />
154+
155+
<p><strong>Be part of the Symfony community, gain valuable experience, and help make this event amazing!
156+
We can't wait to meet you! 🧑‍💻💬 #SymfonyCon</strong></p>
157+
158+
<hr />
159+
160+
<p><a class="block text-center" href="https://linktr.ee/symfony">
161+
<img src="https://symfony.com/uploads/assets/blog/Banner-BLOG.png" alt="Banner Blog">
162+
</a></p>
163+
164+
<hr style="margin-bottom: 5px" />
165+
<div style="font-size: 90%">
166+
<a href="https://symfony.com/sponsor">Sponsor</a> the Symfony project.
167+
</div>
168+
]]></content:encoded>
169+
<guid isPermaLink="false">https://symfony.com/blog/symfonycon-amsterdam-2025-call-for-it-student-volunteers-volunteer-learn-and-connect?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed</guid>
170+
<dc:creator><![CDATA[ Paola Suárez ]]></dc:creator>
171+
<pubDate>Wed, 10 Sep 2025 09:00:00 +0200</pubDate>
172+
<comments>https://symfony.com/blog/symfonycon-amsterdam-2025-call-for-it-student-volunteers-volunteer-learn-and-connect?utm_source=Symfony%20Blog%20Feed&amp;utm_medium=feed#comments-list</comments>
173+
</item>
174+
</channel>
175+
</rss>

0 commit comments

Comments
 (0)