repair-agent c219ec2fcf
Some checks failed
Build and Deploy Backend / build-and-deploy (push) Failing after 4m28s
feat(hw-ws-service): 将 Go WebSocket 服务纳入 CI/CD 并通过 Traefik 统一入口
## 变更内容

### k8s/ingress.yaml
- 新增 /xiaozhi/v1/ 路径规则,将 WebSocket 流量路由到 hw-ws-svc:8888
- Traefik 最长前缀优先,/xiaozhi/v1/ 不影响 / 下的 Django 路由

### hw_service_go/k8s/service.yaml
- Service 类型由 LoadBalancer 改为 ClusterIP
- 移除阿里云 SLB 注解(通过 Traefik Ingress 统一暴露,不再需要独立公网 IP)

### hw_service_go/k8s/deployment.yaml
- 镜像地址改为 ${CI_REGISTRY_IMAGE}/hw-ws-service:latest 占位符
- CI/CD 部署时统一通过 sed 替换为华为云 SWR 实际地址

### hw_service_go/internal/server/server.go
- 新增 GET /xiaozhi/v1/healthz 接口,返回 {"status":"ok","active_connections":N}
- 用于部署后验证服务存活及当前连接数

### .gitea/workflows/deploy.yaml
- 新增 Build and Push HW WebSocket Service 步骤,构建并推送 hw_service_go 镜像
- 部署步骤新增 kubectl apply hw_service_go/k8s/deployment.yaml 和 service.yaml
- 新增 kubectl rollout restart deployment/hw-ws-service

### run.sh
- 本地同时启动 Django + hw_service_go 的开发脚本

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 17:16:26 +08:00

143 lines
4.2 KiB
Go

package rtcclient_test
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/qy/hw-ws-service/internal/rtcclient"
)
func successBody(title, audioURL string) []byte {
b, _ := json.Marshal(map[string]any{
"code": 0,
"message": "success",
"data": map[string]string{
"title": title,
"audio_url": audioURL,
},
})
return b
}
func errorBody(code int, msg string) []byte {
b, _ := json.Marshal(map[string]any{
"code": code,
"message": msg,
"data": nil,
})
return b
}
func TestFetchStoryByMAC_Success(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/v1/devices/stories/" {
t.Errorf("unexpected path: %s", r.URL.Path)
}
w.Header().Set("Content-Type", "application/json")
w.Write(successBody("小红帽", "https://example.com/story.mp3"))
}))
defer srv.Close()
client := rtcclient.New(srv.URL)
story, err := client.FetchStoryByMAC(context.Background(), "aa:bb:cc:dd:ee:ff")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if story == nil {
t.Fatal("expected story, got nil")
}
if story.Title != "小红帽" {
t.Errorf("title = %q, want %q", story.Title, "小红帽")
}
if story.AudioURL != "https://example.com/story.mp3" {
t.Errorf("audio_url = %q", story.AudioURL)
}
}
// TestFetchStoryByMAC_MACUppercase verifies the client always sends uppercase MAC.
func TestFetchStoryByMAC_MACUppercase(t *testing.T) {
var gotMAC string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotMAC = r.URL.Query().Get("mac_address")
w.Header().Set("Content-Type", "application/json")
w.Write(successBody("test", "https://example.com/t.mp3"))
}))
defer srv.Close()
client := rtcclient.New(srv.URL)
client.FetchStoryByMAC(context.Background(), "aa:bb:cc:dd:ee:ff") //nolint:errcheck
if gotMAC != "AA:BB:CC:DD:EE:FF" {
t.Errorf("MAC not uppercased: got %q, want %q", gotMAC, "AA:BB:CC:DD:EE:FF")
}
}
// TestFetchStoryByMAC_NotFound verifies that HTTP 404 returns (nil, nil).
func TestFetchStoryByMAC_NotFound(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
}))
defer srv.Close()
client := rtcclient.New(srv.URL)
story, err := client.FetchStoryByMAC(context.Background(), "AA:BB:CC:DD:EE:FF")
if err != nil {
t.Fatalf("unexpected error for 404: %v", err)
}
if story != nil {
t.Errorf("expected nil story for 404, got %+v", story)
}
}
// TestFetchStoryByMAC_BusinessError verifies that code != 0 returns (nil, nil).
func TestFetchStoryByMAC_BusinessError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write(errorBody(404, "暂无可播放的故事"))
}))
defer srv.Close()
client := rtcclient.New(srv.URL)
story, err := client.FetchStoryByMAC(context.Background(), "AA:BB:CC:DD:EE:FF")
if err != nil {
t.Fatalf("unexpected error for business error response: %v", err)
}
if story != nil {
t.Errorf("expected nil story for business error, got %+v", story)
}
}
// TestFetchStoryByMAC_ServerError verifies that HTTP 5xx returns an error.
func TestFetchStoryByMAC_ServerError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer srv.Close()
client := rtcclient.New(srv.URL)
_, err := client.FetchStoryByMAC(context.Background(), "AA:BB:CC:DD:EE:FF")
if err == nil {
t.Error("expected error for HTTP 500, got nil")
}
}
// TestFetchStoryByMAC_ContextCanceled verifies that a canceled context returns an error.
func TestFetchStoryByMAC_ContextCanceled(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Never respond — let the client time out
<-r.Context().Done()
}))
defer srv.Close()
ctx, cancel := context.WithCancel(context.Background())
cancel() // cancel immediately
client := rtcclient.New(srv.URL)
_, err := client.FetchStoryByMAC(ctx, "AA:BB:CC:DD:EE:FF")
if err == nil {
t.Error("expected error for canceled context, got nil")
}
}