تست فرمهای HTML
در این فصل میخواهیم یک تست end-to-end برای route POST /user/signup اضافه کنیم که توسط handler userSignupPost ما مدیریت میشود.
تست این route به دلیل بررسی ضد CSRF که برنامه ما انجام میدهد کمی پیچیدهتر میشود. هر درخواستی که به POST /user/signup میدهیم همیشه یک پاسخ 400 Bad Request دریافت میکند مگر اینکه درخواست شامل یک CSRF token و cookie معتبر باشد. برای دور زدن این، باید workflow یک کاربر واقعی را به عنوان بخشی از تست خود شبیهسازی کنیم، به این شکل:
یک درخواست
GET /user/signupارسال کنید. این یک پاسخ برمیگرداند که شامل یک CSRF cookie در headerهای پاسخ و CSRF token برای صفحه signup در بدنه پاسخ است.CSRF token را از بدنه پاسخ HTML استخراج کنید.
یک درخواست
POST /user/signupارسال کنید، با استفاده از همانhttp.Clientکه در مرحله 1 استفاده کردیم (تا به طور خودکار CSRF cookie را با درخواستPOSTارسال کند) و شامل کردن CSRF token در کنار دادههایPOSTدیگری که میخواهیم تست کنیم.
بیایید با افزودن یک تابع helper جدید به فایل cmd/web/testutils_test.go برای استخراج CSRF token (در صورت وجود) از بدنه پاسخ HTML شروع کنیم:
package main import ( "bytes" "html" // New import "io" "log/slog" "net/http" "net/http/cookiejar" "net/http/httptest" "regexp" // New import "testing" "time" "snippetbox.alexedwards.net/internal/models/mocks" "github.com/alexedwards/scs/v2" "github.com/go-playground/form/v4" ) // Define a regular expression which captures the CSRF token value from the // HTML for our user signup page. var csrfTokenRX = regexp.MustCompile(`<input type='hidden' name='csrf_token' value='(.+)'>`) func extractCSRFToken(t *testing.T, body string) string { // Use the FindStringSubmatch method to extract the token from the HTML body. // Note that this returns an array with the entire matched pattern in the // first position, and the values of any captured data in the subsequent // positions. matches := csrfTokenRX.FindStringSubmatch(body) if len(matches) < 2 { t.Fatal("no csrf token found in body") } return html.UnescapeString(matches[1]) } ...
حالا که این در جای خود قرار دارد، بیایید به فایل cmd/web/handlers_test.go برگردیم و یک تست جدید TestUserSignup ایجاد کنیم.
برای شروع، این را طوری میسازیم که یک درخواست GET /user/signup انجام دهد و سپس CSRF token را از بدنه پاسخ HTML استخراج و چاپ کند. به این شکل:
package main ... func TestUserSignup(t *testing.T) { // Create the application struct containing our mocked dependencies and set // up the test server for running an end-to-end test. app := newTestApplication(t) ts := newTestServer(t, app.routes()) defer ts.Close() // Make a GET /user/signup request and then extract the CSRF token from the // response body. _, _, body := ts.get(t, "/user/signup") csrfToken := extractCSRFToken(t, body) // Log the CSRF token value in our test output using the t.Logf() function. // The t.Logf() function works in the same way as fmt.Printf(), but writes // the provided message to the test output. t.Logf("CSRF token is: %q", csrfToken) }
مهم است که باید تستها را با flag -v (برای فعال کردن خروجی verbose) اجرا کنید تا هر خروجی از تابع t.Logf() را ببینید.
بیایید همین حالا این کار را انجام دهیم:
$ go test -v -run="TestUserSignup" ./cmd/web/
=== RUN TestUserSignup
handlers_test.go:81: CSRF token is: "C92tcpQpL1n6aIUaF8XAonwy+YjcVnyaAaOvfkdl6vJqoNSbgaTtdBRC61pFMoGP2ojV+sZ1d0SUikah3mfREQ=="
--- PASS: TestUserSignup (0.01s)
PASS
ok snippetbox.alexedwards.net/cmd/web 0.010s
خوب، به نظر میرسد که کار میکند. تست بدون هیچ مشکلی اجرا میشود و CSRF token را که از بدنه پاسخ HTML استخراج کردهایم چاپ میکند.
تست درخواستهای POST
حالا بیایید به فایل cmd/web/testutils_test.go برگردیم و یک متد جدید postForm() روی نوع testServer ایجاد کنیم که میتوانیم از آن برای ارسال یک درخواست POST به سرور تست با دادههای فرم خاص در بدنه درخواست استفاده کنیم.
پیش بروید و کد زیر را اضافه کنید (که همان الگوی کلی را دنبال میکند که برای متد get() قبلاً در کتاب استفاده کردیم):
package main import ( "bytes" "html" "io" "log/slog" "net/http" "net/http/cookiejar" "net/http/httptest" "net/url" // New import "regexp" "testing" "time" "snippetbox.alexedwards.net/internal/models/mocks" "github.com/alexedwards/scs/v2" "github.com/go-playground/form/v4" ) ... // Create a postForm method for sending POST requests to the test server. The // final parameter to this method is a url.Values object which can contain any // form data that you want to send in the request body. func (ts *testServer) postForm(t *testing.T, urlPath string, form url.Values) (int, http.Header, string) { rs, err := ts.Client().PostForm(ts.URL+urlPath, form) if err != nil { t.Fatal(err) } // Read the response body from the test server. defer rs.Body.Close() body, err := io.ReadAll(rs.Body) if err != nil { t.Fatal(err) } body = bytes.TrimSpace(body) // Return the response status, headers and body. return rs.StatusCode, rs.Header, string(body) }
و حالا، در نهایت، آمادهایم که چند sub-test مبتنی بر جدول (table-driven) برای تست رفتار route POST /user/signup برنامه خود اضافه کنیم. به طور خاص، میخواهیم تست کنیم که:
- یک signup معتبر منجر به پاسخ
303 See Otherمیشود. - یک ارسال فرم بدون CSRF token معتبر منجر به پاسخ
400 Bad Requestمیشود. - یک ارسال فرم نامعتبر منجر به پاسخ
422 Unprocessable Entityمیشود و فرم signup دوباره نمایش داده میشود. این باید زمانی اتفاق بیفتد که:- فیلدهای name، email یا password خالی باشند.
- ایمیل در فرمت معتبر نباشد.
- رمز عبور کمتر از 8 کاراکتر باشد.
- آدرس ایمیل قبلاً استفاده شده باشد.
پیش بروید و تابع TestUserSignup را برای انجام این تستها به این شکل بهروزرسانی کنید:
package main import ( "net/http" "net/url" // New import "testing" "snippetbox.alexedwards.net/internal/assert" ) ... func TestUserSignup(t *testing.T) { app := newTestApplication(t) ts := newTestServer(t, app.routes()) defer ts.Close() _, _, body := ts.get(t, "/user/signup") validCSRFToken := extractCSRFToken(t, body) const ( validName = "Bob" validPassword = "validPa$$word" validEmail = "bob@example.com" formTag = "<form action='/user/signup' method='POST' novalidate>" ) tests := []struct { name string userName string userEmail string userPassword string csrfToken string wantCode int wantFormTag string }{ { name: "Valid submission", userName: validName, userEmail: validEmail, userPassword: validPassword, csrfToken: validCSRFToken, wantCode: http.StatusSeeOther, }, { name: "Invalid CSRF Token", userName: validName, userEmail: validEmail, userPassword: validPassword, csrfToken: "wrongToken", wantCode: http.StatusBadRequest, }, { name: "Empty name", userName: "", userEmail: validEmail, userPassword: validPassword, csrfToken: validCSRFToken, wantCode: http.StatusUnprocessableEntity, wantFormTag: formTag, }, { name: "Empty email", userName: validName, userEmail: "", userPassword: validPassword, csrfToken: validCSRFToken, wantCode: http.StatusUnprocessableEntity, wantFormTag: formTag, }, { name: "Empty password", userName: validName, userEmail: validEmail, userPassword: "", csrfToken: validCSRFToken, wantCode: http.StatusUnprocessableEntity, wantFormTag: formTag, }, { name: "Invalid email", userName: validName, userEmail: "bob@example.", userPassword: validPassword, csrfToken: validCSRFToken, wantCode: http.StatusUnprocessableEntity, wantFormTag: formTag, }, { name: "Short password", userName: validName, userEmail: validEmail, userPassword: "pa$$", csrfToken: validCSRFToken, wantCode: http.StatusUnprocessableEntity, wantFormTag: formTag, }, { name: "Duplicate email", userName: validName, userEmail: "dupe@example.com", userPassword: validPassword, csrfToken: validCSRFToken, wantCode: http.StatusUnprocessableEntity, wantFormTag: formTag, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { form := url.Values{} form.Add("name", tt.userName) form.Add("email", tt.userEmail) form.Add("password", tt.userPassword) form.Add("csrf_token", tt.csrfToken) code, _, body := ts.postForm(t, "/user/signup", form) assert.Equal(t, code, tt.wantCode) if tt.wantFormTag != "" { assert.StringContains(t, body, tt.wantFormTag) } }) } }
اگر تست را اجرا کنید، باید ببینید که همه sub-testها اجرا میشوند و با موفقیت پاس میشوند — مشابه این:
$ go test -v -run="TestUserSignup" ./cmd/web/
=== RUN TestUserSignup
=== RUN TestUserSignup/Valid_submission
=== RUN TestUserSignup/Invalid_CSRF_Token
=== RUN TestUserSignup/Empty_name
=== RUN TestUserSignup/Empty_email
=== RUN TestUserSignup/Empty_password
=== RUN TestUserSignup/Invalid_email
=== RUN TestUserSignup/Short_password
=== RUN TestUserSignup/Long_password
=== RUN TestUserSignup/Duplicate_email
--- PASS: TestUserSignup (0.01s)
--- PASS: TestUserSignup/Valid_submission (0.00s)
--- PASS: TestUserSignup/Invalid_CSRF_Token (0.00s)
--- PASS: TestUserSignup/Empty_name (0.00s)
--- PASS: TestUserSignup/Empty_email (0.00s)
--- PASS: TestUserSignup/Empty_password (0.00s)
--- PASS: TestUserSignup/Invalid_email (0.00s)
--- PASS: TestUserSignup/Short_password (0.00s)
--- PASS: TestUserSignup/Long_password (0.00s)
--- PASS: TestUserSignup/Duplicate_email (0.00s)
PASS
ok snippetbox.alexedwards.net/cmd/web 0.016s