第三批(最终批):把 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>
168 lines
6.0 KiB
Go
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())
|
|
}
|
|
}
|
|
}
|