Опирается на правила:
GOTEST-29,GOTEST-30,GOTEST-X10,GOTEST-X11из Go Test Strategy → раздел 9. Авторизация в тестах.
Важно знать
- Реальный JWKS/Keycloak не поднимаем — подменяем JWT-валидатор на
fakeAuthMiddleware, которая кладёт готовыйPrincipalв контекст.fakeAuthMiddleware(principal)— единственный способ прокинуть авторизацию в тестовый роутер; не отключаем middleware целиком.- Хелперы
AdminPrincipal()/CustomerPrincipal(id)живут в пакетеtesthelper— единый source-of-truth; не собираемPrincipalвручную в каждом тесте.- Сценарий с неверной ролью обязателен — тест должен убедиться, что эндпоинт вернёт
403, если роль не соответствует.401 Unauthorizedпроверяется отдельным тестом без middleware — запрос без токена должен возвращать401.newTestServer(t, principal)принимаетPrincipalкак параметр — каждый тест явно выбирает роль, без глобального состояния.t.Cleanup(srv.Close)регистрируется вnewTestServer— утечек соединений нет.- Отключение auth-middleware маскирует баги авторизации, которые всплывут только в проде.
UCP-подход к авторизации в тестах решает один вопрос: как убедиться, что RBAC-логика работает, не поднимая внешний identity-провайдер. В Java это TestJwtConfiguration + TestHttpHeaders.withAdminToken(). В Go — fakeAuthMiddleware + testhelper с фиксированными Principal-значениями.
GOTEST-29 — fakeAuthMiddleware вместо реального JWT-валидатора
Продакшн-роутер проверяет подпись токена через JWKS-эндпоинт. В тесте подпись нас не интересует — нас интересует, как сервис ведёт себя при конкретной роли. Поэтому подменяем middleware целиком:
func fakeAuthMiddleware(principal auth.Principal) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := auth.WithPrincipal(r.Context(), principal)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
auth.WithPrincipal — та же функция, что использует продакшн-middleware. Обработчик не знает, откуда пришёл Principal: из реального токена или из теста. Это и есть цель подмены.
newTestServer принимает Principal явным параметром и регистрирует fakeAuthMiddleware вместо продакшн-валидатора:
func newTestServer(t *testing.T, principal auth.Principal) (*httptest.Server, *OrderDatabasePreparer) {
t.Helper()
pool, _ := pgxpool.New(context.Background(), testDSN)
t.Cleanup(pool.Close)
q := db.New(pool)
repo := order.NewRepository(q)
clock := &fixedClock{at: time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC)}
ids := &seqIDGenerator{}
svc := order.NewService(repo, clock, ids)
r := chi.NewRouter()
r.Use(fakeAuthMiddleware(principal))
orderhttp.Mount(r, svc)
srv := httptest.NewServer(r)
t.Cleanup(srv.Close)
return srv, &OrderDatabasePreparer{pool: pool}
}
Роутер собирается в тесте — можно поставить любой middleware в любой позиции без изменения продакшн-кода.
GOTEST-30 — хелперы ролей в testhelper
Пакет testhelper — единственное место, где определяются тестовые Principal-значения. Не дублируем Principal{ID: "...", Role: "..."} в каждом тесте:
// testhelper/auth.go
package testhelper
import "myservice/internal/auth"
func AdminPrincipal() auth.Principal {
return auth.Principal{ID: "admin-1", Role: "admin"}
}
func CustomerPrincipal(id string) auth.Principal {
return auth.Principal{ID: id, Role: "customer"}
}
func SberManagerPrincipal(id string) auth.Principal {
return auth.Principal{ID: id, Role: "sber-manager"}
}
Зачем один пакет: если Principal поменяет структуру (добавится поле Permissions, TenantID) — меняем в одном месте, все тесты подхватят автоматически.
Пример теста с успешной авторизацией клиента через CustomerPrincipal:
func TestGetOrder_AsCustomer_ReturnsOrder(t *testing.T) {
customerID := "cust-42"
srv, prep := newTestServer(t, testhelper.CustomerPrincipal(customerID))
// Arrange
prep.Clear(t).
CreateCustomer(t, customerID).
CreateOrder(t, "order-1", customerID, "CONFIRMED")
// Act
req, _ := http.NewRequest(http.MethodGet, srv.URL+"/orders/order-1", nil)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
// Assert
require.Equal(t, http.StatusOK, resp.StatusCode)
var body map[string]any
require.NoError(t, json.NewDecoder(resp.Body).Decode(&body))
assert.Equal(t, "order-1", body["id"])
assert.Equal(t, "CONFIRMED", body["status"])
}
GOTEST-X11 — Auth-middleware не отключать, 403 проверять явно
Соблазн «упростить тест» — убрать auth-middleware из тестового роутера совсем. Это ошибка: тест перестаёт проверять, что endpoint защищён. Продакшн может сломаться по-тихому.
Правило: каждый защищённый эндпоинт должен иметь хотя бы один тест с неверной ролью, который проверяет 403:
func TestCreateOrder_AsCustomer_Forbidden(t *testing.T) {
srv, prep := newTestServer(t, testhelper.CustomerPrincipal("cust-1"))
prep.Clear(t).CreateCustomer(t, "cust-1")
// Act — customer пытается вызвать эндпоинт только для sber-manager
resp, err := http.Post(srv.URL+"/orders/bulk-import", "application/json",
strings.NewReader(`{"items":[]}`))
require.NoError(t, err)
defer resp.Body.Close()
// Assert
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
}
И отдельный тест на 401 — запрос вовсе без токена. Для этого поднимаем роутер без fakeAuthMiddleware (с продакшн-middleware, которая умеет возвращать 401 на отсутствующий токен):
func TestCreateOrder_NoToken_Unauthorized(t *testing.T) {
pool, _ := pgxpool.New(context.Background(), testDSN)
t.Cleanup(pool.Close)
r := chi.NewRouter()
r.Use(auth.RequireToken()) // продакшн-middleware: 401 если нет заголовка
orderhttp.Mount(r, order.NewService(order.NewRepository(db.New(pool)),
&fixedClock{}, &seqIDGenerator{}))
srv := httptest.NewServer(r)
t.Cleanup(srv.Close)
resp, err := http.Post(srv.URL+"/orders", "application/json",
strings.NewReader(`{"customerId":"c1","amount":100}`))
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
}
Роли для других Bounded Context: Product, Customer
Если сервис работает в домене Product/Sber (каталог, контракты), хелперы остаются теми же — меняется только набор ролей:
// testhelper/auth.go (для product-сервиса)
func ProductManagerPrincipal(id string) auth.Principal {
return auth.Principal{ID: id, Role: "product-manager"}
}
func SberContractPrincipal(id string) auth.Principal {
return auth.Principal{ID: id, Role: "sber-contract"}
}
Тест на проверку доступа к черновику продукта:
func TestGetProductDraft_AsProductManager_ReturnsProduct(t *testing.T) {
managerID := "pm-7"
srv, prep := newTestServer(t, testhelper.ProductManagerPrincipal(managerID))
// Arrange
prep.Clear(t).
CreateProduct(t, "prod-1", "DRAFT").
AssignManager(t, "prod-1", managerID)
// Act
req, _ := http.NewRequest(http.MethodGet, srv.URL+"/products/prod-1", nil)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
// Assert
require.Equal(t, http.StatusOK, resp.StatusCode)
var body map[string]any
require.NoError(t, json.NewDecoder(resp.Body).Decode(&body))
assert.Equal(t, "DRAFT", body["status"])
}
func TestGetProductDraft_AsCustomer_Forbidden(t *testing.T) {
srv, prep := newTestServer(t, testhelper.CustomerPrincipal("cust-99"))
prep.Clear(t).CreateProduct(t, "prod-1", "DRAFT")
req, _ := http.NewRequest(http.MethodGet, srv.URL+"/products/prod-1", nil)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
}
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
| Реальный Keycloak/JWKS в интеграционном тесте | GOTEST-X10 | fakeAuthMiddleware с готовым Principal |
| Отключить auth-middleware в тестовом роутере | GOTEST-X11 | fakeAuthMiddleware с нужной ролью; тест 403/401 обязателен |
Собирать Principal{...} руками в каждом тесте | GOTEST-30 | хелперы AdminPrincipal() / CustomerPrincipal(id) в testhelper |
| Один тест проверяет и happy path, и 403 | GOTEST-3 | один тест — один сценарий |
t.Parallel() с общим TRUNCATE без транзакции | GOTEST-18 | изоляция через tx.Rollback в t.Cleanup |
testify/mock для auth-интерфейса вместо fakeAuthMiddleware | GOTEST-X10 | подмена middleware целиком |
Куда дальше
- Структура одного теста — AAA, имена
Test<Action>_<Condition>_<Expected>,requirevsassert. - Инфраструктура теста — TestMain + контейнер —
TestMain,newTestServer,testDSN. - DatabasePreparer в Go —
Clear(t), fluentCreate*(t, ...), порядок FK. - Пирамида тестов — unit на агрегат без инфраструктуры, unit на контроллер с
httptest.NewRecorder. - Авторизация — нормативный гайд —
Principal, ABAC, ownership-check.