feat(server/stats): stats-overhaul Phase2 — 每设备归因 + GB 综合配额
服务端记账从「账户」细到「每设备」(每设备独立 dp_uuid),配额单位分钟→GB 按账户综合卡控。000015_per_device_usage 迁移(mysql+sqlite 双份):devices.dp_uuid + usage_device_daily 表 + plans.daily_mb。handler_grpc 按 dp_uuid 回映射 (user_id,device_id) 双写账户+每设备;usage 服务/handler 暴露 /v1/usage(/devices)。 含 sqlite_per_device / usage handler 测试。 注:此迁移 prod 已 migrate up 运行、部署二进制已内嵌;本次补提交使 git 与线上一致。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
package usage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/wangjia/pangolin/server/internal/codes"
|
||||
)
|
||||
|
||||
// authedRequest builds a request whose context carries an authenticated user id,
|
||||
// mirroring what auth.RequireAuth installs upstream.
|
||||
func authedRequest(method, target string, userID int64) *http.Request {
|
||||
r := httptest.NewRequest(method, target, nil)
|
||||
if userID != 0 {
|
||||
r = r.WithContext(context.WithValue(r.Context(), codes.CtxKeyUserID, userID))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// The DeviceUsageHandler gates on auth, method and the days query param before
|
||||
// ever touching the service/DB — these branches are verified DB-free.
|
||||
func TestDeviceUsageHandler_Guards(t *testing.T) {
|
||||
h := NewDeviceUsageHandler(NewService(nil, nil, nil, 0))
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
req *http.Request
|
||||
status int
|
||||
}{
|
||||
{"no auth → 401", authedRequest(http.MethodGet, "/v1/usage/devices", 0), http.StatusUnauthorized},
|
||||
{"wrong method → 405", authedRequest(http.MethodPost, "/v1/usage/devices", 7), http.StatusMethodNotAllowed},
|
||||
{"days out of range → 400", authedRequest(http.MethodGet, "/v1/usage/devices?days=999", 7), http.StatusBadRequest},
|
||||
{"days non-numeric → 400", authedRequest(http.MethodGet, "/v1/usage/devices?days=abc", 7), http.StatusBadRequest},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, c.req)
|
||||
if w.Code != c.status {
|
||||
t.Errorf("status=%d, want %d (body=%s)", w.Code, c.status, w.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user