1+ import { describe , it , expect , beforeEach } from "vitest" ;
2+ import { readFileSync } from "fs" ;
3+ import { fileURLToPath } from "url" ;
4+ import { dirname , join } from "path" ;
5+ import { feedLoader } from "../src/feed-loader.js" ;
6+ import { ItemSchema } from "../src/schema.js" ;
7+ import { server , http , HttpResponse } from "./setup.js" ;
8+
9+ const __filename = fileURLToPath ( import . meta. url ) ;
10+ const __dirname = dirname ( __filename ) ;
11+
12+ const mockStore = {
13+ data : new Map ( ) ,
14+ clear ( ) {
15+ this . data . clear ( ) ;
16+ } ,
17+ set ( { id, data, rendered } : { id : string ; data : any ; rendered : any } ) {
18+ this . data . set ( id , { data, rendered } ) ;
19+ } ,
20+ get ( id : string ) {
21+ return this . data . get ( id ) ;
22+ } ,
23+ has ( id : string ) {
24+ return this . data . has ( id ) ;
25+ } ,
26+ keys ( ) {
27+ return this . data . keys ( ) ;
28+ } ,
29+ values ( ) {
30+ return Array . from ( this . data . values ( ) ) ;
31+ }
32+ } ;
33+
34+ const mockMeta = {
35+ data : new Map ( ) ,
36+ get ( key : string ) {
37+ return this . data . get ( key ) ;
38+ } ,
39+ set ( key : string , value : any ) {
40+ this . data . set ( key , value ) ;
41+ } ,
42+ has ( key : string ) {
43+ return this . data . has ( key ) ;
44+ } ,
45+ delete ( key : string ) {
46+ return this . data . delete ( key ) ;
47+ }
48+ } ;
49+
50+ const mockLogger = {
51+ info : ( ) => { } ,
52+ warn : ( ) => { } ,
53+ error : ( ) => { }
54+ } ;
55+
56+ const mockParseData = async ( { data } : { id : string ; data : any } ) => {
57+ const result = ItemSchema . parse ( data ) ;
58+ return result ;
59+ } ;
60+
61+ describe ( "Astro Loader Interface Compliance" , ( ) => {
62+ beforeEach ( ( ) => {
63+ mockStore . clear ( ) ;
64+ mockMeta . data . clear ( ) ;
65+ } ) ;
66+
67+ describe ( "Loader Interface" , ( ) => {
68+ it ( "should implement the Loader interface correctly" , ( ) => {
69+ const loader = feedLoader ( { url : "https://example.com/feed.xml" } ) ;
70+
71+ expect ( loader ) . toHaveProperty ( "name" ) ;
72+ expect ( loader ) . toHaveProperty ( "load" ) ;
73+ expect ( loader ) . toHaveProperty ( "schema" ) ;
74+
75+ expect ( typeof loader . name ) . toBe ( "string" ) ;
76+ expect ( typeof loader . load ) . toBe ( "function" ) ;
77+ expect ( loader . schema ) . toBeDefined ( ) ;
78+
79+ expect ( loader . name ) . toBe ( "feed-loader" ) ;
80+ } ) ;
81+
82+ it ( "should have correct schema export" , ( ) => {
83+ const loader = feedLoader ( { url : "https://example.com/feed.xml" } ) ;
84+
85+ expect ( loader . schema ) . toBe ( ItemSchema ) ;
86+ } ) ;
87+
88+ it ( "should accept URL as string or URL object" , ( ) => {
89+ const stringLoader = feedLoader ( { url : "https://example.com/feed.xml" } ) ;
90+ const urlLoader = feedLoader ( { url : new URL ( "https://example.com/feed.xml" ) } ) ;
91+
92+ expect ( stringLoader . name ) . toBe ( "feed-loader" ) ;
93+ expect ( urlLoader . name ) . toBe ( "feed-loader" ) ;
94+ } ) ;
95+ } ) ;
96+
97+ describe ( "Data Store Integration" , ( ) => {
98+ it ( "should clear store before loading new data" , async ( ) => {
99+ const rssContent = readFileSync ( join ( __dirname , "fixtures/rss2.xml" ) , "utf-8" ) ;
100+
101+ server . use (
102+ http . get ( "https://example.com/feed.xml" , ( ) => {
103+ return new HttpResponse ( rssContent , {
104+ status : 200 ,
105+ headers : {
106+ "content-type" : "application/rss+xml"
107+ }
108+ } ) ;
109+ } )
110+ ) ;
111+
112+ mockStore . set ( {
113+ id : "old-item" ,
114+ data : { title : "Old Item" } ,
115+ rendered : { html : "Old content" }
116+ } ) ;
117+
118+ expect ( mockStore . data . size ) . toBe ( 1 ) ;
119+
120+ const loader = feedLoader ( { url : "https://example.com/feed.xml" } ) ;
121+ await loader . load ( {
122+ store : mockStore as any ,
123+ logger : mockLogger as any ,
124+ parseData : mockParseData as any ,
125+ meta : mockMeta
126+ } ) ;
127+
128+ expect ( mockStore . data . size ) . toBe ( 3 ) ;
129+ expect ( mockStore . has ( "old-item" ) ) . toBe ( false ) ;
130+ } ) ;
131+
132+ it ( "should store items with correct structure" , async ( ) => {
133+ const rssContent = readFileSync ( join ( __dirname , "fixtures/rss2.xml" ) , "utf-8" ) ;
134+
135+ server . use (
136+ http . get ( "https://example.com/feed.xml" , ( ) => {
137+ return new HttpResponse ( rssContent , {
138+ status : 200 ,
139+ headers : {
140+ "content-type" : "application/rss+xml"
141+ }
142+ } ) ;
143+ } )
144+ ) ;
145+
146+ const loader = feedLoader ( { url : "https://example.com/feed.xml" } ) ;
147+ await loader . load ( {
148+ store : mockStore as any ,
149+ logger : mockLogger as any ,
150+ parseData : mockParseData as any ,
151+ meta : mockMeta
152+ } ) ;
153+
154+ const storedItem = mockStore . get ( "https://example.com/first-post" ) ;
155+
156+ expect ( storedItem ) . toHaveProperty ( "data" ) ;
157+ expect ( storedItem ) . toHaveProperty ( "rendered" ) ;
158+ expect ( storedItem . rendered ) . toHaveProperty ( "html" ) ;
159+
160+ expect ( storedItem . data . title ) . toBe ( "First Post" ) ;
161+ expect ( storedItem . data . link ) . toBe ( "https://example.com/first-post" ) ;
162+ expect ( storedItem . data . guid ) . toBe ( "https://example.com/first-post" ) ;
163+ expect ( storedItem . rendered . html ) . toBe ( "This is the first post in our RSS feed" ) ;
164+ } ) ;
165+
166+ it ( "should handle empty description gracefully" , async ( ) => {
167+ const feedContent = `<?xml version="1.0" encoding="UTF-8"?>
168+ <rss version="2.0">
169+ <channel>
170+ <title>Test Feed</title>
171+ <item>
172+ <title>Item without description</title>
173+ <link>https://example.com/no-desc</link>
174+ <guid>https://example.com/no-desc</guid>
175+ </item>
176+ </channel>
177+ </rss>` ;
178+
179+ server . use (
180+ http . get ( "https://example.com/no-desc.xml" , ( ) => {
181+ return new HttpResponse ( feedContent , {
182+ status : 200 ,
183+ headers : {
184+ "content-type" : "application/rss+xml"
185+ }
186+ } ) ;
187+ } )
188+ ) ;
189+
190+ const loader = feedLoader ( { url : "https://example.com/no-desc.xml" } ) ;
191+ await loader . load ( {
192+ store : mockStore as any ,
193+ logger : mockLogger as any ,
194+ parseData : mockParseData as any ,
195+ meta : mockMeta
196+ } ) ;
197+
198+ const storedItem = mockStore . get ( "https://example.com/no-desc" ) ;
199+ expect ( storedItem ) . toBeDefined ( ) ;
200+ expect ( storedItem . rendered . html ) . toBe ( "" ) ;
201+ } ) ;
202+ } ) ;
203+
204+ describe ( "Schema Validation" , ( ) => {
205+ it ( "should validate parsed data against schema" , async ( ) => {
206+ const rssContent = readFileSync ( join ( __dirname , "fixtures/rss2.xml" ) , "utf-8" ) ;
207+
208+ server . use (
209+ http . get ( "https://example.com/schema-test.xml" , ( ) => {
210+ return new HttpResponse ( rssContent , {
211+ status : 200 ,
212+ headers : {
213+ "content-type" : "application/rss+xml"
214+ }
215+ } ) ;
216+ } )
217+ ) ;
218+
219+ const loader = feedLoader ( { url : "https://example.com/schema-test.xml" } ) ;
220+ await loader . load ( {
221+ store : mockStore as any ,
222+ logger : mockLogger as any ,
223+ parseData : mockParseData as any ,
224+ meta : mockMeta
225+ } ) ;
226+
227+ const storedItem = mockStore . get ( "https://example.com/first-post" ) ;
228+ const validationResult = ItemSchema . safeParse ( storedItem ! . data ) ;
229+
230+ expect ( validationResult . success ) . toBe ( true ) ;
231+ if ( validationResult . success ) {
232+ expect ( validationResult . data . title ) . toBe ( "First Post" ) ;
233+ expect ( validationResult . data . link ) . toBe ( "https://example.com/first-post" ) ;
234+ expect ( validationResult . data . guid ) . toBe ( "https://example.com/first-post" ) ;
235+ }
236+ } ) ;
237+
238+ it ( "should handle all schema fields correctly" , async ( ) => {
239+ const complexRss = `<?xml version="1.0" encoding="UTF-8"?>
240+ <rss version="2.0">
241+ <channel>
242+ <title>Complex Feed</title>
243+ <item>
244+ <title>Complex Item</title>
245+ <link>https://example.com/complex</link>
246+ <description>Complex description</description>
247+ <pubDate>Wed, 21 Jun 2023 10:00:00 GMT</pubDate>
248+ <guid>https://example.com/complex</guid>
249+ <author>author@example.com (Author Name)</author>
250+ <category>Technology</category>
251+ <category>News</category>
252+ <enclosure url="https://example.com/file.mp3" length="1024" type="audio/mpeg" />
253+ </item>
254+ </channel>
255+ </rss>` ;
256+
257+ server . use (
258+ http . get ( "https://example.com/complex.xml" , ( ) => {
259+ return new HttpResponse ( complexRss , {
260+ status : 200 ,
261+ headers : {
262+ "content-type" : "application/rss+xml"
263+ }
264+ } ) ;
265+ } )
266+ ) ;
267+
268+ const loader = feedLoader ( { url : "https://example.com/complex.xml" } ) ;
269+ await loader . load ( {
270+ store : mockStore as any ,
271+ logger : mockLogger as any ,
272+ parseData : mockParseData as any ,
273+ meta : mockMeta
274+ } ) ;
275+
276+ const storedItem = mockStore . get ( "https://example.com/complex" ) ;
277+ expect ( storedItem ) . toBeDefined ( ) ;
278+
279+ const validationResult = ItemSchema . safeParse ( storedItem ! . data ) ;
280+ expect ( validationResult . success ) . toBe ( true ) ;
281+
282+ if ( validationResult . success ) {
283+ expect ( validationResult . data . title ) . toBe ( "Complex Item" ) ;
284+ expect ( validationResult . data . categories ) . toContain ( "Technology" ) ;
285+ expect ( validationResult . data . categories ) . toContain ( "News" ) ;
286+ expect ( validationResult . data . enclosures ) . toHaveLength ( 1 ) ;
287+ expect ( validationResult . data . enclosures ! [ 0 ] ! . url ) . toBe ( "https://example.com/file.mp3" ) ;
288+ expect ( validationResult . data . enclosures ! [ 0 ] ! . type ) . toBe ( "audio/mpeg" ) ;
289+ }
290+ } ) ;
291+ } ) ;
292+
293+ } ) ;
0 commit comments