1010 */
1111import { describe , it , expect , vi , beforeEach , afterEach } from "vite-plus/test" ;
1212import createCloudflareCdnCacheAdapter , {
13+ encodeCloudflareCacheTag ,
1314 CloudflareCdnCacheAdapter ,
1415} from "../packages/cloudflare/src/cache/cdn-adapter.runtime.js" ;
1516import {
@@ -230,17 +231,46 @@ describe("CloudflareCdnCacheAdapter", () => {
230231 cacheControl : "s-maxage=60" ,
231232 tags : [ "/blog" , "_N_T_/blog" , "posts" ] ,
232233 } ) ;
233- expect ( headers [ "Cache-Tag" ] ) . toBe ( "/blog,_N_T_/blog,posts" ) ;
234+ expect ( headers [ "Cache-Tag" ] ) . toBe (
235+ [ "/blog" , "_N_T_/blog" , "posts" ] . map ( encodeCloudflareCacheTag ) . join ( "," ) ,
236+ ) ;
234237 expect ( headers [ "Cache-Control" ] ) . toBe ( "public, max-age=0, must-revalidate" ) ;
235238 expect ( headers [ "CDN-Cache-Control" ] ) . toBe ( "public, max-age=60" ) ;
236239 } ) ;
237240
238- it ( "skips tags containing the comma separator or that are too long" , ( ) => {
241+ it ( "preserves an empty Next.js tag in response invalidation metadata" , ( ) => {
242+ const tags = [ "" , "posts" ] ;
243+ const headers = adapter . buildResponseHeaders ( { cacheControl : "s-maxage=60" , tags } ) ;
244+ expect ( headers [ "Cache-Tag" ] ) . toBe ( tags . map ( encodeCloudflareCacheTag ) . join ( "," ) ) ;
245+ } ) ;
246+
247+ it ( "encodes the complete Next-valid set of long Unicode and case-sensitive tags" , ( ) => {
248+ const tags = [
249+ "é" . repeat ( 128 ) ,
250+ "Product List" ,
251+ "product list" ,
252+ ...Array . from ( { length : 125 } , ( _ , index ) => `tag-${ index } -${ "x" . repeat ( 240 ) } ` ) ,
253+ ] ;
239254 const headers = adapter . buildResponseHeaders ( {
240255 cacheControl : "s-maxage=60" ,
241- tags : [ "a,b" , "x" . repeat ( 2000 ) , "ok" ] ,
256+ tags,
257+ } ) ;
258+ expect ( headers [ "Cache-Tag" ] ) . toBe ( tags . map ( encodeCloudflareCacheTag ) . join ( "," ) ) ;
259+ expect ( headers [ "Cache-Tag" ] ?. split ( "," ) ) . toHaveLength ( 128 ) ;
260+ } ) ;
261+
262+ it ( "makes a response uncacheable rather than emitting incomplete tag metadata" , ( ) => {
263+ expect (
264+ adapter . buildResponseHeaders ( {
265+ cacheControl : "s-maxage=60" ,
266+ tags : Array . from ( { length : 500 } , ( _ , index ) => `tag-${ index } ` ) ,
267+ } ) ,
268+ ) . toEqual ( {
269+ "Cache-Control" : "no-store" ,
270+ "CDN-Cache-Control" : null ,
271+ "Cloudflare-CDN-Cache-Control" : null ,
272+ "Cache-Tag" : null ,
242273 } ) ;
243- expect ( headers [ "Cache-Tag" ] ) . toBe ( "ok" ) ;
244274 } ) ;
245275
246276 it ( "returns no-store and clears owned headers when there is no cacheable policy" , ( ) => {
@@ -364,7 +394,7 @@ describe("CloudflareCdnCacheAdapter", () => {
364394 "public, max-age=60, stale-while-revalidate=31536000" ,
365395 ) ;
366396 expect ( response . headers . get ( "Cloudflare-CDN-Cache-Control" ) ) . toBeNull ( ) ;
367- expect ( response . headers . get ( "Cache-Tag" ) ) . toBe ( "/dynamic-html" ) ;
397+ expect ( response . headers . get ( "Cache-Tag" ) ) . toBe ( encodeCloudflareCacheTag ( "/dynamic-html" ) ) ;
368398 await expect ( response . text ( ) ) . resolves . toBe ( "<h1>personalized</h1>" ) ;
369399 await Promise . all ( pendingCacheWrites ) ;
370400 expect ( isrSet ) . not . toHaveBeenCalled ( ) ;
@@ -502,25 +532,35 @@ describe("CloudflareCdnCacheAdapter", () => {
502532 expect ( response . headers . get ( "Cache-Control" ) ) . toBe ( "public, max-age=0, must-revalidate" ) ;
503533 expect ( response . headers . get ( "CDN-Cache-Control" ) ) . toBe ( "public, max-age=60" ) ;
504534 expect ( response . headers . get ( "Cloudflare-CDN-Cache-Control" ) ) . toBeNull ( ) ;
505- expect ( response . headers . get ( "Cache-Tag" ) ) . toBe ( "/dashboard" ) ;
535+ expect ( response . headers . get ( "Cache-Tag" ) ) . toBe ( encodeCloudflareCacheTag ( "/dashboard" ) ) ;
506536 expect ( response . headers . get ( "X-Vinext-Cache" ) ) . toBe ( "MISS" ) ;
507537 await expect ( response . text ( ) ) . resolves . toBe ( "pending-dynamic-flight" ) ;
508538 } ) ;
509539
510540 it ( "revalidateTag purges the Workers Cache by tag via ctx.cache.purge" , async ( ) => {
511- const purge = vi . fn ( async ( ) => { } ) ;
541+ const purge = vi . fn ( async ( ) => ( { errors : [ ] , success : true } ) ) ;
512542 await runWithExecutionContext ( { waitUntil ( ) { } , cache : { purge } } , async ( ) => {
513543 await adapter . revalidateTag ( [ "posts" , "_N_T_/blog" ] ) ;
514544 } ) ;
515- expect ( purge ) . toHaveBeenCalledWith ( { tags : [ "posts" , "_N_T_/blog" ] } ) ;
545+ expect ( purge ) . toHaveBeenCalledWith ( {
546+ tags : [ "posts" , "_N_T_/blog" ] . map ( encodeCloudflareCacheTag ) ,
547+ } ) ;
516548 } ) ;
517549
518550 it ( "revalidateTag normalizes a single tag to an array" , async ( ) => {
519- const purge = vi . fn ( async ( ) => { } ) ;
551+ const purge = vi . fn ( async ( ) => ( { errors : [ ] , success : true } ) ) ;
520552 await runWithExecutionContext ( { waitUntil ( ) { } , cache : { purge } } , async ( ) => {
521553 await adapter . revalidateTag ( "posts" ) ;
522554 } ) ;
523- expect ( purge ) . toHaveBeenCalledWith ( { tags : [ "posts" ] } ) ;
555+ expect ( purge ) . toHaveBeenCalledWith ( { tags : [ encodeCloudflareCacheTag ( "posts" ) ] } ) ;
556+ } ) ;
557+
558+ it ( "revalidateTag purges an empty Next.js tag" , async ( ) => {
559+ const purge = vi . fn ( async ( ) => ( { errors : [ ] , success : true } ) ) ;
560+ await runWithExecutionContext ( { waitUntil ( ) { } , cache : { purge } } , async ( ) => {
561+ await adapter . revalidateTag ( "" ) ;
562+ } ) ;
563+ expect ( purge ) . toHaveBeenCalledWith ( { tags : [ encodeCloudflareCacheTag ( "" ) ] } ) ;
524564 } ) ;
525565
526566 it ( "revalidateTag is a no-op when the Workers Cache is absent (e.g. Node dev)" , async ( ) => {
@@ -529,12 +569,24 @@ describe("CloudflareCdnCacheAdapter", () => {
529569 } ) ;
530570
531571 it ( "revalidateTag does not purge for an empty tag set" , async ( ) => {
532- const purge = vi . fn ( async ( ) => { } ) ;
572+ const purge = vi . fn ( async ( ) => ( { errors : [ ] , success : true } ) ) ;
533573 await runWithExecutionContext ( { waitUntil ( ) { } , cache : { purge } } , async ( ) => {
534574 await adapter . revalidateTag ( [ ] ) ;
535575 } ) ;
536576 expect ( purge ) . not . toHaveBeenCalled ( ) ;
537577 } ) ;
578+
579+ it ( "surfaces a resolved Workers Cache purge failure" , async ( ) => {
580+ const purge = vi . fn ( async ( ) => ( {
581+ errors : [ { code : 10000 , message : "rate limited" } ] ,
582+ success : false ,
583+ } ) ) ;
584+ await expect (
585+ runWithExecutionContext ( { waitUntil ( ) { } , cache : { purge } } , ( ) =>
586+ adapter . revalidateTag ( "posts" ) ,
587+ ) ,
588+ ) . rejects . toThrow ( "Workers Cache purge failed: 10000: rate limited" ) ;
589+ } ) ;
538590} ) ;
539591
540592// ─── Adapter selection ────────────────────────────────────────────────────
0 commit comments