frankenphp_test.go 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695
  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 TestInput_module(t *testing.T) { testInput(t, nil) }
  196. func TestInput_worker(t *testing.T) { testInput(t, &testOptions{workerScript: "input.php"}) }
  197. func testInput(t *testing.T, opts *testOptions) {
  198. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  199. req := httptest.NewRequest("POST", "http://example.com/input.php", strings.NewReader(fmt.Sprintf("post data %d", i)))
  200. w := httptest.NewRecorder()
  201. handler(w, req)
  202. resp := w.Result()
  203. body, _ := io.ReadAll(resp.Body)
  204. assert.Equal(t, fmt.Sprintf("post data %d", i), string(body))
  205. assert.Equal(t, "bar", resp.Header.Get("Foo"))
  206. }, opts)
  207. }
  208. func TestPostSuperGlobals_module(t *testing.T) { testPostSuperGlobals(t, nil) }
  209. func TestPostSuperGlobals_worker(t *testing.T) {
  210. testPostSuperGlobals(t, &testOptions{workerScript: "super-globals.php"})
  211. }
  212. func testPostSuperGlobals(t *testing.T, opts *testOptions) {
  213. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  214. formData := url.Values{"baz": {"bat"}, "i": {fmt.Sprintf("%d", i)}}
  215. req := httptest.NewRequest("POST", fmt.Sprintf("http://example.com/super-globals.php?foo=bar&iG=%d", i), strings.NewReader(formData.Encode()))
  216. req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
  217. w := httptest.NewRecorder()
  218. handler(w, req)
  219. resp := w.Result()
  220. body, _ := io.ReadAll(resp.Body)
  221. assert.Contains(t, string(body), "'foo' => 'bar'")
  222. assert.Contains(t, string(body), fmt.Sprintf("'i' => '%d'", i))
  223. assert.Contains(t, string(body), "'baz' => 'bat'")
  224. assert.Contains(t, string(body), fmt.Sprintf("'iG' => '%d'", i))
  225. }, opts)
  226. }
  227. func TestCookies_module(t *testing.T) { testCookies(t, nil) }
  228. func TestCookies_worker(t *testing.T) { testCookies(t, &testOptions{workerScript: "cookies.php"}) }
  229. func testCookies(t *testing.T, opts *testOptions) {
  230. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  231. req := httptest.NewRequest("GET", "http://example.com/cookies.php", nil)
  232. req.AddCookie(&http.Cookie{Name: "foo", Value: "bar"})
  233. req.AddCookie(&http.Cookie{Name: "i", Value: fmt.Sprintf("%d", i)})
  234. w := httptest.NewRecorder()
  235. handler(w, req)
  236. resp := w.Result()
  237. body, _ := io.ReadAll(resp.Body)
  238. assert.Contains(t, string(body), "'foo' => 'bar'")
  239. assert.Contains(t, string(body), fmt.Sprintf("'i' => '%d'", i))
  240. }, opts)
  241. }
  242. func TestSession_module(t *testing.T) { testSession(t, nil) }
  243. func TestSession_worker(t *testing.T) {
  244. testSession(t, &testOptions{workerScript: "session.php"})
  245. }
  246. func testSession(t *testing.T, opts *testOptions) {
  247. if opts == nil {
  248. opts = &testOptions{}
  249. }
  250. opts.realServer = true
  251. runTest(t, func(_ func(http.ResponseWriter, *http.Request), ts *httptest.Server, i int) {
  252. jar, err := cookiejar.New(&cookiejar.Options{})
  253. assert.NoError(t, err)
  254. client := &http.Client{Jar: jar}
  255. resp1, err := client.Get(ts.URL + "/session.php")
  256. assert.NoError(t, err)
  257. body1, _ := io.ReadAll(resp1.Body)
  258. assert.Equal(t, "Count: 0\n", string(body1))
  259. resp2, err := client.Get(ts.URL + "/session.php")
  260. assert.NoError(t, err)
  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. var logOnce sync.Once
  269. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  270. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/phpinfo.php?i=%d", i), nil)
  271. w := httptest.NewRecorder()
  272. handler(w, req)
  273. resp := w.Result()
  274. body, _ := io.ReadAll(resp.Body)
  275. logOnce.Do(func() {
  276. t.Log(string(body))
  277. })
  278. assert.Contains(t, string(body), "frankenphp")
  279. assert.Contains(t, string(body), fmt.Sprintf("i=%d", i))
  280. }, opts)
  281. }
  282. func TestPersistentObject_module(t *testing.T) { testPersistentObject(t, nil) }
  283. func TestPersistentObject_worker(t *testing.T) {
  284. testPersistentObject(t, &testOptions{workerScript: "persistent-object.php"})
  285. }
  286. func testPersistentObject(t *testing.T, opts *testOptions) {
  287. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  288. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/persistent-object.php?i=%d", i), nil)
  289. w := httptest.NewRecorder()
  290. handler(w, req)
  291. resp := w.Result()
  292. body, _ := io.ReadAll(resp.Body)
  293. assert.Equal(t, fmt.Sprintf(`request: %d
  294. class exists: 1
  295. id: obj1
  296. object id: 1`, i), string(body))
  297. }, opts)
  298. }
  299. func TestAutoloader_module(t *testing.T) { testAutoloader(t, nil) }
  300. func TestAutoloader_worker(t *testing.T) {
  301. testAutoloader(t, &testOptions{workerScript: "autoloader.php"})
  302. }
  303. func testAutoloader(t *testing.T, opts *testOptions) {
  304. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  305. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/autoloader.php?i=%d", i), nil)
  306. w := httptest.NewRecorder()
  307. handler(w, req)
  308. resp := w.Result()
  309. body, _ := io.ReadAll(resp.Body)
  310. assert.Equal(t, fmt.Sprintf(`request %d
  311. my_autoloader`, i), string(body))
  312. }, opts)
  313. }
  314. func TestLog_module(t *testing.T) { testLog(t, &testOptions{}) }
  315. func TestLog_worker(t *testing.T) {
  316. testLog(t, &testOptions{workerScript: "log.php"})
  317. }
  318. func testLog(t *testing.T, opts *testOptions) {
  319. logger, logs := observer.New(zap.InfoLevel)
  320. opts.logger = zap.New(logger)
  321. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  322. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/log.php?i=%d", i), nil)
  323. w := httptest.NewRecorder()
  324. handler(w, req)
  325. for logs.FilterMessage(fmt.Sprintf("request %d", i)).Len() <= 0 {
  326. }
  327. }, opts)
  328. }
  329. func TestConnectionAbort_module(t *testing.T) { testConnectionAbort(t, &testOptions{}) }
  330. func TestConnectionAbort_worker(t *testing.T) {
  331. testConnectionAbort(t, &testOptions{workerScript: "connectionStatusLog.php"})
  332. }
  333. func testConnectionAbort(t *testing.T, opts *testOptions) {
  334. testFinish := func(finish string) {
  335. t.Run(fmt.Sprintf("finish=%s", finish), func(t *testing.T) {
  336. logger, logs := observer.New(zap.InfoLevel)
  337. opts.logger = zap.New(logger)
  338. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  339. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/connectionStatusLog.php?i=%d&finish=%s", i, finish), nil)
  340. w := httptest.NewRecorder()
  341. ctx, cancel := context.WithCancel(req.Context())
  342. req = req.WithContext(ctx)
  343. cancel()
  344. handler(w, req)
  345. for logs.FilterMessage(fmt.Sprintf("request %d: 1", i)).Len() <= 0 {
  346. }
  347. }, opts)
  348. })
  349. }
  350. testFinish("0")
  351. testFinish("1")
  352. }
  353. func TestException_module(t *testing.T) { testException(t, &testOptions{}) }
  354. func TestException_worker(t *testing.T) {
  355. testException(t, &testOptions{workerScript: "exception.php"})
  356. }
  357. func testException(t *testing.T, opts *testOptions) {
  358. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  359. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/exception.php?i=%d", i), nil)
  360. w := httptest.NewRecorder()
  361. handler(w, req)
  362. resp := w.Result()
  363. body, _ := io.ReadAll(resp.Body)
  364. assert.Contains(t, string(body), "hello")
  365. assert.Contains(t, string(body), fmt.Sprintf(`Uncaught Exception: request %d`, i))
  366. }, opts)
  367. }
  368. func TestEarlyHints_module(t *testing.T) { testEarlyHints(t, &testOptions{}) }
  369. func TestEarlyHints_worker(t *testing.T) {
  370. testEarlyHints(t, &testOptions{workerScript: "early-hints.php"})
  371. }
  372. func testEarlyHints(t *testing.T, opts *testOptions) {
  373. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  374. var earlyHintReceived bool
  375. trace := &httptrace.ClientTrace{
  376. Got1xxResponse: func(code int, header textproto.MIMEHeader) error {
  377. switch code {
  378. case http.StatusEarlyHints:
  379. assert.Equal(t, "</style.css>; rel=preload; as=style", header.Get("Link"))
  380. assert.Equal(t, strconv.Itoa(i), header.Get("Request"))
  381. earlyHintReceived = true
  382. }
  383. return nil
  384. },
  385. }
  386. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/early-hints.php?i=%d", i), nil)
  387. w := NewRecorder()
  388. w.ClientTrace = trace
  389. handler(w, req)
  390. assert.Equal(t, strconv.Itoa(i), w.Header().Get("Request"))
  391. assert.Equal(t, "", w.Header().Get("Link"))
  392. assert.True(t, earlyHintReceived)
  393. }, opts)
  394. }
  395. type streamResponseRecorder struct {
  396. *httptest.ResponseRecorder
  397. writeCallback func(buf []byte)
  398. }
  399. func (srr *streamResponseRecorder) Write(buf []byte) (int, error) {
  400. srr.writeCallback(buf)
  401. return srr.ResponseRecorder.Write(buf)
  402. }
  403. func TestFlush_module(t *testing.T) { testFlush(t, &testOptions{}) }
  404. func TestFlush_worker(t *testing.T) {
  405. testFlush(t, &testOptions{workerScript: "flush.php"})
  406. }
  407. func testFlush(t *testing.T, opts *testOptions) {
  408. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  409. var j int
  410. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/flush.php?i=%d", i), nil)
  411. w := &streamResponseRecorder{httptest.NewRecorder(), func(buf []byte) {
  412. if j == 0 {
  413. assert.Equal(t, []byte("He"), buf)
  414. } else {
  415. assert.Equal(t, []byte(fmt.Sprintf("llo %d", i)), buf)
  416. }
  417. j++
  418. }}
  419. handler(w, req)
  420. assert.Equal(t, 2, j)
  421. }, opts)
  422. }
  423. func TestLargeRequest_module(t *testing.T) {
  424. testLargeRequest(t, &testOptions{})
  425. }
  426. func TestLargeRequest_worker(t *testing.T) {
  427. testLargeRequest(t, &testOptions{workerScript: "large-request.php"})
  428. }
  429. func testLargeRequest(t *testing.T, opts *testOptions) {
  430. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  431. req := httptest.NewRequest(
  432. "POST",
  433. fmt.Sprintf("http://example.com/large-request.php?i=%d", i),
  434. strings.NewReader(strings.Repeat("f", 1_048_576)),
  435. )
  436. w := httptest.NewRecorder()
  437. handler(w, req)
  438. resp := w.Result()
  439. body, _ := io.ReadAll(resp.Body)
  440. assert.Contains(t, string(body), fmt.Sprintf("Request body size: 1048576 (%d)", i))
  441. }, opts)
  442. }
  443. func TestVersion(t *testing.T) {
  444. v := frankenphp.Version()
  445. assert.GreaterOrEqual(t, v.MajorVersion, 8)
  446. assert.GreaterOrEqual(t, v.MinorVersion, 0)
  447. assert.GreaterOrEqual(t, v.ReleaseVersion, 0)
  448. assert.GreaterOrEqual(t, v.VersionID, 0)
  449. assert.NotEmpty(t, v.Version, 0)
  450. }
  451. func TestFiberNoCgo_module(t *testing.T) { testFiberNoCgo(t, &testOptions{}) }
  452. func TestFiberNonCgo_worker(t *testing.T) {
  453. testFiberNoCgo(t, &testOptions{workerScript: "fiber-no-cgo.php"})
  454. }
  455. func testFiberNoCgo(t *testing.T, opts *testOptions) {
  456. runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
  457. req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/fiber-no-cgo.php?i=%d", i), nil)
  458. w := httptest.NewRecorder()
  459. handler(w, req)
  460. resp := w.Result()
  461. body, _ := io.ReadAll(resp.Body)
  462. assert.Equal(t, string(body), fmt.Sprintf("Fiber %d", i))
  463. }, opts)
  464. }
  465. func TestExecuteScriptCLI(t *testing.T) {
  466. if _, err := os.Stat("internal/testcli/testcli"); err != nil {
  467. t.Skip("internal/testcli/testcli has not been compiled, run `cd internal/testcli/ && go build`")
  468. }
  469. cmd := exec.Command("internal/testcli/testcli", "testdata/command.php", "foo", "bar")
  470. stdoutStderr, err := cmd.CombinedOutput()
  471. assert.Error(t, err)
  472. if exitError, ok := err.(*exec.ExitError); ok {
  473. assert.Equal(t, 3, exitError.ExitCode())
  474. }
  475. stdoutStderrStr := string(stdoutStderr)
  476. assert.Contains(t, stdoutStderrStr, `"foo"`)
  477. assert.Contains(t, stdoutStderrStr, `"bar"`)
  478. assert.Contains(t, stdoutStderrStr, "From the CLI")
  479. }
  480. func ExampleServeHTTP() {
  481. if err := frankenphp.Init(); err != nil {
  482. panic(err)
  483. }
  484. defer frankenphp.Shutdown()
  485. http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  486. req, err := frankenphp.NewRequestWithContext(r, frankenphp.WithRequestDocumentRoot("/path/to/document/root", false))
  487. if err != nil {
  488. panic(err)
  489. }
  490. if err := frankenphp.ServeHTTP(w, req); err != nil {
  491. panic(err)
  492. }
  493. })
  494. log.Fatal(http.ListenAndServe(":8080", nil))
  495. }
  496. func ExampleExecuteScriptCLI() {
  497. if len(os.Args) <= 1 {
  498. log.Println("Usage: my-program script.php")
  499. os.Exit(1)
  500. }
  501. os.Exit(frankenphp.ExecuteScriptCLI(os.Args[1], os.Args))
  502. }
  503. func BenchmarkHelloWorld(b *testing.B) {
  504. if err := frankenphp.Init(frankenphp.WithLogger(zap.NewNop())); err != nil {
  505. panic(err)
  506. }
  507. defer frankenphp.Shutdown()
  508. cwd, _ := os.Getwd()
  509. testDataDir := cwd + "/testdata/"
  510. handler := func(w http.ResponseWriter, r *http.Request) {
  511. req, err := frankenphp.NewRequestWithContext(r, frankenphp.WithRequestDocumentRoot(testDataDir, false))
  512. if err != nil {
  513. panic(err)
  514. }
  515. if err := frankenphp.ServeHTTP(w, req); err != nil {
  516. panic(err)
  517. }
  518. }
  519. req := httptest.NewRequest("GET", "http://example.com/index.php", nil)
  520. w := httptest.NewRecorder()
  521. for i := 0; i < b.N; i++ {
  522. handler(w, req)
  523. }
  524. }
  525. func BenchmarkEcho(b *testing.B) {
  526. if err := frankenphp.Init(frankenphp.WithLogger(zap.NewNop())); err != nil {
  527. panic(err)
  528. }
  529. defer frankenphp.Shutdown()
  530. cwd, _ := os.Getwd()
  531. testDataDir := cwd + "/testdata/"
  532. handler := func(w http.ResponseWriter, r *http.Request) {
  533. req, err := frankenphp.NewRequestWithContext(r, frankenphp.WithRequestDocumentRoot(testDataDir, false))
  534. if err != nil {
  535. panic(err)
  536. }
  537. if err := frankenphp.ServeHTTP(w, req); err != nil {
  538. panic(err)
  539. }
  540. }
  541. const body = `{
  542. "squadName": "Super hero squad",
  543. "homeTown": "Metro City",
  544. "formed": 2016,
  545. "secretBase": "Super tower",
  546. "active": true,
  547. "members": [
  548. {
  549. "name": "Molecule Man",
  550. "age": 29,
  551. "secretIdentity": "Dan Jukes",
  552. "powers": ["Radiation resistance", "Turning tiny", "Radiation blast"]
  553. },
  554. {
  555. "name": "Madame Uppercut",
  556. "age": 39,
  557. "secretIdentity": "Jane Wilson",
  558. "powers": [
  559. "Million tonne punch",
  560. "Damage resistance",
  561. "Superhuman reflexes"
  562. ]
  563. },
  564. {
  565. "name": "Eternal Flame",
  566. "age": 1000000,
  567. "secretIdentity": "Unknown",
  568. "powers": [
  569. "Immortality",
  570. "Heat Immunity",
  571. "Inferno",
  572. "Teleportation",
  573. "Interdimensional travel"
  574. ]
  575. }
  576. ]
  577. }`
  578. r := strings.NewReader(body)
  579. req := httptest.NewRequest("POST", "http://example.com/echo.php", r)
  580. w := httptest.NewRecorder()
  581. for i := 0; i < b.N; i++ {
  582. r.Reset(body)
  583. handler(w, req)
  584. }
  585. }