package middleware import ( "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" "hy2xs-admin/model/constant" "hy2xs-admin/model/vo" "hy2xs-admin/service" ) // Состояние сессии сообщается КОДОМ, а не текстом. // // Регрессия. Все отказы аутентификации звали vo.Fail с человеческой строкой, а // код ответа выводился сравнением этой строки с тремя известными литералами. // Под условия подходил только `unauthorized`; истёкший токен уезжал с кодом // системной ошибки 50000, панель показывала оператору голый тост // «token expired» и не понимала, что сессия кончилась. Ключ локализации // common.sessionExpired существовал и был мёртвым, а вернуть оператора на // форму входа мог только переход по маршруту, которому потребовался бы // getAdminInfo. type authResponse struct { Code int `json:"code"` Type string `json:"type"` Errors []vo.FieldError `json:"errors"` } func callJWTHandler(t *testing.T, header string) authResponse { t.Helper() gin.SetMode(gin.TestMode) engine := gin.New() engine.GET("/guarded", JWTHandler(), func(c *gin.Context) { vo.Success(nil, c) }) request := httptest.NewRequest(http.MethodGet, "/guarded", nil) if header != "" { request.Header.Set("Authorization", header) } recorder := httptest.NewRecorder() engine.ServeHTTP(recorder, request) var parsed authResponse if err := json.Unmarshal(recorder.Body.Bytes(), &parsed); err != nil { t.Fatalf("ответ не разбирается как JSON: %s", recorder.Body.String()) } return parsed } func TestJWTHandlerReportsMissingCredentials(t *testing.T) { response := callJWTHandler(t, "") if response.Code != constant.CodeUnauthorizedError { t.Fatalf("код ответа %d, ожидался %d", response.Code, constant.CodeUnauthorizedError) } if len(response.Errors) != 1 || response.Errors[0].Code != constant.ErrCodeUnauthorized { t.Fatalf("неожиданное описание отказа: %+v", response.Errors) } } func TestJWTHandlerReportsMalformedAuthorizationHeader(t *testing.T) { for _, header := range []string{"token-without-scheme", "Basic dXNlcjpwYXNz"} { response := callJWTHandler(t, header) if response.Code != constant.CodeUnauthorizedError { t.Errorf("заголовок %q: код ответа %d, ожидался %d", header, response.Code, constant.CodeUnauthorizedError) continue } if len(response.Errors) != 1 || response.Errors[0].Code != constant.ErrCodeTokenInvalid { t.Errorf("заголовок %q: неожиданное описание отказа: %+v", header, response.Errors) } } } // Истёкшая сессия обязана быть отличима от недействительного токена: панель // показывает оператору разные вещи и по-разному его возвращает на вход. func TestTokenErrorCodeSeparatesExpiryFromInvalidity(t *testing.T) { if code := tokenErrorCode(service.ErrTokenExpired); code != constant.ErrCodeSessionExpired { t.Errorf("истёкший токен получил код %q, ожидался %q", code, constant.ErrCodeSessionExpired) } if code := tokenErrorCode(service.ErrTokenInvalid); code != constant.ErrCodeTokenInvalid { t.Errorf("недействительный токен получил код %q, ожидался %q", code, constant.ErrCodeTokenInvalid) } }