frankenphp_test.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  1. package frankenphp_test
  2. import (
  3. "context"
  4. "fmt"
  5. "io"
  6. "log"
  7. "net/http"
  8. "net/http/cookiejar"
  9. "net/http/httptest"
  10. "net/http/httptrace"
  11. "net/textproto"
  12. "net/url"
  13. "os"
  14. "strconv"
  15. "strings"
  16. "sync"
  17. "testing"
  18. "time"
  19. "github.com/dunglas/frankenphp"
  20. "github.com/stretchr/testify/assert"
  21. "github.com/stretchr/testify/require"
  22. "go.uber.org/zap"
  23. "go.uber.org/zap/zaptest"
  24. "go.uber.org/zap/zaptest/observer"
  25. )
  26. type testOptions struct {
  27. workerScript string
  28. nbWorkers int
  29. nbParrallelRequests int
  30. realServer bool
  31. logger *zap.Logger
  32. initOpts []frankenphp.Option
  33. }
  34. func runTest(t *testing.T, test func(func(http.ResponseWriter, *http.Request), *httptest.Server, int), opts *testOptions) {
  35. if opts == nil {
  36. opts = &testOptions{}
  37. }
  38. if opts.nbWorkers == 0 {
  39. opts.nbWorkers = 2
  40. }
  41. if opts.nbParrallelRequests == 0 {
  42. opts.nbParrallelRequests = 100
  43. }
  44. cwd, _ := os.Getwd()
  45. testDataDir := cwd + "/testdata/"
  46. if opts.logger == nil {
  47. opts.logger = zaptest.NewLogger(t)
  48. }
  49. initOpts := []frankenphp.Option{frankenphp.WithLogger(opts.logger)}
  50. if opts.workerScript != "" {
  51. initOpts = append(initOpts, frankenphp.WithWorkers(testDataDir+opts.workerScript, opts.nbWorkers))
  52. }
  53. initOpts = append(initOpts, opts.initOpts...)
  54. err := frankenphp.Init(initOpts...)
  55. require.Nil(t, err)
  56. defer frankenphp.Shutdown()
  57. handler := func(w http.ResponseWriter, r *http.Request) {
  58. req := frankenphp.NewRequestWithContext(r, testDataDir, nil)
  59. if err := frankenphp.ServeHTTP(w, req); err != nil {
  60. panic(err)
  61. }
  62. }
  63. var ts *httptest.Server
  64. if opts.realServer {
  65. ts = httptest.NewServer(http.HandlerFunc(handler))
  66. defer ts.Close()
  67. }
  68. var wg sync.WaitGroup
  69. wg.Add(opts.nbParrallelRequests)
  70. for i := 0; i < opts.nbParrallelRequests; i++ {
  71. go func(i int) {
  72. test(handler, ts, i)
  73. wg.Done()
  74. }(i)
  75. }
  76. wg.Wait()
  77. }
  78. func TestHelloWorld_module(t *testing.T) { testHelloWorld(t, nil) }
  79. func TestHelloWorld_worker(t *testing.T) { testHelloWorld(t, &testOptions{workerScript: "index.php"}) }
  80. func testHelloWorld(t *testing.T, opts *testOptions) {
  81. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  82. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/index.php?i=%d", i), nil)
  83. w := httptest.NewRecorder()
  84. handler(w, req)
  85. resp := w.Result()
  86. body, _ := io.ReadAll(resp.Body)
  87. assert.Equal(t, fmt.Sprintf("I am by birth a Genevese (%d)", i), string(body))
  88. }, opts)
  89. }
  90. func TestFinishRequest_module(t *testing.T) { testFinishRequest(t, nil) }
  91. func TestFinishRequest_worker(t *testing.T) {
  92. testFinishRequest(t, &testOptions{workerScript: "finish-request.php"})
  93. }
  94. func testFinishRequest(t *testing.T, opts *testOptions) {
  95. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  96. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/finish-request.php?i=%d", i), nil)
  97. w := httptest.NewRecorder()
  98. handler(w, req)
  99. resp := w.Result()
  100. body, _ := io.ReadAll(resp.Body)
  101. assert.Equal(t, fmt.Sprintf("This is output %d\n", i), string(body))
  102. }, opts)
  103. }
  104. func TestServerVariable_module(t *testing.T) { testServerVariable(t, nil) }
  105. func TestServerVariable_worker(t *testing.T) {
  106. testServerVariable(t, &testOptions{workerScript: "server-variable.php"})
  107. }
  108. func testServerVariable(t *testing.T, opts *testOptions) {
  109. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  110. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/server-variable.php/baz/bat?foo=a&bar=b&i=%d#hash", i), nil)
  111. req.SetBasicAuth("kevin", "password")
  112. w := httptest.NewRecorder()
  113. handler(w, req)
  114. resp := w.Result()
  115. body, _ := io.ReadAll(resp.Body)
  116. strBody := string(body)
  117. assert.Contains(t, strBody, "[REMOTE_HOST]")
  118. assert.Contains(t, strBody, "[REMOTE_USER] => kevin")
  119. assert.Contains(t, strBody, "[PHP_AUTH_USER] => kevin")
  120. assert.Contains(t, strBody, "[PHP_AUTH_PW] => password")
  121. assert.Contains(t, strBody, "[HTTP_AUTHORIZATION] => Basic a2V2aW46cGFzc3dvcmQ=")
  122. assert.Contains(t, strBody, "[DOCUMENT_ROOT]")
  123. assert.Contains(t, strBody, "[PHP_SELF] => /server-variable.php/baz/bat")
  124. assert.Contains(t, strBody, "[CONTENT_TYPE]")
  125. assert.Contains(t, strBody, fmt.Sprintf("[QUERY_STRING] => foo=a&bar=b&i=%d#hash", i))
  126. assert.Contains(t, strBody, fmt.Sprintf("[REQUEST_URI] => /server-variable.php/baz/bat?foo=a&bar=b&i=%d#hash", i))
  127. assert.Contains(t, strBody, "[CONTENT_LENGTH]")
  128. assert.Contains(t, strBody, "[REMOTE_ADDR]")
  129. assert.Contains(t, strBody, "[REMOTE_PORT]")
  130. assert.Contains(t, strBody, "[REQUEST_SCHEME] => http")
  131. assert.Contains(t, strBody, "[DOCUMENT_URI]")
  132. assert.Contains(t, strBody, "[AUTH_TYPE]")
  133. assert.Contains(t, strBody, "[REMOTE_IDENT]")
  134. assert.Contains(t, strBody, "[REQUEST_METHOD] => GET")
  135. assert.Contains(t, strBody, "[SERVER_NAME] => example.com")
  136. assert.Contains(t, strBody, "[SERVER_PROTOCOL] => HTTP/1.1")
  137. assert.Contains(t, strBody, "[SCRIPT_FILENAME]")
  138. assert.Contains(t, strBody, "[SERVER_SOFTWARE] => FrankenPHP")
  139. assert.Contains(t, strBody, "[REQUEST_TIME_FLOAT]")
  140. assert.Contains(t, strBody, "[REQUEST_TIME]")
  141. assert.Contains(t, strBody, "[REQUEST_TIME]")
  142. }, opts)
  143. }
  144. func TestPathInfo_module(t *testing.T) { testPathInfo(t, nil) }
  145. func TestPathInfo_worker(t *testing.T) {
  146. testPathInfo(t, &testOptions{workerScript: "server-variable.php"})
  147. }
  148. func testPathInfo(t *testing.T, opts *testOptions) {
  149. runTest(t, func(_ func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  150. handler := func(w http.ResponseWriter, r *http.Request) {
  151. cwd, _ := os.Getwd()
  152. testDataDir := cwd + "/testdata/"
  153. requestURI := r.URL.RequestURI()
  154. rewriteRequest := frankenphp.NewRequestWithContext(r, testDataDir, nil)
  155. rewriteRequest.URL.Path = "/server-variable.php/pathinfo"
  156. fc, _ := frankenphp.FromContext(rewriteRequest.Context())
  157. fc.Env["REQUEST_URI"] = requestURI
  158. err := frankenphp.ServeHTTP(w, rewriteRequest)
  159. assert.NoError(t, err)
  160. }
  161. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/pathinfo/%d", i), nil)
  162. w := httptest.NewRecorder()
  163. handler(w, req)
  164. resp := w.Result()
  165. body, _ := io.ReadAll(resp.Body)
  166. strBody := string(body)
  167. assert.Contains(t, strBody, "[PATH_INFO] => /pathinfo")
  168. assert.Contains(t, strBody, fmt.Sprintf("[REQUEST_URI] => /pathinfo/%d", i))
  169. assert.Contains(t, strBody, "[PATH_TRANSLATED] =>")
  170. assert.Contains(t, strBody, "[SCRIPT_NAME] => /server-variable.php")
  171. }, opts)
  172. }
  173. func TestHeaders_module(t *testing.T) { testHeaders(t, nil) }
  174. func TestHeaders_worker(t *testing.T) { testHeaders(t, &testOptions{workerScript: "headers.php"}) }
  175. func testHeaders(t *testing.T, opts *testOptions) {
  176. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  177. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/headers.php?i=%d", i), nil)
  178. w := httptest.NewRecorder()
  179. handler(w, req)
  180. resp := w.Result()
  181. body, _ := io.ReadAll(resp.Body)
  182. assert.Equal(t, "Hello", string(body))
  183. assert.Equal(t, 201, resp.StatusCode)
  184. assert.Equal(t, "bar", resp.Header.Get("Foo"))
  185. assert.Equal(t, "bar2", resp.Header.Get("Foo2"))
  186. assert.Equal(t, fmt.Sprintf("%d", i), resp.Header.Get("I"))
  187. }, opts)
  188. }
  189. func TestInput_module(t *testing.T) { testInput(t, nil) }
  190. func TestInput_worker(t *testing.T) { testInput(t, &testOptions{workerScript: "input.php"}) }
  191. func testInput(t *testing.T, opts *testOptions) {
  192. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  193. req := httptest.NewRequest("POST", "http://example.com/input.php", strings.NewReader(fmt.Sprintf("post data %d", i)))
  194. w := httptest.NewRecorder()
  195. handler(w, req)
  196. resp := w.Result()
  197. body, _ := io.ReadAll(resp.Body)
  198. assert.Equal(t, fmt.Sprintf("post data %d", i), string(body))
  199. assert.Equal(t, "bar", resp.Header.Get("Foo"))
  200. }, opts)
  201. }
  202. func TestPostSuperGlobals_module(t *testing.T) { testPostSuperGlobals(t, nil) }
  203. func TestPostSuperGlobals_worker(t *testing.T) {
  204. testPostSuperGlobals(t, &testOptions{workerScript: "super-globals.php"})
  205. }
  206. func testPostSuperGlobals(t *testing.T, opts *testOptions) {
  207. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  208. formData := url.Values{"baz": {"bat"}, "i": {fmt.Sprintf("%d", i)}}
  209. req := httptest.NewRequest("POST", fmt.Sprintf("http://example.com/super-globals.php?foo=bar&iG=%d", i), strings.NewReader(formData.Encode()))
  210. req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
  211. w := httptest.NewRecorder()
  212. handler(w, req)
  213. resp := w.Result()
  214. body, _ := io.ReadAll(resp.Body)
  215. assert.Contains(t, string(body), "'foo' => 'bar'")
  216. assert.Contains(t, string(body), fmt.Sprintf("'i' => '%d'", i))
  217. assert.Contains(t, string(body), "'baz' => 'bat'")
  218. assert.Contains(t, string(body), fmt.Sprintf("'iG' => '%d'", i))
  219. }, opts)
  220. }
  221. func TestCookies_module(t *testing.T) { testCookies(t, nil) }
  222. func TestCookies_worker(t *testing.T) { testCookies(t, &testOptions{workerScript: "cookies.php"}) }
  223. func testCookies(t *testing.T, opts *testOptions) {
  224. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  225. req := httptest.NewRequest("GET", "http://example.com/cookies.php", nil)
  226. req.AddCookie(&http.Cookie{Name: "foo", Value: "bar"})
  227. req.AddCookie(&http.Cookie{Name: "i", Value: fmt.Sprintf("%d", i)})
  228. w := httptest.NewRecorder()
  229. handler(w, req)
  230. resp := w.Result()
  231. body, _ := io.ReadAll(resp.Body)
  232. assert.Contains(t, string(body), "'foo' => 'bar'")
  233. assert.Contains(t, string(body), fmt.Sprintf("'i' => '%d'", i))
  234. }, opts)
  235. }
  236. func TestSession_module(t *testing.T) { testSession(t, nil) }
  237. func TestSession_worker(t *testing.T) {
  238. testSession(t, &testOptions{workerScript: "session.php"})
  239. }
  240. func testSession(t *testing.T, opts *testOptions) {
  241. if opts == nil {
  242. opts = &testOptions{}
  243. }
  244. opts.realServer = true
  245. runTest(t, func(_ func(http.ResponseWriter, *http.Request), ts *httptest.Server, i int) {
  246. jar, err := cookiejar.New(&cookiejar.Options{})
  247. if err != nil {
  248. panic(err)
  249. }
  250. client := &http.Client{Jar: jar}
  251. resp1, err := client.Get(ts.URL + "/session.php")
  252. if err != nil {
  253. panic(err)
  254. }
  255. body1, _ := io.ReadAll(resp1.Body)
  256. assert.Equal(t, "Count: 0\n", string(body1))
  257. resp2, err := client.Get(ts.URL + "/session.php")
  258. if err != nil {
  259. panic(err)
  260. }
  261. body2, _ := io.ReadAll(resp2.Body)
  262. assert.Equal(t, "Count: 1\n", string(body2))
  263. }, opts)
  264. }
  265. func TestPhpInfo_module(t *testing.T) { testPhpInfo(t, nil) }
  266. func TestPhpInfo_worker(t *testing.T) { testPhpInfo(t, &testOptions{workerScript: "phpinfo.php"}) }
  267. func testPhpInfo(t *testing.T, opts *testOptions) {
  268. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  269. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/phpinfo.php?i=%d", i), nil)
  270. w := httptest.NewRecorder()
  271. handler(w, req)
  272. resp := w.Result()
  273. body, _ := io.ReadAll(resp.Body)
  274. assert.Contains(t, string(body), "frankenphp")
  275. assert.Contains(t, string(body), fmt.Sprintf("i=%d", i))
  276. }, opts)
  277. }
  278. func TestPersistentObject_module(t *testing.T) { testPersistentObject(t, nil) }
  279. func TestPersistentObject_worker(t *testing.T) {
  280. testPersistentObject(t, &testOptions{workerScript: "persistent-object.php"})
  281. }
  282. func testPersistentObject(t *testing.T, opts *testOptions) {
  283. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  284. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/persistent-object.php?i=%d", i), nil)
  285. w := httptest.NewRecorder()
  286. handler(w, req)
  287. resp := w.Result()
  288. body, _ := io.ReadAll(resp.Body)
  289. assert.Equal(t, fmt.Sprintf(`request: %d
  290. class exists: 1
  291. id: obj1
  292. object id: 1`, i), string(body))
  293. }, opts)
  294. }
  295. func TestAutoloader_module(t *testing.T) { testAutoloader(t, nil) }
  296. func TestAutoloader_worker(t *testing.T) {
  297. testAutoloader(t, &testOptions{workerScript: "autoloader.php"})
  298. }
  299. func testAutoloader(t *testing.T, opts *testOptions) {
  300. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  301. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/autoloader.php?i=%d", i), nil)
  302. w := httptest.NewRecorder()
  303. handler(w, req)
  304. resp := w.Result()
  305. body, _ := io.ReadAll(resp.Body)
  306. assert.Equal(t, fmt.Sprintf(`request %d
  307. my_autoloader`, i), string(body))
  308. }, opts)
  309. }
  310. func TestLog_module(t *testing.T) { testLog(t, &testOptions{}) }
  311. func TestLog_worker(t *testing.T) {
  312. testLog(t, &testOptions{workerScript: "log.php"})
  313. }
  314. func testLog(t *testing.T, opts *testOptions) {
  315. logger, logs := observer.New(zap.InfoLevel)
  316. opts.logger = zap.New(logger)
  317. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  318. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/log.php?i=%d", i), nil)
  319. w := httptest.NewRecorder()
  320. handler(w, req)
  321. var found bool
  322. searched := fmt.Sprintf("request %d", i)
  323. for _, entry := range logs.All() {
  324. if entry.Message == searched {
  325. found = true
  326. break
  327. }
  328. }
  329. assert.True(t, found)
  330. }, opts)
  331. }
  332. func TestConnectionAbortNormal_module(t *testing.T) { testConnectionAbortNormal(t, &testOptions{}) }
  333. func TestConnectionAbortNormal_worker(t *testing.T) {
  334. testConnectionAbortNormal(t, &testOptions{workerScript: "connectionStatusLog.php"})
  335. }
  336. func testConnectionAbortNormal(t *testing.T, opts *testOptions) {
  337. logger, logs := observer.New(zap.InfoLevel)
  338. opts.logger = zap.New(logger)
  339. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  340. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/connectionStatusLog.php?i=%d", i), nil)
  341. w := httptest.NewRecorder()
  342. ctx, cancel := context.WithCancel(req.Context())
  343. req = req.WithContext(ctx)
  344. cancel()
  345. handler(w, req)
  346. // todo: remove conditions on wall clock to avoid race conditions/flakiness
  347. time.Sleep(1000 * time.Microsecond)
  348. var found bool
  349. searched := fmt.Sprintf("request %d: 1", i)
  350. for _, entry := range logs.All() {
  351. if entry.Message == searched {
  352. found = true
  353. break
  354. }
  355. }
  356. assert.True(t, found)
  357. }, opts)
  358. }
  359. func TestConnectionAbortFlush_module(t *testing.T) { testConnectionAbortFlush(t, &testOptions{}) }
  360. func TestConnectionAbortFlush_worker(t *testing.T) {
  361. testConnectionAbortFlush(t, &testOptions{workerScript: "connectionStatusLog.php"})
  362. }
  363. func testConnectionAbortFlush(t *testing.T, opts *testOptions) {
  364. logger, logs := observer.New(zap.InfoLevel)
  365. opts.logger = zap.New(logger)
  366. runTest(t, func(handler func(w http.ResponseWriter, response *http.Request), _ *httptest.Server, i int) {
  367. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/connectionStatusLog.php?i=%d&flush", i), nil)
  368. w := httptest.NewRecorder()
  369. ctx, cancel := context.WithCancel(req.Context())
  370. req = req.WithContext(ctx)
  371. cancel()
  372. handler(w, req)
  373. // todo: remove conditions on wall clock to avoid race conditions/flakiness
  374. time.Sleep(1000 * time.Microsecond)
  375. var found bool
  376. searched := fmt.Sprintf("request %d: 1", i)
  377. for _, entry := range logs.All() {
  378. if entry.Message == searched {
  379. found = true
  380. break
  381. }
  382. }
  383. assert.True(t, found)
  384. }, opts)
  385. }
  386. func TestConnectionAbortFinish_module(t *testing.T) { testConnectionAbortFinish(t, &testOptions{}) }
  387. func TestConnectionAbortFinish_worker(t *testing.T) {
  388. testConnectionAbortFinish(t, &testOptions{workerScript: "connectionStatusLog.php"})
  389. }
  390. func testConnectionAbortFinish(t *testing.T, opts *testOptions) {
  391. logger, logs := observer.New(zap.InfoLevel)
  392. opts.logger = zap.New(logger)
  393. runTest(t, func(handler func(w http.ResponseWriter, response *http.Request), _ *httptest.Server, i int) {
  394. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/connectionStatusLog.php?i=%d&finish", i), nil)
  395. w := httptest.NewRecorder()
  396. ctx, cancel := context.WithCancel(req.Context())
  397. req = req.WithContext(ctx)
  398. cancel()
  399. handler(w, req)
  400. // todo: remove conditions on wall clock to avoid race conditions/flakiness
  401. time.Sleep(1000 * time.Microsecond)
  402. var found bool
  403. searched := fmt.Sprintf("request %d: 0", i)
  404. for _, entry := range logs.All() {
  405. if entry.Message == searched {
  406. found = true
  407. break
  408. }
  409. }
  410. assert.True(t, found)
  411. }, opts)
  412. }
  413. func TestException_module(t *testing.T) { testException(t, &testOptions{}) }
  414. func TestException_worker(t *testing.T) {
  415. testException(t, &testOptions{workerScript: "exception.php"})
  416. }
  417. func testException(t *testing.T, opts *testOptions) {
  418. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  419. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/exception.php?i=%d", i), nil)
  420. w := httptest.NewRecorder()
  421. handler(w, req)
  422. resp := w.Result()
  423. body, _ := io.ReadAll(resp.Body)
  424. assert.Contains(t, string(body), "hello")
  425. assert.Contains(t, string(body), fmt.Sprintf(`Uncaught Exception: request %d`, i))
  426. }, opts)
  427. }
  428. func TestEarlyHints_module(t *testing.T) { testEarlyHints(t, &testOptions{}) }
  429. func TestEarlyHints_worker(t *testing.T) {
  430. testEarlyHints(t, &testOptions{workerScript: "early-hints.php"})
  431. }
  432. func testEarlyHints(t *testing.T, opts *testOptions) {
  433. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  434. var earlyHintReceived bool
  435. trace := &httptrace.ClientTrace{
  436. Got1xxResponse: func(code int, header textproto.MIMEHeader) error {
  437. switch code {
  438. case http.StatusEarlyHints:
  439. assert.Equal(t, "</style.css>; rel=preload; as=style", header.Get("Link"))
  440. assert.Equal(t, strconv.Itoa(i), header.Get("Request"))
  441. earlyHintReceived = true
  442. }
  443. return nil
  444. },
  445. }
  446. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/early-hints.php?i=%d", i), nil)
  447. w := NewRecorder()
  448. w.ClientTrace = trace
  449. handler(w, req)
  450. assert.Equal(t, strconv.Itoa(i), w.Header().Get("Request"))
  451. assert.Equal(t, "", w.Header().Get("Link"))
  452. assert.True(t, earlyHintReceived)
  453. }, opts)
  454. }
  455. type streamResponseRecorder struct {
  456. *httptest.ResponseRecorder
  457. writeCallback func(buf []byte)
  458. }
  459. func (srr *streamResponseRecorder) Write(buf []byte) (int, error) {
  460. srr.writeCallback(buf)
  461. return srr.ResponseRecorder.Write(buf)
  462. }
  463. func TestFlush_module(t *testing.T) { testFlush(t, &testOptions{}) }
  464. func TestFlush_worker(t *testing.T) {
  465. testFlush(t, &testOptions{workerScript: "flush.php"})
  466. }
  467. func testFlush(t *testing.T, opts *testOptions) {
  468. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  469. var j int
  470. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/flush.php?i=%d", i), nil)
  471. w := &streamResponseRecorder{httptest.NewRecorder(), func(buf []byte) {
  472. if j == 0 {
  473. assert.Equal(t, []byte("He"), buf)
  474. } else {
  475. assert.Equal(t, []byte(fmt.Sprintf("llo %d", i)), buf)
  476. }
  477. j++
  478. }}
  479. handler(w, req)
  480. assert.Equal(t, 2, j)
  481. }, opts)
  482. }
  483. func ExampleServeHTTP() {
  484. if err := frankenphp.Init(); err != nil {
  485. panic(err)
  486. }
  487. defer frankenphp.Shutdown()
  488. http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  489. req := frankenphp.NewRequestWithContext(r, "/path/to/document/root", nil)
  490. if err := frankenphp.ServeHTTP(w, req); err != nil {
  491. panic(err)
  492. }
  493. })
  494. log.Fatal(http.ListenAndServe(":8080", nil))
  495. }