maintenance_handlers_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. package handlers
  2. import (
  3. "net/url"
  4. "testing"
  5. "github.com/seaweedfs/seaweedfs/weed/admin/config"
  6. "github.com/seaweedfs/seaweedfs/weed/worker/tasks"
  7. "github.com/seaweedfs/seaweedfs/weed/worker/tasks/balance"
  8. "github.com/seaweedfs/seaweedfs/weed/worker/tasks/base"
  9. "github.com/seaweedfs/seaweedfs/weed/worker/tasks/erasure_coding"
  10. "github.com/seaweedfs/seaweedfs/weed/worker/tasks/vacuum"
  11. )
  12. func TestParseTaskConfigFromForm_WithEmbeddedStruct(t *testing.T) {
  13. // Create a maintenance handlers instance for testing
  14. h := &MaintenanceHandlers{}
  15. // Test with balance config
  16. t.Run("Balance Config", func(t *testing.T) {
  17. // Simulate form data
  18. formData := url.Values{
  19. "enabled": {"on"}, // checkbox field
  20. "scan_interval_seconds_value": {"30"}, // interval field
  21. "scan_interval_seconds_unit": {"minutes"}, // interval unit
  22. "max_concurrent": {"2"}, // number field
  23. "imbalance_threshold": {"0.15"}, // float field
  24. "min_server_count": {"3"}, // number field
  25. }
  26. // Get schema
  27. schema := tasks.GetTaskConfigSchema("balance")
  28. if schema == nil {
  29. t.Fatal("Failed to get balance schema")
  30. }
  31. // Create config instance
  32. config := &balance.Config{}
  33. // Parse form data
  34. err := h.parseTaskConfigFromForm(formData, schema, config)
  35. if err != nil {
  36. t.Fatalf("Failed to parse form data: %v", err)
  37. }
  38. // Verify embedded struct fields were set correctly
  39. if !config.Enabled {
  40. t.Errorf("Expected Enabled=true, got %v", config.Enabled)
  41. }
  42. if config.ScanIntervalSeconds != 1800 { // 30 minutes * 60
  43. t.Errorf("Expected ScanIntervalSeconds=1800, got %v", config.ScanIntervalSeconds)
  44. }
  45. if config.MaxConcurrent != 2 {
  46. t.Errorf("Expected MaxConcurrent=2, got %v", config.MaxConcurrent)
  47. }
  48. // Verify balance-specific fields were set correctly
  49. if config.ImbalanceThreshold != 0.15 {
  50. t.Errorf("Expected ImbalanceThreshold=0.15, got %v", config.ImbalanceThreshold)
  51. }
  52. if config.MinServerCount != 3 {
  53. t.Errorf("Expected MinServerCount=3, got %v", config.MinServerCount)
  54. }
  55. })
  56. // Test with vacuum config
  57. t.Run("Vacuum Config", func(t *testing.T) {
  58. // Simulate form data
  59. formData := url.Values{
  60. // "enabled" field omitted to simulate unchecked checkbox
  61. "scan_interval_seconds_value": {"4"}, // interval field
  62. "scan_interval_seconds_unit": {"hours"}, // interval unit
  63. "max_concurrent": {"3"}, // number field
  64. "garbage_threshold": {"0.4"}, // float field
  65. "min_volume_age_seconds_value": {"2"}, // interval field
  66. "min_volume_age_seconds_unit": {"days"}, // interval unit
  67. "min_interval_seconds_value": {"1"}, // interval field
  68. "min_interval_seconds_unit": {"days"}, // interval unit
  69. }
  70. // Get schema
  71. schema := tasks.GetTaskConfigSchema("vacuum")
  72. if schema == nil {
  73. t.Fatal("Failed to get vacuum schema")
  74. }
  75. // Create config instance
  76. config := &vacuum.Config{}
  77. // Parse form data
  78. err := h.parseTaskConfigFromForm(formData, schema, config)
  79. if err != nil {
  80. t.Fatalf("Failed to parse form data: %v", err)
  81. }
  82. // Verify embedded struct fields were set correctly
  83. if config.Enabled {
  84. t.Errorf("Expected Enabled=false, got %v", config.Enabled)
  85. }
  86. if config.ScanIntervalSeconds != 14400 { // 4 hours * 3600
  87. t.Errorf("Expected ScanIntervalSeconds=14400, got %v", config.ScanIntervalSeconds)
  88. }
  89. if config.MaxConcurrent != 3 {
  90. t.Errorf("Expected MaxConcurrent=3, got %v", config.MaxConcurrent)
  91. }
  92. // Verify vacuum-specific fields were set correctly
  93. if config.GarbageThreshold != 0.4 {
  94. t.Errorf("Expected GarbageThreshold=0.4, got %v", config.GarbageThreshold)
  95. }
  96. if config.MinVolumeAgeSeconds != 172800 { // 2 days * 86400
  97. t.Errorf("Expected MinVolumeAgeSeconds=172800, got %v", config.MinVolumeAgeSeconds)
  98. }
  99. if config.MinIntervalSeconds != 86400 { // 1 day * 86400
  100. t.Errorf("Expected MinIntervalSeconds=86400, got %v", config.MinIntervalSeconds)
  101. }
  102. })
  103. // Test with erasure coding config
  104. t.Run("Erasure Coding Config", func(t *testing.T) {
  105. // Simulate form data
  106. formData := url.Values{
  107. "enabled": {"on"}, // checkbox field
  108. "scan_interval_seconds_value": {"2"}, // interval field
  109. "scan_interval_seconds_unit": {"hours"}, // interval unit
  110. "max_concurrent": {"1"}, // number field
  111. "quiet_for_seconds_value": {"10"}, // interval field
  112. "quiet_for_seconds_unit": {"minutes"}, // interval unit
  113. "fullness_ratio": {"0.85"}, // float field
  114. "collection_filter": {"test_collection"}, // string field
  115. "min_size_mb": {"50"}, // number field
  116. }
  117. // Get schema
  118. schema := tasks.GetTaskConfigSchema("erasure_coding")
  119. if schema == nil {
  120. t.Fatal("Failed to get erasure_coding schema")
  121. }
  122. // Create config instance
  123. config := &erasure_coding.Config{}
  124. // Parse form data
  125. err := h.parseTaskConfigFromForm(formData, schema, config)
  126. if err != nil {
  127. t.Fatalf("Failed to parse form data: %v", err)
  128. }
  129. // Verify embedded struct fields were set correctly
  130. if !config.Enabled {
  131. t.Errorf("Expected Enabled=true, got %v", config.Enabled)
  132. }
  133. if config.ScanIntervalSeconds != 7200 { // 2 hours * 3600
  134. t.Errorf("Expected ScanIntervalSeconds=7200, got %v", config.ScanIntervalSeconds)
  135. }
  136. if config.MaxConcurrent != 1 {
  137. t.Errorf("Expected MaxConcurrent=1, got %v", config.MaxConcurrent)
  138. }
  139. // Verify erasure coding-specific fields were set correctly
  140. if config.QuietForSeconds != 600 { // 10 minutes * 60
  141. t.Errorf("Expected QuietForSeconds=600, got %v", config.QuietForSeconds)
  142. }
  143. if config.FullnessRatio != 0.85 {
  144. t.Errorf("Expected FullnessRatio=0.85, got %v", config.FullnessRatio)
  145. }
  146. if config.CollectionFilter != "test_collection" {
  147. t.Errorf("Expected CollectionFilter='test_collection', got %v", config.CollectionFilter)
  148. }
  149. if config.MinSizeMB != 50 {
  150. t.Errorf("Expected MinSizeMB=50, got %v", config.MinSizeMB)
  151. }
  152. })
  153. }
  154. func TestConfigurationValidation(t *testing.T) {
  155. // Test that config structs can be validated and converted to protobuf format
  156. taskTypes := []struct {
  157. name string
  158. config interface{}
  159. }{
  160. {
  161. "balance",
  162. &balance.Config{
  163. BaseConfig: base.BaseConfig{
  164. Enabled: true,
  165. ScanIntervalSeconds: 2400,
  166. MaxConcurrent: 3,
  167. },
  168. ImbalanceThreshold: 0.18,
  169. MinServerCount: 4,
  170. },
  171. },
  172. {
  173. "vacuum",
  174. &vacuum.Config{
  175. BaseConfig: base.BaseConfig{
  176. Enabled: false,
  177. ScanIntervalSeconds: 7200,
  178. MaxConcurrent: 2,
  179. },
  180. GarbageThreshold: 0.35,
  181. MinVolumeAgeSeconds: 86400,
  182. MinIntervalSeconds: 604800,
  183. },
  184. },
  185. {
  186. "erasure_coding",
  187. &erasure_coding.Config{
  188. BaseConfig: base.BaseConfig{
  189. Enabled: true,
  190. ScanIntervalSeconds: 3600,
  191. MaxConcurrent: 1,
  192. },
  193. QuietForSeconds: 900,
  194. FullnessRatio: 0.9,
  195. CollectionFilter: "important",
  196. MinSizeMB: 100,
  197. },
  198. },
  199. }
  200. for _, test := range taskTypes {
  201. t.Run(test.name, func(t *testing.T) {
  202. // Test that configs can be converted to protobuf TaskPolicy
  203. switch cfg := test.config.(type) {
  204. case *balance.Config:
  205. policy := cfg.ToTaskPolicy()
  206. if policy == nil {
  207. t.Fatal("ToTaskPolicy returned nil")
  208. }
  209. if policy.Enabled != cfg.Enabled {
  210. t.Errorf("Expected Enabled=%v, got %v", cfg.Enabled, policy.Enabled)
  211. }
  212. if policy.MaxConcurrent != int32(cfg.MaxConcurrent) {
  213. t.Errorf("Expected MaxConcurrent=%v, got %v", cfg.MaxConcurrent, policy.MaxConcurrent)
  214. }
  215. case *vacuum.Config:
  216. policy := cfg.ToTaskPolicy()
  217. if policy == nil {
  218. t.Fatal("ToTaskPolicy returned nil")
  219. }
  220. if policy.Enabled != cfg.Enabled {
  221. t.Errorf("Expected Enabled=%v, got %v", cfg.Enabled, policy.Enabled)
  222. }
  223. if policy.MaxConcurrent != int32(cfg.MaxConcurrent) {
  224. t.Errorf("Expected MaxConcurrent=%v, got %v", cfg.MaxConcurrent, policy.MaxConcurrent)
  225. }
  226. case *erasure_coding.Config:
  227. policy := cfg.ToTaskPolicy()
  228. if policy == nil {
  229. t.Fatal("ToTaskPolicy returned nil")
  230. }
  231. if policy.Enabled != cfg.Enabled {
  232. t.Errorf("Expected Enabled=%v, got %v", cfg.Enabled, policy.Enabled)
  233. }
  234. if policy.MaxConcurrent != int32(cfg.MaxConcurrent) {
  235. t.Errorf("Expected MaxConcurrent=%v, got %v", cfg.MaxConcurrent, policy.MaxConcurrent)
  236. }
  237. default:
  238. t.Fatalf("Unknown config type: %T", test.config)
  239. }
  240. // Test that configs can be validated
  241. switch cfg := test.config.(type) {
  242. case *balance.Config:
  243. if err := cfg.Validate(); err != nil {
  244. t.Errorf("Validation failed: %v", err)
  245. }
  246. case *vacuum.Config:
  247. if err := cfg.Validate(); err != nil {
  248. t.Errorf("Validation failed: %v", err)
  249. }
  250. case *erasure_coding.Config:
  251. if err := cfg.Validate(); err != nil {
  252. t.Errorf("Validation failed: %v", err)
  253. }
  254. }
  255. })
  256. }
  257. }
  258. func TestParseFieldFromForm_EdgeCases(t *testing.T) {
  259. h := &MaintenanceHandlers{}
  260. // Test checkbox parsing (boolean fields)
  261. t.Run("Checkbox Fields", func(t *testing.T) {
  262. tests := []struct {
  263. name string
  264. formData url.Values
  265. expectedValue bool
  266. }{
  267. {"Checked checkbox", url.Values{"test_field": {"on"}}, true},
  268. {"Unchecked checkbox", url.Values{}, false},
  269. {"Empty value checkbox", url.Values{"test_field": {""}}, true}, // Present but empty means checked
  270. }
  271. for _, test := range tests {
  272. t.Run(test.name, func(t *testing.T) {
  273. schema := &tasks.TaskConfigSchema{
  274. Schema: config.Schema{
  275. Fields: []*config.Field{
  276. {
  277. JSONName: "test_field",
  278. Type: config.FieldTypeBool,
  279. InputType: "checkbox",
  280. },
  281. },
  282. },
  283. }
  284. type TestConfig struct {
  285. TestField bool `json:"test_field"`
  286. }
  287. config := &TestConfig{}
  288. err := h.parseTaskConfigFromForm(test.formData, schema, config)
  289. if err != nil {
  290. t.Fatalf("parseTaskConfigFromForm failed: %v", err)
  291. }
  292. if config.TestField != test.expectedValue {
  293. t.Errorf("Expected %v, got %v", test.expectedValue, config.TestField)
  294. }
  295. })
  296. }
  297. })
  298. // Test interval parsing
  299. t.Run("Interval Fields", func(t *testing.T) {
  300. tests := []struct {
  301. name string
  302. value string
  303. unit string
  304. expectedSecs int
  305. }{
  306. {"Minutes", "30", "minutes", 1800},
  307. {"Hours", "2", "hours", 7200},
  308. {"Days", "1", "days", 86400},
  309. }
  310. for _, test := range tests {
  311. t.Run(test.name, func(t *testing.T) {
  312. formData := url.Values{
  313. "test_field_value": {test.value},
  314. "test_field_unit": {test.unit},
  315. }
  316. schema := &tasks.TaskConfigSchema{
  317. Schema: config.Schema{
  318. Fields: []*config.Field{
  319. {
  320. JSONName: "test_field",
  321. Type: config.FieldTypeInterval,
  322. InputType: "interval",
  323. },
  324. },
  325. },
  326. }
  327. type TestConfig struct {
  328. TestField int `json:"test_field"`
  329. }
  330. config := &TestConfig{}
  331. err := h.parseTaskConfigFromForm(formData, schema, config)
  332. if err != nil {
  333. t.Fatalf("parseTaskConfigFromForm failed: %v", err)
  334. }
  335. if config.TestField != test.expectedSecs {
  336. t.Errorf("Expected %d seconds, got %d", test.expectedSecs, config.TestField)
  337. }
  338. })
  339. }
  340. })
  341. }