frankenphp_test.go 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737
  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. "os/exec"
  15. "strconv"
  16. "strings"
  17. "sync"
  18. "testing"
  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. env map[string]string
  30. nbParrallelRequests int
  31. realServer bool
  32. logger *zap.Logger
  33. initOpts []frankenphp.Option
  34. }
  35. func runTest(t *testing.T, test func(func(http.ResponseWriter, *http.Request), *httptest.Server, int), opts *testOptions) {
  36. if opts == nil {
  37. opts = &testOptions{}
  38. }
  39. if opts.nbParrallelRequests == 0 {
  40. opts.nbParrallelRequests = 100
  41. }
  42. cwd, _ := os.Getwd()
  43. testDataDir := cwd + "/testdata/"
  44. if opts.logger == nil {
  45. opts.logger = zaptest.NewLogger(t)
  46. }
  47. initOpts := []frankenphp.Option{frankenphp.WithLogger(opts.logger)}
  48. if opts.workerScript != "" {
  49. initOpts = append(initOpts, frankenphp.WithWorkers(testDataDir+opts.workerScript, opts.nbWorkers, opts.env))
  50. }
  51. initOpts = append(initOpts, opts.initOpts...)
  52. err := frankenphp.Init(initOpts...)
  53. require.Nil(t, err)
  54. defer frankenphp.Shutdown()
  55. handler := func(w http.ResponseWriter, r *http.Request) {
  56. req, err := frankenphp.NewRequestWithContext(r, frankenphp.WithRequestDocumentRoot(testDataDir, false))
  57. assert.NoError(t, err)
  58. err = frankenphp.ServeHTTP(w, req)
  59. assert.NoError(t, err)
  60. }
  61. var ts *httptest.Server
  62. if opts.realServer {
  63. ts = httptest.NewServer(http.HandlerFunc(handler))
  64. defer ts.Close()
  65. }
  66. var wg sync.WaitGroup
  67. wg.Add(opts.nbParrallelRequests)
  68. for i := 0; i < opts.nbParrallelRequests; i++ {
  69. go func(i int) {
  70. test(handler, ts, i)
  71. wg.Done()
  72. }(i)
  73. }
  74. wg.Wait()
  75. }
  76. func TestHelloWorld_module(t *testing.T) { testHelloWorld(t, nil) }
  77. func TestHelloWorld_worker(t *testing.T) {
  78. testHelloWorld(t, &testOptions{workerScript: "index.php"})
  79. }
  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) {
  105. testServerVariable(t, nil)
  106. }
  107. func TestServerVariable_worker(t *testing.T) {
  108. testServerVariable(t, &testOptions{workerScript: "server-variable.php"})
  109. }
  110. func testServerVariable(t *testing.T, opts *testOptions) {
  111. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  112. req := httptest.NewRequest("POST", fmt.Sprintf("http://example.com/server-variable.php/baz/bat?foo=a&bar=b&i=%d#hash", i), strings.NewReader("foo"))
  113. req.SetBasicAuth("kevin", "password")
  114. req.Header.Add("Content-Type", "text/plain")
  115. w := httptest.NewRecorder()
  116. handler(w, req)
  117. resp := w.Result()
  118. body, _ := io.ReadAll(resp.Body)
  119. strBody := string(body)
  120. assert.Contains(t, strBody, "[REMOTE_HOST]")
  121. assert.Contains(t, strBody, "[REMOTE_USER] => kevin")
  122. assert.Contains(t, strBody, "[PHP_AUTH_USER] => kevin")
  123. assert.Contains(t, strBody, "[PHP_AUTH_PW] => password")
  124. assert.Contains(t, strBody, "[HTTP_AUTHORIZATION] => Basic a2V2aW46cGFzc3dvcmQ=")
  125. assert.Contains(t, strBody, "[DOCUMENT_ROOT]")
  126. assert.Contains(t, strBody, "[PHP_SELF] => /server-variable.php/baz/bat")
  127. assert.Contains(t, strBody, "[CONTENT_TYPE] => text/plain")
  128. assert.Contains(t, strBody, fmt.Sprintf("[QUERY_STRING] => foo=a&bar=b&i=%d#hash", i))
  129. assert.Contains(t, strBody, fmt.Sprintf("[REQUEST_URI] => /server-variable.php/baz/bat?foo=a&bar=b&i=%d#hash", i))
  130. assert.Contains(t, strBody, "[CONTENT_LENGTH]")
  131. assert.Contains(t, strBody, "[REMOTE_ADDR]")
  132. assert.Contains(t, strBody, "[REMOTE_PORT]")
  133. assert.Contains(t, strBody, "[REQUEST_SCHEME] => http")
  134. assert.Contains(t, strBody, "[DOCUMENT_URI]")
  135. assert.Contains(t, strBody, "[AUTH_TYPE]")
  136. assert.Contains(t, strBody, "[REMOTE_IDENT]")
  137. assert.Contains(t, strBody, "[REQUEST_METHOD] => POST")
  138. assert.Contains(t, strBody, "[SERVER_NAME] => example.com")
  139. assert.Contains(t, strBody, "[SERVER_PROTOCOL] => HTTP/1.1")
  140. assert.Contains(t, strBody, "[SCRIPT_FILENAME]")
  141. assert.Contains(t, strBody, "[SERVER_SOFTWARE] => FrankenPHP")
  142. assert.Contains(t, strBody, "[REQUEST_TIME_FLOAT]")
  143. assert.Contains(t, strBody, "[REQUEST_TIME]")
  144. assert.Contains(t, strBody, "[SERVER_PORT] => 80")
  145. }, opts)
  146. }
  147. func TestPathInfo_module(t *testing.T) { testPathInfo(t, nil) }
  148. func TestPathInfo_worker(t *testing.T) {
  149. testPathInfo(t, &testOptions{workerScript: "server-variable.php"})
  150. }
  151. func testPathInfo(t *testing.T, opts *testOptions) {
  152. runTest(t, func(_ func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  153. handler := func(w http.ResponseWriter, r *http.Request) {
  154. cwd, _ := os.Getwd()
  155. testDataDir := cwd + "/testdata/"
  156. requestURI := r.URL.RequestURI()
  157. r.URL.Path = "/server-variable.php/pathinfo"
  158. rewriteRequest, err := frankenphp.NewRequestWithContext(r,
  159. frankenphp.WithRequestDocumentRoot(testDataDir, false),
  160. frankenphp.WithRequestEnv(map[string]string{"REQUEST_URI": requestURI}),
  161. )
  162. assert.NoError(t, err)
  163. err = frankenphp.ServeHTTP(w, rewriteRequest)
  164. assert.NoError(t, err)
  165. }
  166. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/pathinfo/%d", i), nil)
  167. w := httptest.NewRecorder()
  168. handler(w, req)
  169. resp := w.Result()
  170. body, _ := io.ReadAll(resp.Body)
  171. strBody := string(body)
  172. assert.Contains(t, strBody, "[PATH_INFO] => /pathinfo")
  173. assert.Contains(t, strBody, fmt.Sprintf("[REQUEST_URI] => /pathinfo/%d", i))
  174. assert.Contains(t, strBody, "[PATH_TRANSLATED] =>")
  175. assert.Contains(t, strBody, "[SCRIPT_NAME] => /server-variable.php")
  176. }, opts)
  177. }
  178. func TestHeaders_module(t *testing.T) { testHeaders(t, nil) }
  179. func TestHeaders_worker(t *testing.T) { testHeaders(t, &testOptions{workerScript: "headers.php"}) }
  180. func testHeaders(t *testing.T, opts *testOptions) {
  181. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  182. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/headers.php?i=%d", i), nil)
  183. w := httptest.NewRecorder()
  184. handler(w, req)
  185. resp := w.Result()
  186. body, _ := io.ReadAll(resp.Body)
  187. assert.Equal(t, "Hello", string(body))
  188. assert.Equal(t, 201, resp.StatusCode)
  189. assert.Equal(t, "bar", resp.Header.Get("Foo"))
  190. assert.Equal(t, "bar2", resp.Header.Get("Foo2"))
  191. assert.Empty(t, resp.Header.Get("Invalid"))
  192. assert.Equal(t, fmt.Sprintf("%d", i), resp.Header.Get("I"))
  193. }, opts)
  194. }
  195. func TestResponseHeaders_module(t *testing.T) { testResponseHeaders(t, nil) }
  196. func TestResponseHeaders_worker(t *testing.T) {
  197. testResponseHeaders(t, &testOptions{workerScript: "response-headers.php"})
  198. }
  199. func testResponseHeaders(t *testing.T, opts *testOptions) {
  200. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  201. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/response-headers.php?i=%d", i), nil)
  202. w := httptest.NewRecorder()
  203. handler(w, req)
  204. resp := w.Result()
  205. body, _ := io.ReadAll(resp.Body)
  206. assert.Contains(t, string(body), "'X-Powered-By' => 'PH")
  207. assert.Contains(t, string(body), "'Foo' => 'bar',")
  208. assert.Contains(t, string(body), "'Foo2' => 'bar2',")
  209. assert.Contains(t, string(body), fmt.Sprintf("'I' => '%d',", i))
  210. assert.NotContains(t, string(body), "Invalid")
  211. }, opts)
  212. }
  213. func TestInput_module(t *testing.T) { testInput(t, nil) }
  214. func TestInput_worker(t *testing.T) { testInput(t, &testOptions{workerScript: "input.php"}) }
  215. func testInput(t *testing.T, opts *testOptions) {
  216. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  217. req := httptest.NewRequest("POST", "http://example.com/input.php", strings.NewReader(fmt.Sprintf("post data %d", i)))
  218. w := httptest.NewRecorder()
  219. handler(w, req)
  220. resp := w.Result()
  221. body, _ := io.ReadAll(resp.Body)
  222. assert.Equal(t, fmt.Sprintf("post data %d", i), string(body))
  223. assert.Equal(t, "bar", resp.Header.Get("Foo"))
  224. }, opts)
  225. }
  226. func TestPostSuperGlobals_module(t *testing.T) { testPostSuperGlobals(t, nil) }
  227. func TestPostSuperGlobals_worker(t *testing.T) {
  228. testPostSuperGlobals(t, &testOptions{workerScript: "super-globals.php"})
  229. }
  230. func testPostSuperGlobals(t *testing.T, opts *testOptions) {
  231. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  232. formData := url.Values{"baz": {"bat"}, "i": {fmt.Sprintf("%d", i)}}
  233. req := httptest.NewRequest("POST", fmt.Sprintf("http://example.com/super-globals.php?foo=bar&iG=%d", i), strings.NewReader(formData.Encode()))
  234. req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
  235. w := httptest.NewRecorder()
  236. handler(w, req)
  237. resp := w.Result()
  238. body, _ := io.ReadAll(resp.Body)
  239. assert.Contains(t, string(body), "'foo' => 'bar'")
  240. assert.Contains(t, string(body), fmt.Sprintf("'i' => '%d'", i))
  241. assert.Contains(t, string(body), "'baz' => 'bat'")
  242. assert.Contains(t, string(body), fmt.Sprintf("'iG' => '%d'", i))
  243. }, opts)
  244. }
  245. func TestCookies_module(t *testing.T) { testCookies(t, nil) }
  246. func TestCookies_worker(t *testing.T) { testCookies(t, &testOptions{workerScript: "cookies.php"}) }
  247. func testCookies(t *testing.T, opts *testOptions) {
  248. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  249. req := httptest.NewRequest("GET", "http://example.com/cookies.php", nil)
  250. req.AddCookie(&http.Cookie{Name: "foo", Value: "bar"})
  251. req.AddCookie(&http.Cookie{Name: "i", Value: fmt.Sprintf("%d", i)})
  252. w := httptest.NewRecorder()
  253. handler(w, req)
  254. resp := w.Result()
  255. body, _ := io.ReadAll(resp.Body)
  256. assert.Contains(t, string(body), "'foo' => 'bar'")
  257. assert.Contains(t, string(body), fmt.Sprintf("'i' => '%d'", i))
  258. }, opts)
  259. }
  260. func TestSession_module(t *testing.T) { testSession(t, nil) }
  261. func TestSession_worker(t *testing.T) {
  262. testSession(t, &testOptions{workerScript: "session.php"})
  263. }
  264. func testSession(t *testing.T, opts *testOptions) {
  265. if opts == nil {
  266. opts = &testOptions{}
  267. }
  268. opts.realServer = true
  269. runTest(t, func(_ func(http.ResponseWriter, *http.Request), ts *httptest.Server, i int) {
  270. jar, err := cookiejar.New(&cookiejar.Options{})
  271. assert.NoError(t, err)
  272. client := &http.Client{Jar: jar}
  273. resp1, err := client.Get(ts.URL + "/session.php")
  274. assert.NoError(t, err)
  275. body1, _ := io.ReadAll(resp1.Body)
  276. assert.Equal(t, "Count: 0\n", string(body1))
  277. resp2, err := client.Get(ts.URL + "/session.php")
  278. assert.NoError(t, err)
  279. body2, _ := io.ReadAll(resp2.Body)
  280. assert.Equal(t, "Count: 1\n", string(body2))
  281. }, opts)
  282. }
  283. func TestPhpInfo_module(t *testing.T) { testPhpInfo(t, nil) }
  284. func TestPhpInfo_worker(t *testing.T) { testPhpInfo(t, &testOptions{workerScript: "phpinfo.php"}) }
  285. func testPhpInfo(t *testing.T, opts *testOptions) {
  286. var logOnce sync.Once
  287. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  288. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/phpinfo.php?i=%d", i), nil)
  289. w := httptest.NewRecorder()
  290. handler(w, req)
  291. resp := w.Result()
  292. body, _ := io.ReadAll(resp.Body)
  293. logOnce.Do(func() {
  294. t.Log(string(body))
  295. })
  296. assert.Contains(t, string(body), "frankenphp")
  297. assert.Contains(t, string(body), fmt.Sprintf("i=%d", i))
  298. }, opts)
  299. }
  300. func TestPersistentObject_module(t *testing.T) { testPersistentObject(t, nil) }
  301. func TestPersistentObject_worker(t *testing.T) {
  302. testPersistentObject(t, &testOptions{workerScript: "persistent-object.php"})
  303. }
  304. func testPersistentObject(t *testing.T, opts *testOptions) {
  305. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  306. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/persistent-object.php?i=%d", i), nil)
  307. w := httptest.NewRecorder()
  308. handler(w, req)
  309. resp := w.Result()
  310. body, _ := io.ReadAll(resp.Body)
  311. assert.Equal(t, fmt.Sprintf(`request: %d
  312. class exists: 1
  313. id: obj1
  314. object id: 1`, i), string(body))
  315. }, opts)
  316. }
  317. func TestAutoloader_module(t *testing.T) { testAutoloader(t, nil) }
  318. func TestAutoloader_worker(t *testing.T) {
  319. testAutoloader(t, &testOptions{workerScript: "autoloader.php"})
  320. }
  321. func testAutoloader(t *testing.T, opts *testOptions) {
  322. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  323. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/autoloader.php?i=%d", i), nil)
  324. w := httptest.NewRecorder()
  325. handler(w, req)
  326. resp := w.Result()
  327. body, _ := io.ReadAll(resp.Body)
  328. assert.Equal(t, fmt.Sprintf(`request %d
  329. my_autoloader`, i), string(body))
  330. }, opts)
  331. }
  332. func TestLog_module(t *testing.T) { testLog(t, &testOptions{}) }
  333. func TestLog_worker(t *testing.T) {
  334. testLog(t, &testOptions{workerScript: "log.php"})
  335. }
  336. func testLog(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/log.php?i=%d", i), nil)
  341. w := httptest.NewRecorder()
  342. handler(w, req)
  343. for logs.FilterMessage(fmt.Sprintf("request %d", i)).Len() <= 0 {
  344. }
  345. }, opts)
  346. }
  347. func TestConnectionAbort_module(t *testing.T) { testConnectionAbort(t, &testOptions{}) }
  348. func TestConnectionAbort_worker(t *testing.T) {
  349. testConnectionAbort(t, &testOptions{workerScript: "connectionStatusLog.php"})
  350. }
  351. func testConnectionAbort(t *testing.T, opts *testOptions) {
  352. testFinish := func(finish string) {
  353. t.Run(fmt.Sprintf("finish=%s", finish), func(t *testing.T) {
  354. logger, logs := observer.New(zap.InfoLevel)
  355. opts.logger = zap.New(logger)
  356. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  357. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/connectionStatusLog.php?i=%d&finish=%s", i, finish), nil)
  358. w := httptest.NewRecorder()
  359. ctx, cancel := context.WithCancel(req.Context())
  360. req = req.WithContext(ctx)
  361. cancel()
  362. handler(w, req)
  363. for logs.FilterMessage(fmt.Sprintf("request %d: 1", i)).Len() <= 0 {
  364. }
  365. }, opts)
  366. })
  367. }
  368. testFinish("0")
  369. testFinish("1")
  370. }
  371. func TestException_module(t *testing.T) { testException(t, &testOptions{}) }
  372. func TestException_worker(t *testing.T) {
  373. testException(t, &testOptions{workerScript: "exception.php"})
  374. }
  375. func testException(t *testing.T, opts *testOptions) {
  376. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  377. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/exception.php?i=%d", i), nil)
  378. w := httptest.NewRecorder()
  379. handler(w, req)
  380. resp := w.Result()
  381. body, _ := io.ReadAll(resp.Body)
  382. assert.Contains(t, string(body), "hello")
  383. assert.Contains(t, string(body), fmt.Sprintf(`Uncaught Exception: request %d`, i))
  384. }, opts)
  385. }
  386. func TestEarlyHints_module(t *testing.T) { testEarlyHints(t, &testOptions{}) }
  387. func TestEarlyHints_worker(t *testing.T) {
  388. testEarlyHints(t, &testOptions{workerScript: "early-hints.php"})
  389. }
  390. func testEarlyHints(t *testing.T, opts *testOptions) {
  391. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  392. var earlyHintReceived bool
  393. trace := &httptrace.ClientTrace{
  394. Got1xxResponse: func(code int, header textproto.MIMEHeader) error {
  395. switch code {
  396. case http.StatusEarlyHints:
  397. assert.Equal(t, "</style.css>; rel=preload; as=style", header.Get("Link"))
  398. assert.Equal(t, strconv.Itoa(i), header.Get("Request"))
  399. earlyHintReceived = true
  400. }
  401. return nil
  402. },
  403. }
  404. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/early-hints.php?i=%d", i), nil)
  405. w := NewRecorder()
  406. w.ClientTrace = trace
  407. handler(w, req)
  408. assert.Equal(t, strconv.Itoa(i), w.Header().Get("Request"))
  409. assert.Equal(t, "", w.Header().Get("Link"))
  410. assert.True(t, earlyHintReceived)
  411. }, opts)
  412. }
  413. type streamResponseRecorder struct {
  414. *httptest.ResponseRecorder
  415. writeCallback func(buf []byte)
  416. }
  417. func (srr *streamResponseRecorder) Write(buf []byte) (int, error) {
  418. srr.writeCallback(buf)
  419. return srr.ResponseRecorder.Write(buf)
  420. }
  421. func TestFlush_module(t *testing.T) { testFlush(t, &testOptions{}) }
  422. func TestFlush_worker(t *testing.T) {
  423. testFlush(t, &testOptions{workerScript: "flush.php"})
  424. }
  425. func testFlush(t *testing.T, opts *testOptions) {
  426. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  427. var j int
  428. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/flush.php?i=%d", i), nil)
  429. w := &streamResponseRecorder{httptest.NewRecorder(), func(buf []byte) {
  430. if j == 0 {
  431. assert.Equal(t, []byte("He"), buf)
  432. } else {
  433. assert.Equal(t, []byte(fmt.Sprintf("llo %d", i)), buf)
  434. }
  435. j++
  436. }}
  437. handler(w, req)
  438. assert.Equal(t, 2, j)
  439. }, opts)
  440. }
  441. func TestLargeRequest_module(t *testing.T) {
  442. testLargeRequest(t, &testOptions{})
  443. }
  444. func TestLargeRequest_worker(t *testing.T) {
  445. testLargeRequest(t, &testOptions{workerScript: "large-request.php"})
  446. }
  447. func testLargeRequest(t *testing.T, opts *testOptions) {
  448. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  449. req := httptest.NewRequest(
  450. "POST",
  451. fmt.Sprintf("http://example.com/large-request.php?i=%d", i),
  452. strings.NewReader(strings.Repeat("f", 1_048_576)),
  453. )
  454. w := httptest.NewRecorder()
  455. handler(w, req)
  456. resp := w.Result()
  457. body, _ := io.ReadAll(resp.Body)
  458. assert.Contains(t, string(body), fmt.Sprintf("Request body size: 1048576 (%d)", i))
  459. }, opts)
  460. }
  461. func TestVersion(t *testing.T) {
  462. v := frankenphp.Version()
  463. assert.GreaterOrEqual(t, v.MajorVersion, 8)
  464. assert.GreaterOrEqual(t, v.MinorVersion, 0)
  465. assert.GreaterOrEqual(t, v.ReleaseVersion, 0)
  466. assert.GreaterOrEqual(t, v.VersionID, 0)
  467. assert.NotEmpty(t, v.Version, 0)
  468. }
  469. func TestFiberNoCgo_module(t *testing.T) { testFiberNoCgo(t, &testOptions{}) }
  470. func TestFiberNonCgo_worker(t *testing.T) {
  471. testFiberNoCgo(t, &testOptions{workerScript: "fiber-no-cgo.php"})
  472. }
  473. func testFiberNoCgo(t *testing.T, opts *testOptions) {
  474. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  475. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/fiber-no-cgo.php?i=%d", i), nil)
  476. w := httptest.NewRecorder()
  477. handler(w, req)
  478. resp := w.Result()
  479. body, _ := io.ReadAll(resp.Body)
  480. assert.Equal(t, string(body), fmt.Sprintf("Fiber %d", i))
  481. }, opts)
  482. }
  483. func TestRequestHeaders_module(t *testing.T) { testRequestHeaders(t, &testOptions{}) }
  484. func TestRequestHeaders_worker(t *testing.T) {
  485. testRequestHeaders(t, &testOptions{workerScript: "request-headers.php"})
  486. }
  487. func testRequestHeaders(t *testing.T, opts *testOptions) {
  488. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  489. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/request-headers.php?i=%d", i), nil)
  490. req.Header.Add(strings.Clone("Content-Type"), strings.Clone("text/plain"))
  491. req.Header.Add(strings.Clone("Frankenphp-I"), strings.Clone(strconv.Itoa(i)))
  492. w := httptest.NewRecorder()
  493. handler(w, req)
  494. resp := w.Result()
  495. body, _ := io.ReadAll(resp.Body)
  496. assert.Contains(t, string(body), "[Content-Type] => text/plain")
  497. assert.Contains(t, string(body), fmt.Sprintf("[Frankenphp-I] => %d", i))
  498. }, opts)
  499. }
  500. func TestExecuteScriptCLI(t *testing.T) {
  501. if _, err := os.Stat("internal/testcli/testcli"); err != nil {
  502. t.Skip("internal/testcli/testcli has not been compiled, run `cd internal/testcli/ && go build`")
  503. }
  504. cmd := exec.Command("internal/testcli/testcli", "testdata/command.php", "foo", "bar")
  505. stdoutStderr, err := cmd.CombinedOutput()
  506. assert.Error(t, err)
  507. if exitError, ok := err.(*exec.ExitError); ok {
  508. assert.Equal(t, 3, exitError.ExitCode())
  509. }
  510. stdoutStderrStr := string(stdoutStderr)
  511. assert.Contains(t, stdoutStderrStr, `"foo"`)
  512. assert.Contains(t, stdoutStderrStr, `"bar"`)
  513. assert.Contains(t, stdoutStderrStr, "From the CLI")
  514. }
  515. func ExampleServeHTTP() {
  516. if err := frankenphp.Init(); err != nil {
  517. panic(err)
  518. }
  519. defer frankenphp.Shutdown()
  520. http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  521. req, err := frankenphp.NewRequestWithContext(r, frankenphp.WithRequestDocumentRoot("/path/to/document/root", false))
  522. if err != nil {
  523. panic(err)
  524. }
  525. if err := frankenphp.ServeHTTP(w, req); err != nil {
  526. panic(err)
  527. }
  528. })
  529. log.Fatal(http.ListenAndServe(":8080", nil))
  530. }
  531. func ExampleExecuteScriptCLI() {
  532. if len(os.Args) <= 1 {
  533. log.Println("Usage: my-program script.php")
  534. os.Exit(1)
  535. }
  536. os.Exit(frankenphp.ExecuteScriptCLI(os.Args[1], os.Args))
  537. }
  538. func BenchmarkHelloWorld(b *testing.B) {
  539. if err := frankenphp.Init(frankenphp.WithLogger(zap.NewNop())); err != nil {
  540. panic(err)
  541. }
  542. defer frankenphp.Shutdown()
  543. cwd, _ := os.Getwd()
  544. testDataDir := cwd + "/testdata/"
  545. handler := func(w http.ResponseWriter, r *http.Request) {
  546. req, err := frankenphp.NewRequestWithContext(r, frankenphp.WithRequestDocumentRoot(testDataDir, false))
  547. if err != nil {
  548. panic(err)
  549. }
  550. if err := frankenphp.ServeHTTP(w, req); err != nil {
  551. panic(err)
  552. }
  553. }
  554. req := httptest.NewRequest("GET", "http://example.com/index.php", nil)
  555. w := httptest.NewRecorder()
  556. for i := 0; i < b.N; i++ {
  557. handler(w, req)
  558. }
  559. }
  560. func BenchmarkEcho(b *testing.B) {
  561. if err := frankenphp.Init(frankenphp.WithLogger(zap.NewNop())); err != nil {
  562. panic(err)
  563. }
  564. defer frankenphp.Shutdown()
  565. cwd, _ := os.Getwd()
  566. testDataDir := cwd + "/testdata/"
  567. handler := func(w http.ResponseWriter, r *http.Request) {
  568. req, err := frankenphp.NewRequestWithContext(r, frankenphp.WithRequestDocumentRoot(testDataDir, false))
  569. if err != nil {
  570. panic(err)
  571. }
  572. if err := frankenphp.ServeHTTP(w, req); err != nil {
  573. panic(err)
  574. }
  575. }
  576. const body = `{
  577. "squadName": "Super hero squad",
  578. "homeTown": "Metro City",
  579. "formed": 2016,
  580. "secretBase": "Super tower",
  581. "active": true,
  582. "members": [
  583. {
  584. "name": "Molecule Man",
  585. "age": 29,
  586. "secretIdentity": "Dan Jukes",
  587. "powers": ["Radiation resistance", "Turning tiny", "Radiation blast"]
  588. },
  589. {
  590. "name": "Madame Uppercut",
  591. "age": 39,
  592. "secretIdentity": "Jane Wilson",
  593. "powers": [
  594. "Million tonne punch",
  595. "Damage resistance",
  596. "Superhuman reflexes"
  597. ]
  598. },
  599. {
  600. "name": "Eternal Flame",
  601. "age": 1000000,
  602. "secretIdentity": "Unknown",
  603. "powers": [
  604. "Immortality",
  605. "Heat Immunity",
  606. "Inferno",
  607. "Teleportation",
  608. "Interdimensional travel"
  609. ]
  610. }
  611. ]
  612. }`
  613. r := strings.NewReader(body)
  614. req := httptest.NewRequest("POST", "http://example.com/echo.php", r)
  615. w := httptest.NewRecorder()
  616. for i := 0; i < b.N; i++ {
  617. r.Reset(body)
  618. handler(w, req)
  619. }
  620. }