thread-master/apps/backend/internal/logic/radar/service_profile_logic_test.go

165 lines
5.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package radar
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"apps/backend/internal/middleware"
radarRepo "apps/backend/internal/module/radar/repository"
radarUC "apps/backend/internal/module/radar/usecase"
"apps/backend/internal/response"
"apps/backend/internal/svc"
"apps/backend/internal/types"
)
func testCtx(uid int64) (context.Context, *svc.ServiceContext) {
svcCtx := &svc.ServiceContext{Radar: radarUC.New(radarRepo.NewMemory())}
return middleware.WithUID(context.Background(), uid), svcCtx
}
func validProfileReq() *types.UpsertServiceProfileReq {
return &types.UpsertServiceProfileReq{
Services: []types.ServiceItem{{Name: "婚禮攝影", PriceMin: 18000, PriceMax: 36000, Currency: "TWD"}},
Cases: []types.ServiceCasePublic{{Title: "陽明山戶外婚禮"}},
Forbidden: []string{"保證接到案"},
Faq: []types.FaqItem{{Question: "可以加時嗎?", Answer: "可以"}},
ServiceAreas: []string{"TPE"},
Availability: "平日全天",
ToneNote: "親切、不推銷",
}
}
// SP-02填完服務檔案後 GET 要能讀回全部欄位forbidden[] 尤其重要 —— 它是回覆生成的硬性過濾詞。
func TestServiceProfileRoundTripsThroughHTTPTypes(t *testing.T) {
ctx, svcCtx := testCtx(42)
if _, err := NewUpsertServiceProfileLogic(ctx, svcCtx).UpsertServiceProfile(validProfileReq()); err != nil {
t.Fatalf("upsert: %v", err)
}
got, err := NewGetServiceProfileLogic(ctx, svcCtx).GetServiceProfile()
if err != nil {
t.Fatalf("get: %v", err)
}
if !got.Exists {
t.Fatal("exists = false after upsert")
}
if len(got.Services) != 1 || got.Services[0].Name != "婚禮攝影" || got.Services[0].PriceMax != 36000 {
t.Fatalf("services = %+v", got.Services)
}
if len(got.Forbidden) != 1 || got.Forbidden[0] != "保證接到案" {
t.Fatalf("forbidden = %v", got.Forbidden)
}
if len(got.Faq) != 1 || len(got.Cases) != 1 || len(got.ServiceAreas) != 1 {
t.Fatalf("faq/cases/areas lost: %+v", got)
}
if got.Availability != "平日全天" || got.ToneNote != "親切、不推銷" {
t.Fatalf("free-text fields lost: %+v", got)
}
if got.UpdatedAt <= 0 {
t.Fatalf("updated_at = %d, want ns timestamp", got.UpdatedAt)
}
}
// 未建檔要回 exists=false 的 200不是 404表單得先開得起來才有東西填。
func TestGetBeforeFirstSaveReportsAbsentProfile(t *testing.T) {
ctx, svcCtx := testCtx(42)
got, err := NewGetServiceProfileLogic(ctx, svcCtx).GetServiceProfile()
if err != nil {
t.Fatalf("get: %v", err)
}
if got.Exists {
t.Fatal("exists = true without any saved profile")
}
// 空陣列而非 null前端直接 map不必每個欄位判 null。
if got.Services == nil || got.Cases == nil || got.Forbidden == nil || got.Faq == nil || got.ServiceAreas == nil {
t.Fatalf("absent profile must use empty slices, got %+v", got)
}
}
func TestServiceProfileIsolatedByJWTOwner(t *testing.T) {
ctxA, svcCtx := testCtx(42)
if _, err := NewUpsertServiceProfileLogic(ctxA, svcCtx).UpsertServiceProfile(validProfileReq()); err != nil {
t.Fatalf("upsert as 42: %v", err)
}
// 同一個 store、另一個登入者request 沒有任何欄位能指定 owner所以看不到別人的檔案。
ctxB := middleware.WithUID(context.Background(), 43)
got, err := NewGetServiceProfileLogic(ctxB, svcCtx).GetServiceProfile()
if err != nil {
t.Fatalf("get as 43: %v", err)
}
if got.Exists {
t.Fatal("owner 43 read owner 42's service profile")
}
// 43 覆寫自己的檔案不會動到 42 的。
req := validProfileReq()
req.Services[0].Name = "另一種服務"
if _, err := NewUpsertServiceProfileLogic(ctxB, svcCtx).UpsertServiceProfile(req); err != nil {
t.Fatalf("upsert as 43: %v", err)
}
back, err := NewGetServiceProfileLogic(ctxA, svcCtx).GetServiceProfile()
if err != nil {
t.Fatalf("get as 42: %v", err)
}
if back.Services[0].Name != "婚禮攝影" {
t.Fatalf("owner 42's profile was overwritten by owner 43: %+v", back.Services)
}
}
func TestServiceProfileRequiresAuth(t *testing.T) {
svcCtx := &svc.ServiceContext{Radar: radarUC.New(radarRepo.NewMemory())}
anon := context.Background()
if _, err := NewGetServiceProfileLogic(anon, svcCtx).GetServiceProfile(); err == nil {
t.Fatal("GET without JWT succeeded")
} else {
assertStatus(t, err, http.StatusUnauthorized, 401001)
}
if _, err := NewUpsertServiceProfileLogic(anon, svcCtx).UpsertServiceProfile(validProfileReq()); err == nil {
t.Fatal("PUT without JWT succeeded")
} else {
assertStatus(t, err, http.StatusUnauthorized, 401001)
}
}
// 驗證失敗要是明確的 400100能對到欄位的訊息不是 500 也不是空成功。
func TestInvalidProfileMapsToValidationError(t *testing.T) {
ctx, svcCtx := testCtx(42)
req := validProfileReq()
req.Services[0].PriceMin = 50000
req.Services[0].PriceMax = 10000
_, err := NewUpsertServiceProfileLogic(ctx, svcCtx).UpsertServiceProfile(req)
if err == nil {
t.Fatal("reversed price range was accepted")
}
env := assertStatus(t, err, http.StatusBadRequest, 400100)
if env.Message == "" {
t.Fatal("validation error carried no message")
}
}
func assertStatus(t *testing.T, err error, wantHTTP int, wantCode int64) response.Envelope {
t.Helper()
rec := httptest.NewRecorder()
response.Write(context.Background(), rec, nil, err)
if rec.Code != wantHTTP {
t.Fatalf("HTTP = %d, want %d (err=%v)", rec.Code, wantHTTP, err)
}
var env response.Envelope
if decErr := json.NewDecoder(rec.Body).Decode(&env); decErr != nil {
t.Fatalf("decode envelope: %v", decErr)
}
if env.Code != wantCode {
t.Fatalf("code = %d, want %d", env.Code, wantCode)
}
return env
}