thread-master/apps/backend/internal/module/radar/usecase/service_profile_test.go

211 lines
6.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 usecase
import (
"context"
"errors"
"testing"
"apps/backend/internal/module/radar/domain"
"apps/backend/internal/module/radar/repository"
)
func newTestService() *Service {
return New(repository.NewMemory())
}
func sampleProfile() *domain.ServiceProfile {
return &domain.ServiceProfile{
Services: []domain.ServiceItem{
{Name: "婚禮攝影", PriceMin: 18000, PriceMax: 36000},
{Name: "活動紀錄", PriceMin: 8000},
},
Cases: []domain.ServiceCase{{Title: "陽明山戶外婚禮", Summary: "全天紀錄", Link: "https://example.com/case/1"}},
Forbidden: []string{"保證接到案", " 保證接到案 ", "最便宜"},
Faq: []domain.FaqItem{{Question: "可以加時嗎?", Answer: "可以,每小時 3000。"}},
ServiceAreas: []string{"TPE", "nwt", "TPE"},
Availability: "平日全天、週末僅早場",
ToneNote: "親切、不推銷",
}
}
func TestUpsertThenGetKeepsEveryField(t *testing.T) {
svc := newTestService()
ctx := context.Background()
saved, err := svc.UpsertServiceProfile(ctx, 42, sampleProfile())
if err != nil {
t.Fatalf("upsert: %v", err)
}
got, err := svc.GetServiceProfile(ctx, 42)
if err != nil {
t.Fatalf("get: %v", err)
}
if got.OwnerUID != 42 {
t.Fatalf("owner_uid = %d, want 42", got.OwnerUID)
}
if len(got.Services) != 2 || got.Services[0].Name != "婚禮攝影" {
t.Fatalf("services not round-tripped: %+v", got.Services)
}
// 只填 price_min 是合法的「起價」,幣別要自動補上才顯示得出來。
if got.Services[1].Currency != "TWD" {
t.Fatalf("currency = %q, want TWD", got.Services[1].Currency)
}
if len(got.Cases) != 1 || got.Cases[0].Title != "陽明山戶外婚禮" {
t.Fatalf("cases not round-tripped: %+v", got.Cases)
}
// forbidden 是回覆生成的硬性過濾詞,讀不到就等於過濾失效。
if len(got.Forbidden) != 2 {
t.Fatalf("forbidden = %v, want 2 deduped items", got.Forbidden)
}
if len(got.Faq) != 1 || got.Faq[0].Answer == "" {
t.Fatalf("faq not round-tripped: %+v", got.Faq)
}
if len(got.ServiceAreas) != 2 || got.ServiceAreas[0] != "TPE" || got.ServiceAreas[1] != "NWT" {
t.Fatalf("service_areas = %v, want [TPE NWT]", got.ServiceAreas)
}
if got.Availability == "" || got.ToneNote == "" {
t.Fatalf("availability/tone_note lost: %+v", got)
}
if got.UpdatedAt <= 0 || got.CreatedAt <= 0 {
t.Fatalf("timestamps unset: created=%d updated=%d", got.CreatedAt, got.UpdatedAt)
}
if saved.UpdatedAt != got.UpdatedAt {
t.Fatalf("upsert returned a different revision than get")
}
}
func TestUpsertRejectsReversedPriceRange(t *testing.T) {
svc := newTestService()
p := sampleProfile()
p.Services[0].PriceMin = 50000
p.Services[0].PriceMax = 10000
_, err := svc.UpsertServiceProfile(context.Background(), 42, p)
if !errors.Is(err, domain.ErrValidation) {
t.Fatalf("err = %v, want ErrValidation", err)
}
}
func TestUpsertRejectsUnknownServiceAreaCode(t *testing.T) {
svc := newTestService()
p := sampleProfile()
// 縣市只吃代碼白名單;接受「台北」會讓判定的地區比對變成模糊猜測。
p.ServiceAreas = []string{"台北"}
_, err := svc.UpsertServiceProfile(context.Background(), 42, p)
if !errors.Is(err, domain.ErrValidation) {
t.Fatalf("err = %v, want ErrValidation", err)
}
}
func TestUpsertRequiresAreasUnlessRemote(t *testing.T) {
svc := newTestService()
ctx := context.Background()
p := sampleProfile()
p.ServiceAreas = nil
if _, err := svc.UpsertServiceProfile(ctx, 42, p); !errors.Is(err, domain.ErrValidation) {
t.Fatalf("err = %v, want ErrValidation for no area and no remote", err)
}
p = sampleProfile()
p.ServiceAreas = nil
p.RemoteOk = true
if _, err := svc.UpsertServiceProfile(ctx, 42, p); err != nil {
t.Fatalf("remote-only profile rejected: %v", err)
}
}
func TestUpsertRequiresAtLeastOneService(t *testing.T) {
svc := newTestService()
p := sampleProfile()
p.Services = nil
_, err := svc.UpsertServiceProfile(context.Background(), 42, p)
if !errors.Is(err, domain.ErrValidation) {
t.Fatalf("err = %v, want ErrValidation", err)
}
}
// 「沒建檔」與「建了一份空的」必須分得出來SP-01 的訂閱閘門靠這個差別。
func TestGetReportsNotFoundBeforeFirstUpsert(t *testing.T) {
svc := newTestService()
ctx := context.Background()
_, err := svc.GetServiceProfile(ctx, 42)
if !errors.Is(err, domain.ErrNotFound) {
t.Fatalf("err = %v, want ErrNotFound", err)
}
has, err := svc.HasServiceProfile(ctx, 42)
if err != nil || has {
t.Fatalf("HasServiceProfile = (%v, %v), want (false, nil)", has, err)
}
if _, err := svc.UpsertServiceProfile(ctx, 42, sampleProfile()); err != nil {
t.Fatalf("upsert: %v", err)
}
has, err = svc.HasServiceProfile(ctx, 42)
if err != nil || !has {
t.Fatalf("HasServiceProfile = (%v, %v), want (true, nil)", has, err)
}
}
func TestUpsertIsWholeDocumentReplacementAndKeepsCreatedAt(t *testing.T) {
svc := newTestService()
ctx := context.Background()
first, err := svc.UpsertServiceProfile(ctx, 42, sampleProfile())
if err != nil {
t.Fatalf("upsert: %v", err)
}
trimmed := sampleProfile()
trimmed.Services = trimmed.Services[:1]
trimmed.Forbidden = nil
second, err := svc.UpsertServiceProfile(ctx, 42, trimmed)
if err != nil {
t.Fatalf("second upsert: %v", err)
}
// 整份覆寫:刪掉的項目要真的消失,否則使用者永遠刪不掉一個服務或禁語。
if len(second.Services) != 1 {
t.Fatalf("services = %d, want 1 after replacement", len(second.Services))
}
if len(second.Forbidden) != 0 {
t.Fatalf("forbidden = %v, want empty after replacement", second.Forbidden)
}
if second.CreatedAt != first.CreatedAt {
t.Fatalf("created_at changed on update: %d → %d", first.CreatedAt, second.CreatedAt)
}
}
func TestProfilesAreIsolatedPerOwner(t *testing.T) {
svc := newTestService()
ctx := context.Background()
if _, err := svc.UpsertServiceProfile(ctx, 42, sampleProfile()); err != nil {
t.Fatalf("upsert: %v", err)
}
if _, err := svc.GetServiceProfile(ctx, 43); !errors.Is(err, domain.ErrNotFound) {
t.Fatalf("owner 43 saw owner 42's profile: %v", err)
}
}
func TestMatchesRegion(t *testing.T) {
p := &domain.ServiceProfile{ServiceAreas: []string{"TPE"}}
if !p.MatchesRegion("tpe") {
t.Fatal("TPE should match regardless of case")
}
if p.MatchesRegion("KHH") {
t.Fatal("KHH must not match a Taipei-only profile")
}
// 判不出縣市時不算符合,但也不該被當成不符合 —— 那是呼叫端的 unknown 分支。
if p.MatchesRegion("") {
t.Fatal("empty region must not count as a match")
}
remote := &domain.ServiceProfile{RemoteOk: true}
if !remote.MatchesRegion("") {
t.Fatal("remote_ok profile serves any region")
}
}