dana-medical-site/internal/api/security_pack_test.go
2026-10-14 13:33:13 +00:00

128 lines
5.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package api
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
"dana/internal/storage"
)
func ipRequest(remote string, headers map[string]string) *http.Request {
r := httptest.NewRequest(http.MethodGet, "/", nil)
r.RemoteAddr = remote
for k, v := range headers {
r.Header.Set(k, v)
}
return r
}
func TestClientIPIgnoresSpoofedHeaders(t *testing.T) {
saved := trustedProxies
t.Cleanup(func() { trustedProxies = saved })
cases := []struct {
name string
remote string
headers map[string]string
want string
}{
{"посторонний подставил X-Real-IP", "203.0.113.7:5555", map[string]string{"X-Real-IP": "1.1.1.1"}, "203.0.113.7"},
{"посторонний подставил X-Forwarded-For", "203.0.113.7:5555", map[string]string{"X-Forwarded-For": "1.1.1.1"}, "203.0.113.7"},
{"доверенный прокси передал X-Real-IP", "127.0.0.1:4000", map[string]string{"X-Real-IP": "198.51.100.9"}, "198.51.100.9"},
{"доверенный прокси: XFF справа налево", "127.0.0.1:4000", map[string]string{"X-Forwarded-For": "6.6.6.6, 198.51.100.9, 127.0.0.1"}, "198.51.100.9"},
{"доверенный прокси без заголовков", "127.0.0.1:4000", nil, "127.0.0.1"},
}
for _, c := range cases {
if got := clientIP(ipRequest(c.remote, c.headers)); got != c.want {
t.Errorf("%s: %q, ждали %q", c.name, got, c.want)
}
}
// Caddy пропускает X-Real-IP посетителя как есть, а X-Forwarded-For формирует сам — верим XFF
spoof := map[string]string{"X-Real-IP": "6.6.6.6", "X-Forwarded-For": "198.51.100.9"}
if got := clientIP(ipRequest("172.17.0.1:5000", spoof)); got != "198.51.100.9" {
t.Errorf("подделанный X-Real-IP через прокси: %q, ждали адрес из X-Forwarded-For", got)
}
// TRUSTED_PROXIES заменяет список: адрес вне списка перестаёт быть доверенным
if err := SetTrustedProxies("10.0.0.1"); err != nil {
t.Fatal(err)
}
if got := clientIP(ipRequest("172.17.0.1:5000", spoof)); got != "172.17.0.1" {
t.Errorf("Docker-адрес вне TRUSTED_PROXIES: %q", got)
}
if got := clientIP(ipRequest("10.0.0.1:5000", spoof)); got != "198.51.100.9" {
t.Errorf("адрес из TRUSTED_PROXIES: %q", got)
}
if err := SetTrustedProxies("не-адрес"); err == nil {
t.Error("ошибочный TRUSTED_PROXIES должен давать ошибку при запуске")
}
}
func TestRedactURLHidesSecrets(t *testing.T) {
req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet,
"http://127.0.0.1:1/sys/wait_call.php?login=clinic&psw=TOPSECRET", nil)
_, err := (&http.Client{Timeout: 2 * time.Second}).Do(req)
if err == nil {
t.Skip("порт 1 неожиданно ответил")
}
if !strings.Contains(err.Error(), "TOPSECRET") {
t.Fatalf("ожидали, что исходная ошибка содержит адрес: %v", err)
}
if got := redactURL(err).Error(); strings.Contains(got, "TOPSECRET") || strings.Contains(got, "psw=") {
t.Errorf("пароль остался в ошибке: %q", got)
}
plain := errors.New("обычная ошибка")
if redactURL(plain) != plain {
t.Error("ошибку без адреса трогать не нужно")
}
}
func TestMediaHasNoDirectoryListing(t *testing.T) {
dir := t.TempDir()
if err := os.MkdirAll(filepath.Join(dir, "uploads", "2026"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "uploads", "2026", "doc.pdf"), []byte("%PDF-1.4"), 0o644); err != nil {
t.Fatal(err)
}
h := mediaHandler(dir)
for path, want := range map[string]int{
"/media/": http.StatusNotFound,
"/media/uploads/": http.StatusNotFound,
"/media/uploads/2026": http.StatusNotFound,
"/media/uploads/2026/doc.pdf": http.StatusOK,
} {
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil))
if rec.Code != want {
t.Errorf("%s: код %d, ждали %d", path, rec.Code, want)
}
if strings.Contains(rec.Body.String(), "doc.pdf") && path != "/media/uploads/2026/doc.pdf" {
t.Errorf("%s: в ответе виден список файлов", path)
}
}
}
func TestLeadOnlyDraftExplainsBookingLimit(t *testing.T) {
it := storage.ConfirmItem{
Start: time.Date(2026, 10, 12, 15, 30, 0, 0, moscow), Patient: "Анна", Phone: "+79280000000",
DoctorName: "Петрова", Error: errBookingLimit.Error() + ": за час 10 онлайн-записей при лимите 10",
}
d := leadOnlyDraft(it)
if !strings.Contains(d.Comments, "защита от ботов") || strings.Contains(d.Comments, "1С была недоступна") {
t.Errorf("при лимите лид должен объяснять защиту, а не сбой 1С: %q", d.Comments)
}
it.Error = "1С недоступна (HTTP 502): host=10.240.70.1"
d = leadOnlyDraft(it)
if !strings.Contains(d.Comments, "1С была недоступна") || strings.Contains(d.Comments, "10.240.70.1") {
t.Errorf("при сбое 1С — общая причина без внутренних адресов: %q", d.Comments)
}
}