@@ -2,17 +2,17 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
22import { TestError } from '@test/errors'
33import type { Webmention , WebmentionResponse } from '@components/WebMentions/@types'
44import fixture from '../__fixtures__/index.fixture.json'
5- import {
6- fetchWebmentions ,
7- isOwnWebmention ,
8- webmentionsByUrl ,
9- webmentionCountByType ,
10- } from '../index'
11-
12- vi . mock ( 'astro:env/client' , ( ) => ( {
13- WEBMENTION_IO_TOKEN : 'test-token' ,
5+
6+ const tokenState = vi . hoisted ( ( ) => ( { value : 'test-token' } ) )
7+
8+ vi . mock ( 'astro:env/server' , ( ) => ( {
9+ get WEBMENTION_IO_TOKEN ( ) {
10+ return tokenState . value
11+ } ,
1412} ) )
1513
14+ const importWebmentionsModule = async ( ) => import ( '../index' )
15+
1616const fixtureResponse = fixture as WebmentionResponse
1717
1818const createMockResponse = (
@@ -31,16 +31,20 @@ describe('fetchWebmentions', () => {
3131 let fetchMock : ReturnType < typeof vi . fn >
3232
3333 beforeEach ( ( ) => {
34+ vi . resetModules ( )
35+ tokenState . value = 'test-token'
3436 fetchMock = vi . fn ( )
3537 vi . stubGlobal ( 'fetch' , fetchMock )
3638 } )
3739
3840 afterEach ( ( ) => {
3941 vi . unstubAllGlobals ( )
42+ vi . clearAllMocks ( )
4043 } )
4144
4245 it ( 'fetches, filters, and sanitizes webmentions from the API' , async ( ) => {
4346 fetchMock . mockResolvedValue ( createMockResponse ( ) )
47+ const { fetchWebmentions } = await importWebmentionsModule ( )
4448
4549 const results = await fetchWebmentions ( targetUrl )
4650
@@ -56,7 +60,8 @@ describe('fetchWebmentions', () => {
5660 expect ( parsedUrl . searchParams . get ( 'target' ) ) . toBe ( targetUrl )
5761 expect ( parsedUrl . searchParams . get ( 'token' ) ) . toBe ( 'test-token' )
5862 expect ( parsedUrl . searchParams . get ( 'per-page' ) ) . toBe ( '1000' )
59- expect ( init ) . toEqual ( { headers : { 'Cache-Control' : 'max-age=300' } } )
63+ expect ( init ) . toMatchObject ( { headers : { 'Cache-Control' : 'max-age=300' } } )
64+ expect ( init && 'signal' in ( init as Record < string , unknown > ) ) . toBe ( true )
6065
6166 expect ( results ) . toHaveLength ( 3 )
6267 const [ firstResult , secondResult , thirdResult ] = results
@@ -76,6 +81,7 @@ describe('fetchWebmentions', () => {
7681 fetchMock . mockResolvedValue (
7782 createMockResponse ( { ok : false , status : 500 , statusText : 'Server Error' , json : async ( ) => ( { } ) } ) ,
7883 )
84+ const { fetchWebmentions } = await importWebmentionsModule ( )
7985
8086 const results = await fetchWebmentions ( targetUrl )
8187
@@ -87,16 +93,71 @@ describe('fetchWebmentions', () => {
8793 it ( 'returns an empty array when the fetch call fails' , async ( ) => {
8894 const errorSpy = vi . spyOn ( console , 'error' ) . mockImplementation ( ( ) => { } )
8995 fetchMock . mockRejectedValue ( new TestError ( 'network down' ) )
96+ const { fetchWebmentions } = await importWebmentionsModule ( )
9097
9198 const results = await fetchWebmentions ( targetUrl )
9299
93100 expect ( results ) . toEqual ( [ ] )
94101 expect ( errorSpy ) . toHaveBeenCalled ( )
95102 errorSpy . mockRestore ( )
96103 } )
104+
105+ it ( 'deduplicates concurrent requests for the same URL' , async ( ) => {
106+ let resolveFetch : ( ( response : Response ) => void ) | undefined
107+ fetchMock . mockImplementation (
108+ ( ) => new Promise < Response > ( ( resolve ) => {
109+ resolveFetch = resolve
110+ } ) ,
111+ )
112+
113+ const { fetchWebmentions } = await importWebmentionsModule ( )
114+ const firstCall = fetchWebmentions ( targetUrl )
115+ const secondCall = fetchWebmentions ( targetUrl )
116+
117+ expect ( fetchMock ) . toHaveBeenCalledTimes ( 1 )
118+ resolveFetch ?.( createMockResponse ( ) )
119+
120+ const [ firstResult , secondResult ] = await Promise . all ( [ firstCall , secondCall ] )
121+ expect ( firstResult ) . toHaveLength ( 3 )
122+ expect ( secondResult ) . toHaveLength ( 3 )
123+ } )
124+
125+ it ( 'skips repeated fetch attempts during the failure cooldown window' , async ( ) => {
126+ const errorSpy = vi . spyOn ( console , 'error' ) . mockImplementation ( ( ) => { } )
127+ fetchMock . mockRejectedValue ( new TestError ( 'connect timeout' ) )
128+ const { fetchWebmentions } = await importWebmentionsModule ( )
129+
130+ const firstAttempt = await fetchWebmentions ( targetUrl )
131+ expect ( firstAttempt ) . toEqual ( [ ] )
132+ expect ( fetchMock ) . toHaveBeenCalledTimes ( 1 )
133+
134+ fetchMock . mockClear ( )
135+ const secondAttempt = await fetchWebmentions ( targetUrl )
136+ expect ( secondAttempt ) . toEqual ( [ ] )
137+ expect ( fetchMock ) . not . toHaveBeenCalled ( )
138+ errorSpy . mockRestore ( )
139+ } )
140+
141+ it ( 'logs a warning once and skips fetches when the token is missing or placeholder' , async ( ) => {
142+ tokenState . value = 'updateme'
143+ const warnSpy = vi . spyOn ( console , 'warn' ) . mockImplementation ( ( ) => { } )
144+ const { fetchWebmentions } = await importWebmentionsModule ( )
145+
146+ const results = await fetchWebmentions ( targetUrl )
147+
148+ expect ( results ) . toEqual ( [ ] )
149+ expect ( fetchMock ) . not . toHaveBeenCalled ( )
150+ expect ( warnSpy ) . toHaveBeenCalledTimes ( 1 )
151+ warnSpy . mockRestore ( )
152+ } )
97153} )
98154
99155describe ( 'Webmention helpers' , ( ) => {
156+ beforeEach ( ( ) => {
157+ vi . resetModules ( )
158+ tokenState . value = 'test-token'
159+ } )
160+
100161 const helperWebmentions : Webmention [ ] = [
101162 {
102163 'wm-id' : 'helper-1' ,
@@ -121,7 +182,8 @@ describe('Webmention helpers', () => {
121182 } ,
122183 ]
123184
124- it ( 'detects when a webmention originates from the configured domain' , ( ) => {
185+ it ( 'detects when a webmention originates from the configured domain' , async ( ) => {
186+ const { isOwnWebmention } = await importWebmentionsModule ( )
125187 const [ first , second , third ] = helperWebmentions
126188 if ( ! first || ! second || ! third ) {
127189 throw new TestError ( 'helper webmentions fixture must include three entries' )
@@ -132,14 +194,16 @@ describe('Webmention helpers', () => {
132194 expect ( isOwnWebmention ( third , [ 'https://elsewhere.example.com' ] ) ) . toBe ( true )
133195 } )
134196
135- it ( 'filters webmentions by target URL' , ( ) => {
197+ it ( 'filters webmentions by target URL' , async ( ) => {
198+ const { webmentionsByUrl } = await importWebmentionsModule ( )
136199 const filtered = webmentionsByUrl ( helperWebmentions , 'https://example.com/a' )
137200
138201 expect ( filtered ) . toHaveLength ( 2 )
139202 expect ( filtered . every ( ( entry ) => entry [ 'wm-target' ] === 'https://example.com/a' ) ) . toBe ( true )
140203 } )
141204
142- it ( 'counts webmentions that match specific interaction types' , ( ) => {
205+ it ( 'counts webmentions that match specific interaction types' , async ( ) => {
206+ const { webmentionCountByType } = await importWebmentionsModule ( )
143207 const count = webmentionCountByType ( helperWebmentions , 'https://example.com/a' , 'mention-of' , 'like-of' )
144208
145209 expect ( count ) . toBe ( 2 )
0 commit comments