Files
meshtastic_mqtt_server/internal/web/map_tile_proxy_routes_test.go
T
kevinandClaude 9394aa0f4a 重构:拆出 bot / sign / mapsource / llmadmin / web 包
第三批(最终批):把 root 中剩下所有领域文件按功能搬到 internal/ 下。
完成后根目录从 46 个 .go 文件降到 14 个,其中 11 个是 bridge(type alias
+ thin wrapper),仅供尚未改造的 main.go 引用。

新增包
- internal/bot/         Service / TextSender / NewPKIKeyResolver /
                        RegisterRoutes,含 PKI 直连发送、节点信息广播、
                        outbound DM 持久化等业务逻辑。
- internal/sign/        SignDTO / SignDayCountDTO / RegisterAdminRoutes,
                        把原来分散在 admin_sign_routes.go 与 web.go 中的
                        sign DTO/路由收拢到一处。
- internal/mapsource/   AdminDTO / PublicDTO / RegisterAdminRoutes /
                        RegisterPublicRoutes。
- internal/llmadmin/    LLM 消息队列、Provider、ToolRouter、PrimaryConfig 的
                        admin 路由。
- internal/web/         路由总入口(NewRouter/NewHTTPServer/ServeUnixSocket)、
                        各资源的 GET API、admin 用户/登录/MQTT 状态、所有
                        DTO 函数。把 auth.go 的 sessionClaims 升级为
                        auth.SessionClaims;mqtt_status.go 重写成
                        MQTTRuntimeStatus / AdminMQTTStatus 结构体并把
                        client info 解析在 web 包内自带,不再依赖 main 包。
                        map_tile_proxy_routes 与测试一起搬入。

修改
- web.go 中 parseListOptions / writeListResponse / ptrString 等本地 helper
  改为对 internal/webutil 的 thin wrapper,避免重复实现。
- internal/auth 在 step 4 已创建,本批中 web 包正式开始引用其 Manager /
  RequireAdmin / SessionClaims。

根目录新增 bridge:bot_bridge.go / sign_bridge.go / mapsource_bridge.go /
llmadmin_bridge.go / web_bridge.go。后者把 mqttRuntimeStatus 包成
webpkg.MQTTStatusProvider 适配器,使 main.go 中旧字段名保持可用。

go build ./... / go test ./... 全部通过;测试数量未变。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-18 18:24:14 +08:00

168 lines
6.0 KiB
Go

package web
import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
configpkg "meshtastic_mqtt_server/internal/config"
storepkg "meshtastic_mqtt_server/internal/store"
"meshtastic_mqtt_server/internal/store/testutil"
)
func openTestStore(t *testing.T) *storepkg.Store {
return testutil.OpenStore(t)
}
func TestMapTileProxyFetchesAndCaches(t *testing.T) {
st := openTestStore(t)
defer st.Close()
requests := 0
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests++
if r.URL.Path != "/3/1/2.png" {
t.Fatalf("upstream path = %q, want /3/1/2.png", r.URL.Path)
}
w.Header().Set("Content-Type", "image/png")
_, _ = w.Write([]byte("tile-data"))
}))
defer upstream.Close()
row, err := st.CreateMapTileSource(storepkg.MapTileSourceInput{Name: "Tiles", URLTemplate: upstream.URL + "/{z}/{x}/{y}.png", MaxZoom: 18, Enabled: true, ProxyEnabled: true})
if err != nil {
t.Fatalf("CreateMapTileSource() error = %v", err)
}
cacheDir := t.TempDir()
router := NewRouter(configpkg.WebConfig{StaticDir: t.TempDir(), MapTileCacheDir: cacheDir}, st, nil, nil, nil, nil, nil, nil)
url := "/api/map/" + row.URLTemplateHash + "?x=1&y=2&z=3"
for i := 0; i < 2; i++ {
recorder := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, url, nil)
router.ServeHTTP(recorder, req)
if recorder.Code != http.StatusOK {
t.Fatalf("request %d status = %d, body = %s", i+1, recorder.Code, recorder.Body.String())
}
if recorder.Body.String() != "tile-data" {
t.Fatalf("request %d body = %q, want tile-data", i+1, recorder.Body.String())
}
}
if requests != 1 {
t.Fatalf("upstream requests = %d, want 1", requests)
}
cachePath := filepath.Join(cacheDir, row.URLTemplateHash, "3", "1", "2.tile")
data, err := os.ReadFile(cachePath)
if err != nil {
t.Fatalf("read cache file %s: %v", cachePath, err)
}
if string(data) != "tile-data" {
t.Fatalf("cache file = %q, want tile-data", string(data))
}
}
func TestMapTileProxyRejectsInvalidCoordinates(t *testing.T) {
st := openTestStore(t)
defer st.Close()
row, err := st.CreateMapTileSource(storepkg.MapTileSourceInput{Name: "Tiles", URLTemplate: "https://tiles.example.com/{z}/{x}/{y}.png", MaxZoom: 3, Enabled: true, ProxyEnabled: true})
if err != nil {
t.Fatalf("CreateMapTileSource() error = %v", err)
}
router := NewRouter(configpkg.WebConfig{StaticDir: t.TempDir(), MapTileCacheDir: t.TempDir()}, st, nil, nil, nil, nil, nil, nil)
cases := []string{
"/api/map/" + row.URLTemplateHash + "?y=0&z=0",
"/api/map/" + row.URLTemplateHash + "?x=-1&y=0&z=0",
"/api/map/" + row.URLTemplateHash + "?x=0&y=0&z=4",
"/api/map/" + row.URLTemplateHash + "?x=2&y=0&z=1",
}
for _, url := range cases {
recorder := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, url, nil)
router.ServeHTTP(recorder, req)
if recorder.Code != http.StatusBadRequest {
t.Fatalf("%s status = %d, want 400; body = %s", url, recorder.Code, recorder.Body.String())
}
}
}
func TestMapTileProxyUnknownAndDisabledSource(t *testing.T) {
st := openTestStore(t)
defer st.Close()
disabled, err := st.CreateMapTileSource(storepkg.MapTileSourceInput{Name: "Disabled", URLTemplate: "https://disabled.example.com/{z}/{x}/{y}.png", MaxZoom: 3, Enabled: false})
if err != nil {
t.Fatalf("CreateMapTileSource(disabled) error = %v", err)
}
proxyDisabled, err := st.CreateMapTileSource(storepkg.MapTileSourceInput{Name: "ProxyDisabled", URLTemplate: "https://proxy-disabled.example.com/{z}/{x}/{y}.png", MaxZoom: 3, Enabled: true, ProxyEnabled: false})
if err != nil {
t.Fatalf("CreateMapTileSource(proxy disabled) error = %v", err)
}
router := NewRouter(configpkg.WebConfig{StaticDir: t.TempDir(), MapTileCacheDir: t.TempDir()}, st, nil, nil, nil, nil, nil, nil)
cases := []string{
"/api/map/not-a-hash?x=0&y=0&z=0",
"/api/map/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa?x=0&y=0&z=0",
"/api/map/" + disabled.URLTemplateHash + "?x=0&y=0&z=0",
"/api/map/" + proxyDisabled.URLTemplateHash + "?x=0&y=0&z=0",
}
wantStatus := []int{http.StatusBadRequest, http.StatusNotFound, http.StatusNotFound, http.StatusNotFound}
for i, url := range cases {
recorder := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, url, nil)
router.ServeHTTP(recorder, req)
if recorder.Code != wantStatus[i] {
t.Fatalf("%s status = %d, want %d; body = %s", url, recorder.Code, wantStatus[i], recorder.Body.String())
}
}
}
func TestMapTileProxyUpstreamStatus(t *testing.T) {
st := openTestStore(t)
defer st.Close()
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.Path, "/404/") {
http.NotFound(w, r)
return
}
http.Error(w, "upstream error", http.StatusInternalServerError)
}))
defer upstream.Close()
row404, err := st.CreateMapTileSource(storepkg.MapTileSourceInput{Name: "NotFoundTiles", URLTemplate: upstream.URL + "/404/{z}/{x}/{y}.png", MaxZoom: 18, Enabled: true, ProxyEnabled: true})
if err != nil {
t.Fatalf("CreateMapTileSource(404) error = %v", err)
}
row500, err := st.CreateMapTileSource(storepkg.MapTileSourceInput{Name: "StatusTiles", URLTemplate: upstream.URL + "/{z}/{x}/{y}.png", MaxZoom: 18, Enabled: true, ProxyEnabled: true})
if err != nil {
t.Fatalf("CreateMapTileSource(500) error = %v", err)
}
router := NewRouter(configpkg.WebConfig{StaticDir: t.TempDir(), MapTileCacheDir: t.TempDir()}, st, nil, nil, nil, nil, nil, nil)
cases := []struct {
url string
want int
}{
{url: "/api/map/" + row404.URLTemplateHash + "?x=0&y=0&z=0", want: http.StatusNotFound},
{url: "/api/map/" + row500.URLTemplateHash + "?x=0&y=0&z=0", want: http.StatusBadGateway},
}
for _, tc := range cases {
recorder := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, tc.url, nil)
router.ServeHTTP(recorder, req)
if recorder.Code != tc.want {
t.Fatalf("%s status = %d, want %d; body = %s", tc.url, recorder.Code, tc.want, recorder.Body.String())
}
}
}