Some checks failed
Build and Deploy Backend / build-and-deploy (push) Failing after 4m28s
## 变更内容
### 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>
143 lines
4.2 KiB
Go
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")
|
|
}
|
|
}
|