public_settings_injection_schema_test.go 2.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
  1. package dto
  2. import (
  3. "reflect"
  4. "strings"
  5. "testing"
  6. "github.com/Wei-Shaw/sub2api/internal/service"
  7. )
  8. // TestPublicSettingsInjectionPayload_SchemaDoesNotDrift guarantees the SSR
  9. // injection struct exposes every JSON field consumed by the frontend.
  10. //
  11. // Why this test exists: before we extracted a named PublicSettingsInjectionPayload
  12. // type, the inline struct was manually kept in sync with dto.PublicSettings and
  13. // drifted — ChannelMonitorEnabled / AvailableChannelsEnabled were missing, which
  14. // made the frontend read `undefined` on refresh and hide the "可用渠道" menu
  15. // until the async /api/v1/settings/public round-trip finished.
  16. //
  17. // This test compares the two JSON-tag sets and fails if injection is missing
  18. // any field that dto.PublicSettings exposes. Adding a new feature flag with
  19. // only a DTO entry will fail this test until the injection struct is updated.
  20. //
  21. // Intentional exclusions (fields present on dto.PublicSettings that SSR does
  22. // not need to inject) are listed in `dtoOnlyFields` below with a reason.
  23. func TestPublicSettingsInjectionPayload_SchemaDoesNotDrift(t *testing.T) {
  24. injection := jsonTags(reflect.TypeOf(service.PublicSettingsInjectionPayload{}))
  25. dtoKeys := jsonTags(reflect.TypeOf(PublicSettings{}))
  26. // Fields that legitimately live only on the DTO. Keep tiny; document each.
  27. dtoOnlyFields := map[string]string{
  28. // force_email_on_third_party_signup lives on the DTO but is not injected via SSR.
  29. "force_email_on_third_party_signup": "auth-source default, not a feature flag",
  30. }
  31. var missing []string
  32. for key := range dtoKeys {
  33. if _, ok := injection[key]; ok {
  34. continue
  35. }
  36. if _, allowed := dtoOnlyFields[key]; allowed {
  37. continue
  38. }
  39. missing = append(missing, key)
  40. }
  41. if len(missing) > 0 {
  42. t.Fatalf("service.PublicSettingsInjectionPayload is missing JSON fields present on dto.PublicSettings: %s\n"+
  43. "add the field to PublicSettingsInjectionPayload (and GetPublicSettingsForInjection), or "+
  44. "document the exclusion in dtoOnlyFields with a reason.", strings.Join(missing, ", "))
  45. }
  46. }
  47. func jsonTags(t reflect.Type) map[string]struct{} {
  48. out := make(map[string]struct{})
  49. for i := 0; i < t.NumField(); i++ {
  50. f := t.Field(i)
  51. tag := f.Tag.Get("json")
  52. if tag == "" || tag == "-" {
  53. continue
  54. }
  55. name := strings.SplitN(tag, ",", 2)[0]
  56. if name == "" {
  57. continue
  58. }
  59. out[name] = struct{}{}
  60. }
  61. return out
  62. }