@@ -22,13 +22,14 @@ import (
2222 "net/http/httptest"
2323 "net/url"
2424 "os"
25+ "sync/atomic"
2526 "testing"
2627 "time"
2728
2829 "github.com/bartventer/httpcache/internal"
2930 "github.com/bartventer/httpcache/internal/testutil"
3031 "github.com/bartventer/httpcache/store"
31- _ "github.com/bartventer/httpcache/store/memcache"
32+ "github.com/bartventer/httpcache/store/memcache"
3233)
3334
3435func mockTransport (fields func (rt * transport )) * transport {
@@ -860,3 +861,99 @@ func Test_transport_Vary(t *testing.T) {
860861 testutil .AssertEqual (t , tc .wantBody , string (body ), i )
861862 }
862863}
864+
865+ // This test verifies that when a cached response is revalidated via a 304 Not
866+ // Modified, the cache entry is updated with any new headers from the 304
867+ // response, and subsequent requests can HIT the cache again until it becomes
868+ // stale once more.
869+ func Test_transport_RevalidationUpdatesCache (t * testing.T ) {
870+ var originCalls atomic.Int32
871+
872+ const etag = `"v1"`
873+
874+ server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
875+ originCalls .Add (1 )
876+
877+ // Revalidation path: client sends validator, server says cached body is still valid
878+ if r .Header .Get ("If-None-Match" ) == etag {
879+ w .Header ().Set ("ETag" , etag )
880+ w .Header ().Set ("Cache-Control" , "max-age=1" )
881+ w .Header ().Set ("Expires" , time .Now ().Add (1 * time .Second ).UTC ().Format (http .TimeFormat ))
882+ w .WriteHeader (http .StatusNotModified )
883+ return
884+ }
885+
886+ // Initial fetch
887+ w .Header ().Set ("ETag" , etag )
888+ w .Header ().Set ("Cache-Control" , "max-age=1" )
889+ w .Header ().Set ("Expires" , time .Now ().Add (1 * time .Second ).UTC ().Format (http .TimeFormat ))
890+ w .WriteHeader (http .StatusOK )
891+ _ , _ = w .Write ([]byte ("hello" ))
892+ }))
893+ defer server .Close ()
894+
895+ c := memcache .Open ()
896+ tr := newTransport (c )
897+
898+ req , _ := http .NewRequest (http .MethodGet , server .URL , nil )
899+
900+ tests := []struct {
901+ name string
902+ expectedStatusCode int
903+ expectedCacheStatus string
904+ expectedBody string
905+ expectedOriginCalls int32
906+ preReqFunc func ()
907+ }{
908+ {
909+ name : "Initial request should be a MISS" ,
910+ expectedStatusCode : http .StatusOK ,
911+ expectedCacheStatus : internal .CacheStatusMiss .Value ,
912+ expectedBody : "hello" ,
913+ expectedOriginCalls : 1 ,
914+ },
915+ {
916+ name : "Second request should be a HIT" ,
917+ expectedStatusCode : http .StatusOK ,
918+ expectedCacheStatus : internal .CacheStatusHit .Value ,
919+ expectedBody : "hello" ,
920+ expectedOriginCalls : 1 ,
921+ },
922+ {
923+ name : "After becoming stale, request should be REVALIDATED via 304" ,
924+ expectedStatusCode : http .StatusOK ,
925+ expectedCacheStatus : internal .CacheStatusRevalidated .Value ,
926+ expectedBody : "hello" ,
927+ expectedOriginCalls : 2 ,
928+ preReqFunc : func () {
929+ time .Sleep (1100 * time .Millisecond )
930+ },
931+ },
932+ {
933+ name : "After revalidation, request should be HIT again" ,
934+ expectedStatusCode : http .StatusOK ,
935+ expectedCacheStatus : internal .CacheStatusHit .Value ,
936+ expectedBody : "hello" ,
937+ expectedOriginCalls : 2 ,
938+ },
939+ }
940+ for _ , tc := range tests {
941+ t .Run (tc .name , func (t * testing.T ) {
942+ if tc .preReqFunc != nil {
943+ tc .preReqFunc ()
944+ }
945+ resp , err := tr .RoundTrip (req )
946+ testutil .RequireNoError (t , err )
947+ testutil .AssertEqual (t , tc .expectedStatusCode , resp .StatusCode )
948+ testutil .AssertEqual (
949+ t ,
950+ tc .expectedCacheStatus ,
951+ resp .Header .Get (internal .CacheStatusHeader ),
952+ )
953+ body , _ := io .ReadAll (resp .Body )
954+ _ = resp .Body .Close ()
955+ testutil .AssertEqual (t , tc .expectedBody , string (body ))
956+ testutil .AssertEqual (t , tc .expectedOriginCalls , originCalls .Load ())
957+ })
958+ }
959+ }
0 commit comments