128 lines
5.2 KiB
Go
128 lines
5.2 KiB
Go
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)
|
||
}
|
||
}
|